From 2946a6fcf704ef0b6e64d42772fdd65a609fb857 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 8 Mar 2026 05:13:02 +0530 Subject: [PATCH 1/4] fix the database connection is closing issue. Signed-off-by: krishna2323 --- .../IDBKeyValProvider/createStore.ts | 59 +++- .../unit/storage/providers/createStoreTest.ts | 320 ++++++++++++++++++ 2 files changed, 370 insertions(+), 9 deletions(-) create mode 100644 tests/unit/storage/providers/createStoreTest.ts diff --git a/lib/storage/providers/IDBKeyValProvider/createStore.ts b/lib/storage/providers/IDBKeyValProvider/createStore.ts index b9d3da671..ca57a320b 100644 --- a/lib/storage/providers/IDBKeyValProvider/createStore.ts +++ b/lib/storage/providers/IDBKeyValProvider/createStore.ts @@ -1,6 +1,6 @@ import * as IDB from 'idb-keyval'; import type {UseStore} from 'idb-keyval'; -import {logInfo} from '../../../Logger'; +import {logAlert, logInfo} from '../../../Logger'; // This is a copy of the createStore function from idb-keyval, we need a custom implementation // because we need to create the database manually in order to ensure that the store exists before we use it. @@ -8,6 +8,30 @@ import {logInfo} from '../../../Logger'; // source: https://github.com/jakearchibald/idb-keyval/blob/9d19315b4a83897df1e0193dccdc29f78466a0f3/src/index.ts#L12 function createStore(dbName: string, storeName: string): UseStore { let dbp: Promise | undefined; + let closedBy: 'browser' | 'versionchange' | 'verifyStoreExists' | 'unknown' = 'unknown'; + + const attachHandlers = (db: IDBDatabase) => { + // It seems like Safari sometimes likes to just close the connection. + // It's supposed to fire this event when that happens. Let's hope it does! + // eslint-disable-next-line no-param-reassign + db.onclose = () => { + logInfo('IDB connection closed by browser', {dbName, storeName}); + closedBy = 'browser'; + dbp = undefined; + }; + + // When another tab triggers a DB version upgrade, we must close the connection + // to unblock the upgrade; otherwise the other tab's open request hangs indefinitely. + // https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/versionchange_event + // eslint-disable-next-line no-param-reassign + db.onversionchange = () => { + logInfo('IDB connection closing due to versionchange', {dbName, storeName}); + closedBy = 'versionchange'; + db.close(); + dbp = undefined; + }; + }; + const getDB = () => { if (dbp) return dbp; const request = indexedDB.open(dbName); @@ -15,12 +39,7 @@ function createStore(dbName: string, storeName: string): UseStore { dbp = IDB.promisifyRequest(request); dbp.then( - (db) => { - // It seems like Safari sometimes likes to just close the connection. - // It's supposed to fire this event when that happens. Let's hope it does! - // eslint-disable-next-line no-param-reassign - db.onclose = () => (dbp = undefined); - }, + attachHandlers, // eslint-disable-next-line @typescript-eslint/no-empty-function () => {}, ); @@ -36,6 +55,7 @@ function createStore(dbName: string, storeName: string): UseStore { logInfo(`Store ${storeName} does not exist in database ${dbName}.`); const nextVersion = db.version + 1; + closedBy = 'verifyStoreExists'; db.close(); const request = indexedDB.open(dbName, nextVersion); @@ -50,13 +70,34 @@ function createStore(dbName: string, storeName: string): UseStore { }; dbp = IDB.promisifyRequest(request); + // eslint-disable-next-line @typescript-eslint/no-empty-function + dbp.then(attachHandlers, () => {}); return dbp; }; - return (txMode, callback) => - getDB() + function executeTransaction(txMode: IDBTransactionMode, callback: (store: IDBObjectStore) => T | PromiseLike): Promise { + return getDB() .then(verifyStoreExists) .then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName))); + } + + return (txMode, callback) => + executeTransaction(txMode, callback).catch((error) => { + if (error instanceof DOMException && error.name === 'InvalidStateError') { + logAlert('IDB InvalidStateError, retrying with fresh connection', { + dbName, + storeName, + txMode, + closedBy, + errorMessage: error.message, + }); + dbp = undefined; + closedBy = 'unknown'; + // Retry only once — this call is not wrapped, so if it also fails the error propagates normally. + return executeTransaction(txMode, callback); + } + throw error; + }); } export default createStore; diff --git a/tests/unit/storage/providers/createStoreTest.ts b/tests/unit/storage/providers/createStoreTest.ts new file mode 100644 index 000000000..c0dad505e --- /dev/null +++ b/tests/unit/storage/providers/createStoreTest.ts @@ -0,0 +1,320 @@ +import * as IDB from 'idb-keyval'; +import createStore from '../../../../lib/storage/providers/IDBKeyValProvider/createStore'; +import * as Logger from '../../../../lib/Logger'; + +const STORE_NAME = 'teststore'; +let testDbCounter = 0; + +function uniqueDBName() { + testDbCounter += 1; + return `TestCreateStoreDB_${testDbCounter}`; +} + +/** + * Captures the internal IDBDatabase instance used by a store by intercepting + * the first db.transaction() call. + */ +function captureDB(store: ReturnType): Promise { + return new Promise((resolve) => { + const original = IDBDatabase.prototype.transaction; + const spy = jest.spyOn(IDBDatabase.prototype, 'transaction').mockImplementation(function (this: IDBDatabase, ...args) { + spy.mockRestore(); + resolve(this); + return original.apply(this, args); + }); + store('readonly', (s) => IDB.promisifyRequest(s.getAllKeys())); + }); +} + +describe('createStore - connection resilience', () => { + let logAlertSpy: jest.SpyInstance; + let logInfoSpy: jest.SpyInstance; + + beforeEach(() => { + logAlertSpy = jest.spyOn(Logger, 'logAlert'); + logInfoSpy = jest.spyOn(Logger, 'logInfo'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('InvalidStateError retry', () => { + it('should retry once and succeed when db.transaction throws InvalidStateError', 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('The database connection is closing.', 'InvalidStateError'); + } + return original.apply(this, args); + }); + + const result = await store('readonly', (s) => IDB.promisifyRequest(s.get('key1'))); + + expect(result).toBe('initial'); + expect(callCount).toBe(2); + }); + + it('should propagate InvalidStateError if retry also fails', async () => { + const store = createStore(uniqueDBName(), STORE_NAME); + + await store('readwrite', (s) => { + s.put('value', 'key1'); + return IDB.promisifyRequest(s.transaction); + }); + + jest.spyOn(IDBDatabase.prototype, 'transaction').mockImplementation(() => { + throw new DOMException('The database connection is closing.', 'InvalidStateError'); + }); + + await expect(store('readonly', (s) => IDB.promisifyRequest(s.get('key1')))).rejects.toThrow(DOMException); + expect(logAlertSpy).toHaveBeenCalledTimes(1); + }); + + it('should not retry on non-InvalidStateError DOMException', async () => { + const store = createStore(uniqueDBName(), STORE_NAME); + + await store('readwrite', (s) => { + s.put('value', 'key1'); + return IDB.promisifyRequest(s.transaction); + }); + + let callCount = 0; + jest.spyOn(IDBDatabase.prototype, 'transaction').mockImplementation(() => { + callCount += 1; + throw new DOMException('Not found', 'NotFoundError'); + }); + + await expect(store('readonly', (s) => IDB.promisifyRequest(s.get('key1')))).rejects.toThrow(DOMException); + expect(callCount).toBe(1); + expect(logAlertSpy).not.toHaveBeenCalled(); + }); + + it('should not retry on non-DOMException errors', async () => { + const store = createStore(uniqueDBName(), STORE_NAME); + + await store('readwrite', (s) => { + s.put('value', 'key1'); + return IDB.promisifyRequest(s.transaction); + }); + + let callCount = 0; + jest.spyOn(IDBDatabase.prototype, 'transaction').mockImplementation(() => { + callCount += 1; + throw new TypeError('Something went wrong'); + }); + + await expect(store('readonly', (s) => IDB.promisifyRequest(s.get('key1')))).rejects.toThrow(TypeError); + expect(callCount).toBe(1); + expect(logAlertSpy).not.toHaveBeenCalled(); + }); + + it('should preserve data integrity after a successful retry', async () => { + const store = createStore(uniqueDBName(), STORE_NAME); + + await store('readwrite', (s) => { + s.put('existing', 'key0'); + 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('The database connection is closing.', 'InvalidStateError'); + } + return original.apply(this, args); + }); + + await store('readwrite', (s) => { + s.put('retried_value', 'key1'); + return IDB.promisifyRequest(s.transaction); + }); + + jest.restoreAllMocks(); + logAlertSpy = jest.spyOn(Logger, 'logAlert'); + + const result = await store('readonly', (s) => IDB.promisifyRequest(s.get('key1'))); + expect(result).toBe('retried_value'); + }); + }); + + describe('diagnostic logging', () => { + it('should log alert with all diagnostic fields on retry', async () => { + const dbName = uniqueDBName(); + const store = createStore(dbName, STORE_NAME); + + await store('readwrite', (s) => { + s.put('value', '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('The database connection is closing.', 'InvalidStateError'); + } + return original.apply(this, args); + }); + + await store('readwrite', (s) => { + s.put('value2', 'key2'); + return IDB.promisifyRequest(s.transaction); + }); + + expect(logAlertSpy).toHaveBeenCalledWith('IDB InvalidStateError, retrying with fresh connection', { + dbName, + storeName: STORE_NAME, + txMode: 'readwrite', + closedBy: 'unknown', + errorMessage: 'The database connection is closing.', + }); + }); + + it('should log closedBy as "browser" when onclose preceded the error', async () => { + const dbName = uniqueDBName(); + const store = createStore(dbName, STORE_NAME); + + const db = await captureDB(store); + db.onclose!.call(db, new Event('close')); + + 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('The database connection is closing.', 'InvalidStateError'); + } + return original.apply(this, args); + }); + + await store('readonly', (s) => IDB.promisifyRequest(s.getAllKeys())); + + expect(logAlertSpy).toHaveBeenCalledWith('IDB InvalidStateError, retrying with fresh connection', expect.objectContaining({closedBy: 'browser'})); + }); + + it('should log closedBy as "versionchange" when onversionchange preceded the error', async () => { + const dbName = uniqueDBName(); + const store = createStore(dbName, STORE_NAME); + + const db = await captureDB(store); + // @ts-expect-error -- our handler ignores the event argument + db.onversionchange!.call(db, new Event('versionchange')); + + 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('The database connection is closing.', 'InvalidStateError'); + } + return original.apply(this, args); + }); + + await store('readonly', (s) => IDB.promisifyRequest(s.getAllKeys())); + + expect(logAlertSpy).toHaveBeenCalledWith('IDB InvalidStateError, retrying with fresh connection', expect.objectContaining({closedBy: 'versionchange'})); + }); + + it('should reset closedBy to "unknown" after a retry', async () => { + const dbName = uniqueDBName(); + const store = createStore(dbName, STORE_NAME); + + const db = await captureDB(store); + db.onclose!.call(db, new Event('close')); + + 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('The database connection is closing.', 'InvalidStateError'); + } + return original.apply(this, args); + }); + + // First operation triggers retry with closedBy: 'browser' + await store('readonly', (s) => IDB.promisifyRequest(s.getAllKeys())); + expect(logAlertSpy).toHaveBeenCalledWith('IDB InvalidStateError, retrying with fresh connection', expect.objectContaining({closedBy: 'browser'})); + + logAlertSpy.mockClear(); + + // Force another InvalidStateError — closedBy should now be 'unknown' + callCount = 0; + + await store('readonly', (s) => IDB.promisifyRequest(s.getAllKeys())); + expect(logAlertSpy).toHaveBeenCalledWith('IDB InvalidStateError, retrying with fresh connection', expect.objectContaining({closedBy: 'unknown'})); + }); + }); + + describe('onclose handler', () => { + it('should log info when browser closes the connection', async () => { + const dbName = uniqueDBName(); + const store = createStore(dbName, STORE_NAME); + + const db = await captureDB(store); + db.onclose!.call(db, new Event('close')); + + expect(logInfoSpy).toHaveBeenCalledWith('IDB connection closed by browser', {dbName, storeName: STORE_NAME}); + }); + + it('should recover with a fresh connection after browser close', async () => { + const store = createStore(uniqueDBName(), STORE_NAME); + + await store('readwrite', (s) => { + s.put('value', 'key1'); + return IDB.promisifyRequest(s.transaction); + }); + + const db = await captureDB(store); + db.onclose!.call(db, new Event('close')); + + const result = await store('readonly', (s) => IDB.promisifyRequest(s.get('key1'))); + expect(result).toBe('value'); + }); + }); + + describe('onversionchange handler', () => { + it('should close connection and log when versionchange fires', async () => { + const dbName = uniqueDBName(); + const store = createStore(dbName, STORE_NAME); + + const db = await captureDB(store); + const closeSpy = jest.spyOn(db, 'close'); + + // @ts-expect-error -- our handler ignores the event argument + db.onversionchange!.call(db, new Event('versionchange')); + + expect(closeSpy).toHaveBeenCalled(); + expect(logInfoSpy).toHaveBeenCalledWith('IDB connection closing due to versionchange', {dbName, storeName: STORE_NAME}); + }); + + it('should recover with a fresh connection after versionchange', async () => { + const store = createStore(uniqueDBName(), STORE_NAME); + + await store('readwrite', (s) => { + s.put('value', 'key1'); + return IDB.promisifyRequest(s.transaction); + }); + + const db = await captureDB(store); + // @ts-expect-error -- our handler ignores the event argument + db.onversionchange!.call(db, new Event('versionchange')); + + const result = await store('readonly', (s) => IDB.promisifyRequest(s.get('key1'))); + expect(result).toBe('value'); + }); + }); +}); From 8aa7ec661a8d627bb07a1078b20306c1fdf74060 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 8 Mar 2026 05:16:32 +0530 Subject: [PATCH 2/4] fix tests. Signed-off-by: krishna2323 --- .../unit/storage/providers/createStoreTest.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/unit/storage/providers/createStoreTest.ts b/tests/unit/storage/providers/createStoreTest.ts index c0dad505e..7b7749941 100644 --- a/tests/unit/storage/providers/createStoreTest.ts +++ b/tests/unit/storage/providers/createStoreTest.ts @@ -14,16 +14,16 @@ function uniqueDBName() { * Captures the internal IDBDatabase instance used by a store by intercepting * the first db.transaction() call. */ -function captureDB(store: ReturnType): Promise { - return new Promise((resolve) => { - const original = IDBDatabase.prototype.transaction; - const spy = jest.spyOn(IDBDatabase.prototype, 'transaction').mockImplementation(function (this: IDBDatabase, ...args) { - spy.mockRestore(); - resolve(this); - return original.apply(this, args); - }); - store('readonly', (s) => IDB.promisifyRequest(s.getAllKeys())); +async function captureDB(store: ReturnType): Promise { + const captured: {db?: IDBDatabase} = {}; + const original = IDBDatabase.prototype.transaction; + const spy = jest.spyOn(IDBDatabase.prototype, 'transaction').mockImplementation(function (this: IDBDatabase, ...args) { + captured.db = this; + spy.mockRestore(); + return original.apply(this, args); }); + await store('readonly', (s) => IDB.promisifyRequest(s.getAllKeys())); + return captured.db!; } describe('createStore - connection resilience', () => { From f351319ed8b7d35b1119c53bac88d20f3310d000 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 24 Mar 2026 14:42:12 +0530 Subject: [PATCH 3/4] address review comments. Signed-off-by: krishna2323 --- .../IDBKeyValProvider/createStore.ts | 23 ++++++++------ .../unit/storage/providers/createStoreTest.ts | 31 ++++++++++++------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/lib/storage/providers/IDBKeyValProvider/createStore.ts b/lib/storage/providers/IDBKeyValProvider/createStore.ts index ca57a320b..9ec7d1852 100644 --- a/lib/storage/providers/IDBKeyValProvider/createStore.ts +++ b/lib/storage/providers/IDBKeyValProvider/createStore.ts @@ -1,6 +1,8 @@ import * as IDB from 'idb-keyval'; import type {UseStore} from 'idb-keyval'; -import {logAlert, logInfo} from '../../../Logger'; +import * as Logger from '../../../Logger'; + +type ConnectionClosedReason = 'browser' | 'versionchange' | 'verifyStoreExists' | 'unknown'; // This is a copy of the createStore function from idb-keyval, we need a custom implementation // because we need to create the database manually in order to ensure that the store exists before we use it. @@ -8,14 +10,15 @@ import {logAlert, logInfo} from '../../../Logger'; // source: https://github.com/jakearchibald/idb-keyval/blob/9d19315b4a83897df1e0193dccdc29f78466a0f3/src/index.ts#L12 function createStore(dbName: string, storeName: string): UseStore { let dbp: Promise | undefined; - let closedBy: 'browser' | 'versionchange' | 'verifyStoreExists' | 'unknown' = 'unknown'; + let closedBy: ConnectionClosedReason = 'unknown'; const attachHandlers = (db: IDBDatabase) => { - // It seems like Safari sometimes likes to just close the connection. - // It's supposed to fire this event when that happens. Let's hope it does! + // Browsers may close idle IDB connections at any time, especially Safari. + // We clear the cached promise so the next operation opens a fresh connection. + // https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/close_event // eslint-disable-next-line no-param-reassign db.onclose = () => { - logInfo('IDB connection closed by browser', {dbName, storeName}); + Logger.logInfo('IDB connection closed by browser', {dbName, storeName}); closedBy = 'browser'; dbp = undefined; }; @@ -25,7 +28,7 @@ function createStore(dbName: string, storeName: string): UseStore { // https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/versionchange_event // eslint-disable-next-line no-param-reassign db.onversionchange = () => { - logInfo('IDB connection closing due to versionchange', {dbName, storeName}); + Logger.logInfo('IDB connection closing due to version change', {dbName, storeName}); closedBy = 'versionchange'; db.close(); dbp = undefined; @@ -53,7 +56,7 @@ function createStore(dbName: string, storeName: string): UseStore { return db; } - logInfo(`Store ${storeName} does not exist in database ${dbName}.`); + Logger.logInfo(`Store ${storeName} does not exist in database ${dbName}.`); const nextVersion = db.version + 1; closedBy = 'verifyStoreExists'; db.close(); @@ -65,7 +68,7 @@ function createStore(dbName: string, storeName: string): UseStore { return; } - logInfo(`Creating store ${storeName} in database ${dbName}.`); + Logger.logInfo(`Creating store ${storeName} in database ${dbName}.`); updatedDatabase.createObjectStore(storeName); }; @@ -81,10 +84,12 @@ function createStore(dbName: string, storeName: string): UseStore { .then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName))); } + // If the connection was closed between getDB() resolving and db.transaction() executing, + // the transaction throws InvalidStateError. We catch it and retry once with a fresh connection. return (txMode, callback) => executeTransaction(txMode, callback).catch((error) => { if (error instanceof DOMException && error.name === 'InvalidStateError') { - logAlert('IDB InvalidStateError, retrying with fresh connection', { + Logger.logAlert('IDB InvalidStateError, retrying with fresh connection', { dbName, storeName, txMode, diff --git a/tests/unit/storage/providers/createStoreTest.ts b/tests/unit/storage/providers/createStoreTest.ts index 7b7749941..daa1e7ed0 100644 --- a/tests/unit/storage/providers/createStoreTest.ts +++ b/tests/unit/storage/providers/createStoreTest.ts @@ -14,7 +14,7 @@ function uniqueDBName() { * Captures the internal IDBDatabase instance used by a store by intercepting * the first db.transaction() call. */ -async function captureDB(store: ReturnType): Promise { +async function captureDB(store: ReturnType): Promise { const captured: {db?: IDBDatabase} = {}; const original = IDBDatabase.prototype.transaction; const spy = jest.spyOn(IDBDatabase.prototype, 'transaction').mockImplementation(function (this: IDBDatabase, ...args) { @@ -23,10 +23,10 @@ async function captureDB(store: ReturnType): Promise IDB.promisifyRequest(s.getAllKeys())); - return captured.db!; + return captured.db; } -describe('createStore - connection resilience', () => { +describe('createStore', () => { let logAlertSpy: jest.SpyInstance; let logInfoSpy: jest.SpyInstance; @@ -188,7 +188,8 @@ describe('createStore - connection resilience', () => { const store = createStore(dbName, STORE_NAME); const db = await captureDB(store); - db.onclose!.call(db, new Event('close')); + expect(db).toBeDefined(); + db?.onclose?.call(db, new Event('close')); const original = IDBDatabase.prototype.transaction; let callCount = 0; @@ -210,8 +211,9 @@ describe('createStore - connection resilience', () => { const store = createStore(dbName, STORE_NAME); const db = await captureDB(store); + expect(db).toBeDefined(); // @ts-expect-error -- our handler ignores the event argument - db.onversionchange!.call(db, new Event('versionchange')); + db?.onversionchange?.call(db, new Event('versionchange')); const original = IDBDatabase.prototype.transaction; let callCount = 0; @@ -233,7 +235,8 @@ describe('createStore - connection resilience', () => { const store = createStore(dbName, STORE_NAME); const db = await captureDB(store); - db.onclose!.call(db, new Event('close')); + expect(db).toBeDefined(); + db?.onclose?.call(db, new Event('close')); const original = IDBDatabase.prototype.transaction; let callCount = 0; @@ -265,7 +268,8 @@ describe('createStore - connection resilience', () => { const store = createStore(dbName, STORE_NAME); const db = await captureDB(store); - db.onclose!.call(db, new Event('close')); + expect(db).toBeDefined(); + db?.onclose?.call(db, new Event('close')); expect(logInfoSpy).toHaveBeenCalledWith('IDB connection closed by browser', {dbName, storeName: STORE_NAME}); }); @@ -279,7 +283,8 @@ describe('createStore - connection resilience', () => { }); const db = await captureDB(store); - db.onclose!.call(db, new Event('close')); + expect(db).toBeDefined(); + db?.onclose?.call(db, new Event('close')); const result = await store('readonly', (s) => IDB.promisifyRequest(s.get('key1'))); expect(result).toBe('value'); @@ -292,13 +297,14 @@ describe('createStore - connection resilience', () => { const store = createStore(dbName, STORE_NAME); const db = await captureDB(store); - const closeSpy = jest.spyOn(db, 'close'); + expect(db).toBeDefined(); + const closeSpy = jest.spyOn(db!, 'close'); // @ts-expect-error -- our handler ignores the event argument - db.onversionchange!.call(db, new Event('versionchange')); + db?.onversionchange?.call(db, new Event('versionchange')); expect(closeSpy).toHaveBeenCalled(); - expect(logInfoSpy).toHaveBeenCalledWith('IDB connection closing due to versionchange', {dbName, storeName: STORE_NAME}); + expect(logInfoSpy).toHaveBeenCalledWith('IDB connection closing due to version change', {dbName, storeName: STORE_NAME}); }); it('should recover with a fresh connection after versionchange', async () => { @@ -310,8 +316,9 @@ describe('createStore - connection resilience', () => { }); const db = await captureDB(store); + expect(db).toBeDefined(); // @ts-expect-error -- our handler ignores the event argument - db.onversionchange!.call(db, new Event('versionchange')); + db?.onversionchange?.call(db, new Event('versionchange')); const result = await store('readonly', (s) => IDB.promisifyRequest(s.get('key1'))); expect(result).toBe('value'); From f02de7b9143baf34262882f90d6b038e671fee82 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 29 Mar 2026 23:15:19 +0530 Subject: [PATCH 4/4] remove closedBy diagnostic tracking per review feedback Signed-off-by: krishna2323 --- .../IDBKeyValProvider/createStore.ts | 8 -- .../unit/storage/providers/createStoreTest.ts | 79 ------------------- 2 files changed, 87 deletions(-) diff --git a/lib/storage/providers/IDBKeyValProvider/createStore.ts b/lib/storage/providers/IDBKeyValProvider/createStore.ts index 9ec7d1852..d7c6d0f8b 100644 --- a/lib/storage/providers/IDBKeyValProvider/createStore.ts +++ b/lib/storage/providers/IDBKeyValProvider/createStore.ts @@ -2,15 +2,12 @@ import * as IDB from 'idb-keyval'; import type {UseStore} from 'idb-keyval'; import * as Logger from '../../../Logger'; -type ConnectionClosedReason = 'browser' | 'versionchange' | 'verifyStoreExists' | 'unknown'; - // This is a copy of the createStore function from idb-keyval, we need a custom implementation // because we need to create the database manually in order to ensure that the store exists before we use it. // If the store does not exist, idb-keyval will throw an error // source: https://github.com/jakearchibald/idb-keyval/blob/9d19315b4a83897df1e0193dccdc29f78466a0f3/src/index.ts#L12 function createStore(dbName: string, storeName: string): UseStore { let dbp: Promise | undefined; - let closedBy: ConnectionClosedReason = 'unknown'; const attachHandlers = (db: IDBDatabase) => { // Browsers may close idle IDB connections at any time, especially Safari. @@ -19,7 +16,6 @@ function createStore(dbName: string, storeName: string): UseStore { // eslint-disable-next-line no-param-reassign db.onclose = () => { Logger.logInfo('IDB connection closed by browser', {dbName, storeName}); - closedBy = 'browser'; dbp = undefined; }; @@ -29,7 +25,6 @@ function createStore(dbName: string, storeName: string): UseStore { // eslint-disable-next-line no-param-reassign db.onversionchange = () => { Logger.logInfo('IDB connection closing due to version change', {dbName, storeName}); - closedBy = 'versionchange'; db.close(); dbp = undefined; }; @@ -58,7 +53,6 @@ function createStore(dbName: string, storeName: string): UseStore { Logger.logInfo(`Store ${storeName} does not exist in database ${dbName}.`); const nextVersion = db.version + 1; - closedBy = 'verifyStoreExists'; db.close(); const request = indexedDB.open(dbName, nextVersion); @@ -93,11 +87,9 @@ function createStore(dbName: string, storeName: string): UseStore { dbName, storeName, txMode, - closedBy, errorMessage: error.message, }); dbp = undefined; - closedBy = 'unknown'; // Retry only once — this call is not wrapped, so if it also fails the error propagates normally. return executeTransaction(txMode, callback); } diff --git a/tests/unit/storage/providers/createStoreTest.ts b/tests/unit/storage/providers/createStoreTest.ts index daa1e7ed0..231244706 100644 --- a/tests/unit/storage/providers/createStoreTest.ts +++ b/tests/unit/storage/providers/createStoreTest.ts @@ -178,88 +178,9 @@ describe('createStore', () => { dbName, storeName: STORE_NAME, txMode: 'readwrite', - closedBy: 'unknown', errorMessage: 'The database connection is closing.', }); }); - - it('should log closedBy as "browser" when onclose preceded the error', async () => { - const dbName = uniqueDBName(); - const store = createStore(dbName, STORE_NAME); - - const db = await captureDB(store); - expect(db).toBeDefined(); - db?.onclose?.call(db, new Event('close')); - - 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('The database connection is closing.', 'InvalidStateError'); - } - return original.apply(this, args); - }); - - await store('readonly', (s) => IDB.promisifyRequest(s.getAllKeys())); - - expect(logAlertSpy).toHaveBeenCalledWith('IDB InvalidStateError, retrying with fresh connection', expect.objectContaining({closedBy: 'browser'})); - }); - - it('should log closedBy as "versionchange" when onversionchange preceded the error', async () => { - const dbName = uniqueDBName(); - const store = createStore(dbName, STORE_NAME); - - const db = await captureDB(store); - expect(db).toBeDefined(); - // @ts-expect-error -- our handler ignores the event argument - db?.onversionchange?.call(db, new Event('versionchange')); - - 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('The database connection is closing.', 'InvalidStateError'); - } - return original.apply(this, args); - }); - - await store('readonly', (s) => IDB.promisifyRequest(s.getAllKeys())); - - expect(logAlertSpy).toHaveBeenCalledWith('IDB InvalidStateError, retrying with fresh connection', expect.objectContaining({closedBy: 'versionchange'})); - }); - - it('should reset closedBy to "unknown" after a retry', async () => { - const dbName = uniqueDBName(); - const store = createStore(dbName, STORE_NAME); - - const db = await captureDB(store); - expect(db).toBeDefined(); - db?.onclose?.call(db, new Event('close')); - - 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('The database connection is closing.', 'InvalidStateError'); - } - return original.apply(this, args); - }); - - // First operation triggers retry with closedBy: 'browser' - await store('readonly', (s) => IDB.promisifyRequest(s.getAllKeys())); - expect(logAlertSpy).toHaveBeenCalledWith('IDB InvalidStateError, retrying with fresh connection', expect.objectContaining({closedBy: 'browser'})); - - logAlertSpy.mockClear(); - - // Force another InvalidStateError — closedBy should now be 'unknown' - callCount = 0; - - await store('readonly', (s) => IDB.promisifyRequest(s.getAllKeys())); - expect(logAlertSpy).toHaveBeenCalledWith('IDB InvalidStateError, retrying with fresh connection', expect.objectContaining({closedBy: 'unknown'})); - }); }); describe('onclose handler', () => {