diff --git a/lib/storage/providers/IDBKeyValProvider/classifyError.ts b/lib/storage/providers/IDBKeyValProvider/classifyError.ts index 99f3c3e64..3633abdd0 100644 --- a/lib/storage/providers/IDBKeyValProvider/classifyError.ts +++ b/lib/storage/providers/IDBKeyValProvider/classifyError.ts @@ -33,7 +33,7 @@ function classifyIDBError(error: unknown): ValueOf { name.includes('aborterror') || message.includes('connection to indexed database server lost') || message.includes('connection is closing') || - // This is related to https://github.com/Expensify/react-native-onyx/pull/796 — remove this comment when #796 is merged. + message.includes('without an in-progress transaction') || message.includes('idb write transaction aborted without an error') ) { return StorageErrorClass.TRANSIENT; diff --git a/tests/unit/storage/providers/createStoreTest.ts b/tests/unit/storage/providers/createStoreTest.ts index 16f206747..9b834c6bb 100644 --- a/tests/unit/storage/providers/createStoreTest.ts +++ b/tests/unit/storage/providers/createStoreTest.ts @@ -132,6 +132,60 @@ describe('createStore', () => { expect(logAlertSpy).not.toHaveBeenCalled(); }); + it('should retry once and succeed on a WebKit "without an in-progress transaction" UnknownError', async () => { + const store = createStore(uniqueDBName(), STORE_NAME); + + await store('readwrite', (s) => { + s.put('initial', 'key1'); + return IDB.promisifyRequest(s.transaction); + }); + + const original = IDBDatabase.prototype.transaction; + let callCount = 0; + jest.spyOn(IDBDatabase.prototype, 'transaction').mockImplementation(function (this: IDBDatabase, ...args) { + callCount += 1; + if (callCount === 1) { + throw new DOMException('Attempt to get a record from database without an in-progress transaction', 'UnknownError'); + } + return original.apply(this, args); + }); + + const result = await store('readonly', (s) => IDB.promisifyRequest(s.get('key1'))); + + expect(result).toBe('initial'); + expect(callCount).toBe(2); + expect(logInfoSpy).toHaveBeenCalledWith(expect.stringContaining('IDB transient error'), expect.anything()); + }); + + it('should retry once and succeed on a WebKit "generate key" UnknownError during a write', async () => { + const store = createStore(uniqueDBName(), STORE_NAME); + + await store('readwrite', (s) => { + s.put('initial', 'key1'); + return IDB.promisifyRequest(s.transaction); + }); + + const original = IDBDatabase.prototype.transaction; + let callCount = 0; + jest.spyOn(IDBDatabase.prototype, 'transaction').mockImplementation(function (this: IDBDatabase, ...args) { + callCount += 1; + if (callCount === 1) { + throw new DOMException('Attempt to generate key in database without an in-progress transaction', 'UnknownError'); + } + return original.apply(this, args); + }); + + await store('readwrite', (s) => { + s.put('written', 'key1'); + return IDB.promisifyRequest(s.transaction); + }); + + const result = await store('readonly', (s) => IDB.promisifyRequest(s.get('key1'))); + + expect(result).toBe('written'); + expect(callCount).toBe(3); + }); + it('should preserve data integrity after a successful retry', async () => { const store = createStore(uniqueDBName(), STORE_NAME);