From f3288413693acb1735e7569c45601c9463c98d70 Mon Sep 17 00:00:00 2001 From: Amit Shankar Date: Wed, 26 Mar 2025 13:15:06 -0700 Subject: [PATCH 1/6] Add upgradeCallback handler for upgrade telemetry tracking --- src/IndexedDbProvider.ts | 81 +++++++++++++++++++++++++++++++++++++- src/ObjectStoreProvider.ts | 33 ++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/src/IndexedDbProvider.ts b/src/IndexedDbProvider.ts index 6cb57a6..b720804 100644 --- a/src/IndexedDbProvider.ts +++ b/src/IndexedDbProvider.ts @@ -41,6 +41,9 @@ import { IDBCloseConnectionPayload, OnCloseHandler, IObjectStoreProviderLogger, + UpgradeCallback, + UpgradeStep, + UpgradeMetadata, } from "./ObjectStoreProvider"; import { ItemType, KeyPathType, KeyType } from "./ObjectStoreProvider"; import { @@ -79,6 +82,7 @@ export class IndexedDbProvider extends DbProvider { private _dbFactory: IDBFactory; private _fakeComplicatedKeys: boolean; private _handleOnClose: OnCloseHandler | undefined = undefined; + private _upgradeCallback: UpgradeCallback | undefined = undefined; private _lockHelper: TransactionLockHelper | undefined; private logWriter: LogWriter; @@ -88,7 +92,8 @@ export class IndexedDbProvider extends DbProvider { explicitDbFactory?: IDBFactory, explicitDbFactorySupportsCompoundKeys?: boolean, handleOnClose?: OnCloseHandler, - logger?: IObjectStoreProviderLogger + logger?: IObjectStoreProviderLogger, + upgradeCallback?: UpgradeCallback ) { super(); @@ -118,6 +123,10 @@ export class IndexedDbProvider extends DbProvider { if (handleOnClose) { this._handleOnClose = handleOnClose; } + + if (upgradeCallback) { + this._upgradeCallback = upgradeCallback; + } } /** @@ -181,6 +190,8 @@ export class IndexedDbProvider extends DbProvider { const dbOpen = this._dbFactory.open(dbName, schema.version); let migrationPutters: Promise[] = []; + const upgradeSteps: UpgradeStep[] = []; + let upgradeMetadata: UpgradeMetadata | undefined; dbOpen.onupgradeneeded = (event) => { const db: IDBDatabase = dbOpen.result; @@ -194,6 +205,11 @@ export class IndexedDbProvider extends DbProvider { throw new Error("onupgradeneeded: target is null!"); } + upgradeMetadata = { + oldVersion: event.oldVersion, + newVersion: schema.version, + }; + // Avoid clearing object stores when event.oldVersion returns 0. // oldVersion returns 0 if db doesn't exist yet: https://developer.mozilla.org/en-US/docs/Web/API/IDBVersionChangeEvent/oldVersion if (event.oldVersion) { @@ -208,6 +224,11 @@ export class IndexedDbProvider extends DbProvider { ); each(db.objectStoreNames, (name) => { db.deleteObjectStore(name); + upgradeSteps.push({ + step: "DeleteOldVersion", + storeName: name, + timestamp: Date.now(), + }); }); } @@ -215,6 +236,11 @@ export class IndexedDbProvider extends DbProvider { each(db.objectStoreNames, (storeName) => { if (!some(schema.stores, (store) => store.name === storeName)) { db.deleteObjectStore(storeName); + upgradeSteps.push({ + step: "DeleteDeadStores", + storeName, + timestamp: Date.now(), + }); } }); @@ -245,6 +271,13 @@ export class IndexedDbProvider extends DbProvider { store = db.createObjectStore(storeSchema.name, { keyPath: primaryKeyPath, } as any); + + upgradeSteps.push({ + step: "CreatingIndex", + storeName: storeSchema.name, + storeExistedBefore: false, + timestamp: Date.now(), + }); } else { // store exists, might need to update indexes and migrate the data store = trans.objectStore(storeSchema.name); @@ -286,6 +319,12 @@ export class IndexedDbProvider extends DbProvider { storeName: storeSchema.name, indexName, }); + upgradeSteps.push({ + step: "DeletingIndex", + storeName: storeSchema.name, + indexName, + timestamp: Date.now(), + }); store.deleteIndex(indexName); } }); @@ -296,6 +335,12 @@ export class IndexedDbProvider extends DbProvider { let needsMigrate = false; // Check any indexes in the schema that need to be created each(storeSchema.indexes, (indexSchema) => { + upgradeSteps.push({ + step: "CreatingIndex", + storeName: storeSchema.name, + indexName: indexSchema.name, + timestamp: Date.now(), + }); if (!includes(store.indexNames, indexSchema.name)) { const keyPath = indexSchema.keyPath; if (this._fakeComplicatedKeys) { @@ -380,6 +425,12 @@ export class IndexedDbProvider extends DbProvider { }); if (needsMigrate) { + upgradeSteps.push({ + step: "CopyingData", + timestamp: Date.now(), + storeName: storeSchema.name, + }); + // Walk every element in the store and re-put it to fill out the new index. const fakeToken: TransactionToken = { storeNames: [storeSchema.name], @@ -444,6 +495,20 @@ export class IndexedDbProvider extends DbProvider { if (isActualMigration) { this.logWriter.log(`Opening db success`, { dbName }); } + + if (this._upgradeCallback && upgradeSteps.length > 0) { + upgradeSteps.push({ + step: "DBUpgradeComplete", + timestamp: Date.now(), + }); + this._upgradeCallback({ + status: "Success", + isCopyRequired: isActualMigration, + upgradeSteps, + ...upgradeMetadata, + }); + } + this._db = db; this._db.onclose = (event: Event) => { if (this._handleOnClose) { @@ -499,6 +564,20 @@ export class IndexedDbProvider extends DbProvider { dbName, } ); + + // Invoke the upgradeCallback with error details + if (this._upgradeCallback) { + this._upgradeCallback({ + status: "Error", + isCopyRequired: false, + upgradeSteps, + ...upgradeMetadata, + errorMessage: err + ? `${err?.message} ${err?.target?.error} ${err?.target?.error?.name}` + : "Unknown error occurred during upgrade", + }); + } + return Promise.reject(err); } ); diff --git a/src/ObjectStoreProvider.ts b/src/ObjectStoreProvider.ts index 03a7ac2..3b2a6b1 100644 --- a/src/ObjectStoreProvider.ts +++ b/src/ObjectStoreProvider.ts @@ -89,6 +89,39 @@ export type DBClosure = "unexpectedClosure" | "expectedClosure"; export type OnCloseHandler = (payload: IDBCloseConnectionPayload) => void; +export type Status = "Success" | "Error"; +export type StoreUpgradeStep = + | "DeleteOldVersion" + | "DeleteDeadStores" + | "DeletingIndex" + | "CreatingIndex" + | "CopyingData" + | "DBUpgradeComplete"; + +export type UpgradeStep = { + timestamp: number; + step: StoreUpgradeStep; + storeName?: string; + storeExistedBefore?: boolean; + indexName?: string; +}; + +export type UpgradeMetadata = { + oldVersion: number; + newVersion: number; +}; + +export type UpgradeStatus = { + status: Status; + upgradeSteps: UpgradeStep[]; + oldVersion?: number; + newVersion?: number; + isCopyRequired: boolean; + errorMessage?: string; +}; + +export type UpgradeCallback = (upgradeStatus: UpgradeStatus) => void; + // Interface type describing an index being opened for querying. export interface DbIndex { getAll( From 735a4e9f707131be6c0fffd856dada3ed9d949a0 Mon Sep 17 00:00:00 2001 From: Amit Shankar Date: Fri, 28 Mar 2025 17:30:42 -0700 Subject: [PATCH 2/6] update types --- index.ts | 6 ++++++ src/ObjectStoreProvider.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/index.ts b/index.ts index 6cabf2c..0f8036c 100644 --- a/index.ts +++ b/index.ts @@ -28,6 +28,12 @@ export { OnCloseHandler, openListOfProviders, IObjectStoreProviderLogger, + UpgradeStatus, + StoreUpgradeStep, + UpgradeStep, + UpgradeMetadata, + UpgradeDetails, + UpgradeCallback, } from "./src/ObjectStoreProvider"; export { isIE, diff --git a/src/ObjectStoreProvider.ts b/src/ObjectStoreProvider.ts index 3b2a6b1..3690dcd 100644 --- a/src/ObjectStoreProvider.ts +++ b/src/ObjectStoreProvider.ts @@ -89,7 +89,7 @@ export type DBClosure = "unexpectedClosure" | "expectedClosure"; export type OnCloseHandler = (payload: IDBCloseConnectionPayload) => void; -export type Status = "Success" | "Error"; +export type UpgradeStatus = "Success" | "Error"; export type StoreUpgradeStep = | "DeleteOldVersion" | "DeleteDeadStores" @@ -109,18 +109,18 @@ export type UpgradeStep = { export type UpgradeMetadata = { oldVersion: number; newVersion: number; + upgradeScenarioStartTime: number; + upgradePerformanceStartTimeMarker: number; }; -export type UpgradeStatus = { - status: Status; +export type UpgradeDetails = UpgradeMetadata & { + status: UpgradeStatus; upgradeSteps: UpgradeStep[]; - oldVersion?: number; - newVersion?: number; isCopyRequired: boolean; errorMessage?: string; }; -export type UpgradeCallback = (upgradeStatus: UpgradeStatus) => void; +export type UpgradeCallback = (upgradeDetails: UpgradeDetails) => void; // Interface type describing an index being opened for querying. export interface DbIndex { From 2e8de0bf282d946c3a6f5903d038e84298e46378 Mon Sep 17 00:00:00 2001 From: Amit Shankar Date: Sat, 29 Mar 2025 09:17:48 -0700 Subject: [PATCH 3/6] add upgradePerformanceStartTimeMarker and upgradeScenarioStartTime --- src/IndexedDbProvider.ts | 23 +++++++++++++++-------- src/ObjectStoreProvider.ts | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/IndexedDbProvider.ts b/src/IndexedDbProvider.ts index b720804..0af1195 100644 --- a/src/IndexedDbProvider.ts +++ b/src/IndexedDbProvider.ts @@ -191,7 +191,12 @@ export class IndexedDbProvider extends DbProvider { let migrationPutters: Promise[] = []; const upgradeSteps: UpgradeStep[] = []; - let upgradeMetadata: UpgradeMetadata | undefined; + let upgradeMetadata: UpgradeMetadata = { + oldVersion: 0, + newVersion: 0, + upgradeStartTimePerformanceMarker: 0, + upgradeScenarioStartTime: 0, + }; dbOpen.onupgradeneeded = (event) => { const db: IDBDatabase = dbOpen.result; @@ -208,6 +213,8 @@ export class IndexedDbProvider extends DbProvider { upgradeMetadata = { oldVersion: event.oldVersion, newVersion: schema.version, + upgradeScenarioStartTime: Date.now(), + upgradeStartTimePerformanceMarker: performance.now(), }; // Avoid clearing object stores when event.oldVersion returns 0. @@ -227,7 +234,7 @@ export class IndexedDbProvider extends DbProvider { upgradeSteps.push({ step: "DeleteOldVersion", storeName: name, - timestamp: Date.now(), + timestamp: performance.now(), }); }); } @@ -239,7 +246,7 @@ export class IndexedDbProvider extends DbProvider { upgradeSteps.push({ step: "DeleteDeadStores", storeName, - timestamp: Date.now(), + timestamp: performance.now(), }); } }); @@ -276,7 +283,7 @@ export class IndexedDbProvider extends DbProvider { step: "CreatingIndex", storeName: storeSchema.name, storeExistedBefore: false, - timestamp: Date.now(), + timestamp: performance.now(), }); } else { // store exists, might need to update indexes and migrate the data @@ -323,7 +330,7 @@ export class IndexedDbProvider extends DbProvider { step: "DeletingIndex", storeName: storeSchema.name, indexName, - timestamp: Date.now(), + timestamp: performance.now(), }); store.deleteIndex(indexName); } @@ -339,7 +346,7 @@ export class IndexedDbProvider extends DbProvider { step: "CreatingIndex", storeName: storeSchema.name, indexName: indexSchema.name, - timestamp: Date.now(), + timestamp: performance.now(), }); if (!includes(store.indexNames, indexSchema.name)) { const keyPath = indexSchema.keyPath; @@ -427,7 +434,7 @@ export class IndexedDbProvider extends DbProvider { if (needsMigrate) { upgradeSteps.push({ step: "CopyingData", - timestamp: Date.now(), + timestamp: performance.now(), storeName: storeSchema.name, }); @@ -499,7 +506,7 @@ export class IndexedDbProvider extends DbProvider { if (this._upgradeCallback && upgradeSteps.length > 0) { upgradeSteps.push({ step: "DBUpgradeComplete", - timestamp: Date.now(), + timestamp: performance.now(), }); this._upgradeCallback({ status: "Success", diff --git a/src/ObjectStoreProvider.ts b/src/ObjectStoreProvider.ts index 3690dcd..efd90bf 100644 --- a/src/ObjectStoreProvider.ts +++ b/src/ObjectStoreProvider.ts @@ -110,7 +110,7 @@ export type UpgradeMetadata = { oldVersion: number; newVersion: number; upgradeScenarioStartTime: number; - upgradePerformanceStartTimeMarker: number; + upgradeStartTimePerformanceMarker: number; }; export type UpgradeDetails = UpgradeMetadata & { From 21272815e912af76d7081ab010071f5cbb0770c3 Mon Sep 17 00:00:00 2001 From: Amit Shankar Date: Tue, 1 Apr 2025 16:52:32 -0700 Subject: [PATCH 4/6] update unit tests --- src/tests/ObjectStoreProvider.spec.ts | 314 ++++++++++++++++++++++++-- 1 file changed, 301 insertions(+), 13 deletions(-) diff --git a/src/tests/ObjectStoreProvider.spec.ts b/src/tests/ObjectStoreProvider.spec.ts index 2e4d847..f1ccb4d 100644 --- a/src/tests/ObjectStoreProvider.spec.ts +++ b/src/tests/ObjectStoreProvider.spec.ts @@ -12,12 +12,14 @@ import { FullTextTermResolution, IDBCloseConnectionPayload, OnCloseHandler, + UpgradeCallback, } from "../ObjectStoreProvider"; import { InMemoryProvider } from "../InMemoryProvider"; import { IndexedDbProvider } from "../IndexedDbProvider"; import { serializeValueToOrderableString } from "../ObjectStoreProviderUtils"; +import sinon = require("sinon"); type TestObj = { id?: string; val: string }; @@ -26,22 +28,40 @@ function openProvider( schema: DbSchema, wipeFirst: boolean, handleOnClose?: OnCloseHandler, - supportsRollback?: boolean + supportsRollback?: boolean, + upgradeCallback?: UpgradeCallback ) { let provider: DbProvider; - if (providerName === "memory-rbtree") { - provider = new InMemoryProvider("red-black-tree", supportsRollback); - } else if (providerName === "memory-btree") { - provider = new InMemoryProvider("b+tree", supportsRollback); - } else if (providerName === "indexeddb") { - provider = new IndexedDbProvider(); - } else if (providerName === "indexeddbfakekeys") { - provider = new IndexedDbProvider(undefined, false); - } else if (providerName === "indexeddbonclose") { - provider = new IndexedDbProvider(undefined, undefined, handleOnClose); - } else { - throw new Error("Provider not found for name: " + providerName); + + switch (providerName) { + case "memory-rbtree": + provider = new InMemoryProvider("red-black-tree", supportsRollback); + break; + case "memory-btree": + provider = new InMemoryProvider("b+tree", supportsRollback); + break; + case "indexeddb": + provider = new IndexedDbProvider(); + break; + case "indexeddbfakekeys": + provider = new IndexedDbProvider(undefined, false); + break; + case "indexeddbonclose": + provider = new IndexedDbProvider(undefined, undefined, handleOnClose); + break; + case "indexeddbonupgradehandler": + provider = new IndexedDbProvider( + undefined, + undefined, + handleOnClose, + undefined /** logger */, + upgradeCallback + ); + break; + default: + throw new Error("Provider not found for name: " + providerName); } + const dbName = "test"; return openListOfProviders([provider], dbName, schema, wipeFirst, false); } @@ -4518,6 +4538,274 @@ describe("ObjectStoreProvider", function () { (err) => done(err) ); }); + + describe("upgradeCallback", () => { + it("invokes upgradeHandler for success scenario with upgrade steps", (done) => { + const upgradeHandler: UpgradeCallback = (upgradeDetails) => { + try { + console.log(JSON.stringify(upgradeDetails)); + assert.equal(upgradeDetails.status, "Success"); + assert.equal(upgradeDetails.oldVersion, 1); + assert.equal(upgradeDetails.newVersion, 2); + assert.ok(upgradeDetails.upgradeSteps.length > 0); + assert.equal( + upgradeDetails.upgradeSteps[0].step, + "DBUpgradeComplete" + ); + done(); + } catch (err) { + done(err); + } + }; + + openProvider( + "indexeddbonupgradehandler", + { + version: 1, + stores: [ + { + name: "test", + primaryKeyPath: "id", + }, + ], + }, + true + ) + .then((prov) => { + return prov + .put("test", { id: "abc" }) + .then(() => prov.close()) + .catch((e) => prov.close().then(() => Promise.reject(e))); + }) + .then(() => { + return openProvider( + "indexeddbonupgradehandler", + { + version: 2, + stores: [ + { + name: "test", + primaryKeyPath: "id", + }, + { + name: "test2", + primaryKeyPath: "ttt", + }, + ], + }, + false, + undefined, + undefined, + upgradeHandler + ).then((prov) => { + return prov + .put("test2", { id: "def", ttt: "ghi" }) + .then(() => { + const p1 = prov.get("test", "abc").then((itemVal) => { + const item = itemVal as TestObj; + assert(!!item); + assert.equal(item.id, "abc"); + }); + const p2 = prov.get("test2", "abc").then((item) => { + assert(!item); + }); + return Promise.all([p1, p2]); + }) + .then(() => prov.close()) + .catch((e) => + prov.close().then(() => Promise.reject(e)) + ); + }); + }) + .then( + () => {}, + (err) => done(err) + ); + + // openProvider( + // "indexeddbonupgradehandler", + // { + // version: 1, + // lastUsableVersion: 1, + // stores: [ + // { + // name: "test", + // primaryKeyPath: "id", + // }, + // ], + // }, + // true + // ) + // .then((prov) => { + // return prov + // .put("test", { id: "abc", content: "ghi" }) + // .then(() => prov.close()) + // .catch((e) => prov.close().then(() => Promise.reject(e))); + // }) + // .then(() => + // openProvider( + // "indexeddbonupgradehandler", + // { + // version: 2, + // lastUsableVersion: 2, + // stores: [ + // { + // name: "test", + // primaryKeyPath: "id", + // }, + // ], + // }, + // true, + // undefined, + // undefined, + // upgradeHandler + // ).then((prov) => prov.close()) + // ); + }); + + // it("invokes upgradeHandler for failure scenario with upgrade steps", (done) => { + // const upgradeHandler: UpgradeCallback = (upgradeDetails) => { + // try { + // assert.equal(upgradeDetails.status, "Error"); + // assert.equal(upgradeDetails.oldVersion, 1); + // assert.equal(upgradeDetails.newVersion, 2); + // assert.ok(upgradeDetails.errorMessage); + // done(); + // } catch (err) { + // done(err); + // } + // }; + + // // Simulate a failure by providing an invalid schema + // openProvider( + // "indexeddbonupgradehandler", + // { + // version: 2, + // stores: [ + // { + // name: "test", + // primaryKeyPath: "nonexistentKeyPath", // Invalid key path + // }, + // ], + // }, + // true, + // undefined, + // undefined, + // upgradeHandler + // ).catch(() => { + // // Expected failure + // }); + // }); + + it("invokes upgradeHandler for failure scenario with upgrade steps", (done) => { + const upgradeHandler: UpgradeCallback = (upgradeDetails) => { + try { + assert.equal(upgradeDetails.status, "Error"); + assert.equal(upgradeDetails.oldVersion, 1); + assert.equal(upgradeDetails.newVersion, 2); + assert.ok(upgradeDetails.errorMessage); + } catch (err) { + done(err); + } + }; + + // Mock the IndexedDbProvider's open method to simulate a failure + const mockOpen = sinon + .stub(IndexedDbProvider.prototype, "open") + .callsFake(async function ( + _dbName: string, + _schema: DbSchema, + _wipeIfExists: boolean, + _verbose: boolean + ) { + // Simulate the IndexedDB open request + const dbOpen = { + onupgradeneeded: null as ((event: any) => void) | null, + onerror: null as ((event: any) => void) | null, + result: null as IDBDatabase | null, + }; + + // Simulate the onupgradeneeded event throwing an error + setTimeout(() => { + if (dbOpen.onupgradeneeded) { + const event = { + oldVersion: 1, + target: dbOpen, + }; + try { + dbOpen.onupgradeneeded(event); + } catch (e) { + if (dbOpen.onerror) { + dbOpen.onerror({ target: { error: e } }); + } + } + } + }, 0); + + return Promise.reject( + new Error("Simulated upgrade failure") + ); + }); + + // Simulate a failure by providing an invalid schema + openProvider( + "indexeddbonupgradehandler", + { + version: 2, + stores: [ + { + name: "test", + primaryKeyPath: "nonexistentKeyPath", // Invalid key path + }, + ], + }, + true, + undefined, + undefined, + upgradeHandler + ) + .catch(() => { + // Expected failure + }) + .finally(() => { + mockOpen.restore(); // Restore the original method + done(); + }); + }); + + // Additional test to ensure upgradeCallback is invoked even when no upgrade steps are required + it("invokes upgradeHandler for no-op upgrade scenario", (done) => { + const upgradeHandler: UpgradeCallback = (upgradeDetails) => { + try { + assert.equal(upgradeDetails.status, "Success"); + assert.equal(upgradeDetails.oldVersion, 1); + assert.equal(upgradeDetails.newVersion, 1); + assert.equal(upgradeDetails.upgradeSteps.length, 0); + done(); + } catch (err) { + done(err); + } + }; + + openProvider( + "indexeddbonupgradehandler", + { + version: 1, + lastUsableVersion: 1, + stores: [ + { + name: "test", + primaryKeyPath: "id", + }, + ], + }, + true, + undefined, + undefined, + upgradeHandler + ).then((prov) => prov.close()); + }); + }); } }); } From f7d1203ff5a047bf4f49d140e04b21b958620f94 Mon Sep 17 00:00:00 2001 From: Amit Shankar Date: Tue, 1 Apr 2025 22:27:11 -0700 Subject: [PATCH 5/6] v0.7.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ec85486..b4a56a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/objectstoreprovider", - "version": "0.7.1", + "version": "0.7.2", "description": "A cross-browser object store library", "author": "Mukundan Kavanur Kidambi ", "scripts": { From f6bfa4217001cd00fd3ab668bc5fa67d6a332c42 Mon Sep 17 00:00:00 2001 From: Amit Shankar Date: Wed, 2 Apr 2025 17:29:15 -0700 Subject: [PATCH 6/6] Remove sinon mock --- src/IndexedDbProvider.ts | 14 +- src/tests/ObjectStoreProvider.spec.ts | 438 ++++++++++---------------- 2 files changed, 175 insertions(+), 277 deletions(-) diff --git a/src/IndexedDbProvider.ts b/src/IndexedDbProvider.ts index 0af1195..7b27e1a 100644 --- a/src/IndexedDbProvider.ts +++ b/src/IndexedDbProvider.ts @@ -199,6 +199,13 @@ export class IndexedDbProvider extends DbProvider { }; dbOpen.onupgradeneeded = (event) => { + upgradeMetadata = { + oldVersion: event.oldVersion, + newVersion: schema.version, + upgradeScenarioStartTime: Date.now(), + upgradeStartTimePerformanceMarker: performance.now(), + }; + const db: IDBDatabase = dbOpen.result; const target = (event.currentTarget || event.target); const trans = target.transaction; @@ -210,13 +217,6 @@ export class IndexedDbProvider extends DbProvider { throw new Error("onupgradeneeded: target is null!"); } - upgradeMetadata = { - oldVersion: event.oldVersion, - newVersion: schema.version, - upgradeScenarioStartTime: Date.now(), - upgradeStartTimePerformanceMarker: performance.now(), - }; - // Avoid clearing object stores when event.oldVersion returns 0. // oldVersion returns 0 if db doesn't exist yet: https://developer.mozilla.org/en-US/docs/Web/API/IDBVersionChangeEvent/oldVersion if (event.oldVersion) { diff --git a/src/tests/ObjectStoreProvider.spec.ts b/src/tests/ObjectStoreProvider.spec.ts index f1ccb4d..e9a11f0 100644 --- a/src/tests/ObjectStoreProvider.spec.ts +++ b/src/tests/ObjectStoreProvider.spec.ts @@ -17,9 +17,9 @@ import { import { InMemoryProvider } from "../InMemoryProvider"; import { IndexedDbProvider } from "../IndexedDbProvider"; +import * as IndexedDbProviderModule from "../IndexedDbProvider"; import { serializeValueToOrderableString } from "../ObjectStoreProviderUtils"; -import sinon = require("sinon"); type TestObj = { id?: string; val: string }; @@ -79,7 +79,12 @@ describe("ObjectStoreProvider", function () { let provsToTest: string[]; provsToTest = ["memory-rbtree", "memory-btree"]; - provsToTest.push("indexeddb", "indexeddbfakekeys", "indexeddbonclose"); + provsToTest.push( + "indexeddb", + "indexeddbfakekeys", + "indexeddbonclose", + "indexeddbonupgradehandler" + ); it("Number/value/type sorting", () => { const pairsToTest = [ @@ -4043,6 +4048,167 @@ describe("ObjectStoreProvider", function () { ); }); + if (provName === "indexeddbonupgradehandler") { + describe("upgradeCallback", () => { + it("invokes upgradeHandler for success scenario with upgrade steps", (done) => { + const upgradeHandler: UpgradeCallback = (upgradeDetails) => { + try { + assert.equal(upgradeDetails.status, "Success"); + assert.equal(upgradeDetails.oldVersion, 1); + assert.equal(upgradeDetails.newVersion, 2); + assert.ok(upgradeDetails.upgradeSteps.length > 0); + assert.equal( + upgradeDetails.upgradeSteps[1].step, + "DBUpgradeComplete" + ); + done(); + } catch (err) { + done(err); + } + }; + + openProvider( + provName, + { + version: 1, + stores: [ + { + name: "test", + primaryKeyPath: "id", + }, + ], + }, + true + ) + .then((prov) => { + return prov + .put("test", { id: "abc" }) + .then(() => prov.close()) + .catch((e) => prov.close().then(() => Promise.reject(e))); + }) + .then(() => { + return openProvider( + provName, + { + version: 2, + stores: [ + { + name: "test", + primaryKeyPath: "id", + }, + { + name: "test2", + primaryKeyPath: "ttt", + }, + ], + }, + false, + undefined, + undefined, + upgradeHandler + ).then((prov) => { + return prov + .put("test2", { id: "def", ttt: "ghi" }) + .then(() => { + const p1 = prov.get("test", "abc").then((itemVal) => { + const item = itemVal as TestObj; + assert(!!item); + assert.equal(item.id, "abc"); + }); + const p2 = prov.get("test2", "abc").then((item) => { + assert(!item); + }); + return Promise.all([p1, p2]); + }) + .then(() => prov.close()) + .catch((e) => + prov.close().then(() => Promise.reject(e)) + ); + }); + }) + .then( + () => {}, + (err) => done(err) + ); + }); + + it("invokes upgradeHandler for failure scenario during migration", (done) => { + const upgradeHandler: UpgradeCallback = (upgradeDetails) => { + assert.equal(upgradeDetails.status, "Error"); + assert.ok(upgradeDetails.errorMessage); + done(); + }; + + // Save the original function + const originalWrapRequest = + IndexedDbProviderModule.IndexedDbProvider.WrapRequest; + + // Mock the function to simulate a failure + IndexedDbProviderModule.IndexedDbProvider.WrapRequest = + function (): Promise { + console.log("Mocked WrapRequest called"); + return Promise.reject( + new Error("Mocked WrapRequest failure") + ); + }; + + // Open the database with version 1 + openProvider( + "indexeddbonupgradehandler", + { + version: 1, + stores: [ + { + name: "test", + primaryKeyPath: "id", + }, + ], + }, + true, + undefined, + undefined, + upgradeHandler + ) + .then((prov) => prov.close()) + .then(() => { + // Reopen the database with version 2 to trigger the mocked migration failure + return openProvider( + "indexeddbonupgradehandler", + { + version: 2, + stores: [ + { + name: "test", + primaryKeyPath: "id", + indexes: [ + { + name: "ind1", + keyPath: "id", + doNotBackfill: false, + fullText: true, + }, + ], + }, + ], + }, + false, + undefined, + undefined, + upgradeHandler + ); + }) + .catch(() => { + // Expected failure + }) + .finally(() => { + // Restore the original function + IndexedDbProviderModule.IndexedDbProvider.WrapRequest = + originalWrapRequest; + }); + }); + }); + } + // indexed db might backfill anyway behind the scenes if (provName.indexOf("indexeddb") !== 0) { it("Adding an index that does not require backfill", (done) => { @@ -4538,274 +4704,6 @@ describe("ObjectStoreProvider", function () { (err) => done(err) ); }); - - describe("upgradeCallback", () => { - it("invokes upgradeHandler for success scenario with upgrade steps", (done) => { - const upgradeHandler: UpgradeCallback = (upgradeDetails) => { - try { - console.log(JSON.stringify(upgradeDetails)); - assert.equal(upgradeDetails.status, "Success"); - assert.equal(upgradeDetails.oldVersion, 1); - assert.equal(upgradeDetails.newVersion, 2); - assert.ok(upgradeDetails.upgradeSteps.length > 0); - assert.equal( - upgradeDetails.upgradeSteps[0].step, - "DBUpgradeComplete" - ); - done(); - } catch (err) { - done(err); - } - }; - - openProvider( - "indexeddbonupgradehandler", - { - version: 1, - stores: [ - { - name: "test", - primaryKeyPath: "id", - }, - ], - }, - true - ) - .then((prov) => { - return prov - .put("test", { id: "abc" }) - .then(() => prov.close()) - .catch((e) => prov.close().then(() => Promise.reject(e))); - }) - .then(() => { - return openProvider( - "indexeddbonupgradehandler", - { - version: 2, - stores: [ - { - name: "test", - primaryKeyPath: "id", - }, - { - name: "test2", - primaryKeyPath: "ttt", - }, - ], - }, - false, - undefined, - undefined, - upgradeHandler - ).then((prov) => { - return prov - .put("test2", { id: "def", ttt: "ghi" }) - .then(() => { - const p1 = prov.get("test", "abc").then((itemVal) => { - const item = itemVal as TestObj; - assert(!!item); - assert.equal(item.id, "abc"); - }); - const p2 = prov.get("test2", "abc").then((item) => { - assert(!item); - }); - return Promise.all([p1, p2]); - }) - .then(() => prov.close()) - .catch((e) => - prov.close().then(() => Promise.reject(e)) - ); - }); - }) - .then( - () => {}, - (err) => done(err) - ); - - // openProvider( - // "indexeddbonupgradehandler", - // { - // version: 1, - // lastUsableVersion: 1, - // stores: [ - // { - // name: "test", - // primaryKeyPath: "id", - // }, - // ], - // }, - // true - // ) - // .then((prov) => { - // return prov - // .put("test", { id: "abc", content: "ghi" }) - // .then(() => prov.close()) - // .catch((e) => prov.close().then(() => Promise.reject(e))); - // }) - // .then(() => - // openProvider( - // "indexeddbonupgradehandler", - // { - // version: 2, - // lastUsableVersion: 2, - // stores: [ - // { - // name: "test", - // primaryKeyPath: "id", - // }, - // ], - // }, - // true, - // undefined, - // undefined, - // upgradeHandler - // ).then((prov) => prov.close()) - // ); - }); - - // it("invokes upgradeHandler for failure scenario with upgrade steps", (done) => { - // const upgradeHandler: UpgradeCallback = (upgradeDetails) => { - // try { - // assert.equal(upgradeDetails.status, "Error"); - // assert.equal(upgradeDetails.oldVersion, 1); - // assert.equal(upgradeDetails.newVersion, 2); - // assert.ok(upgradeDetails.errorMessage); - // done(); - // } catch (err) { - // done(err); - // } - // }; - - // // Simulate a failure by providing an invalid schema - // openProvider( - // "indexeddbonupgradehandler", - // { - // version: 2, - // stores: [ - // { - // name: "test", - // primaryKeyPath: "nonexistentKeyPath", // Invalid key path - // }, - // ], - // }, - // true, - // undefined, - // undefined, - // upgradeHandler - // ).catch(() => { - // // Expected failure - // }); - // }); - - it("invokes upgradeHandler for failure scenario with upgrade steps", (done) => { - const upgradeHandler: UpgradeCallback = (upgradeDetails) => { - try { - assert.equal(upgradeDetails.status, "Error"); - assert.equal(upgradeDetails.oldVersion, 1); - assert.equal(upgradeDetails.newVersion, 2); - assert.ok(upgradeDetails.errorMessage); - } catch (err) { - done(err); - } - }; - - // Mock the IndexedDbProvider's open method to simulate a failure - const mockOpen = sinon - .stub(IndexedDbProvider.prototype, "open") - .callsFake(async function ( - _dbName: string, - _schema: DbSchema, - _wipeIfExists: boolean, - _verbose: boolean - ) { - // Simulate the IndexedDB open request - const dbOpen = { - onupgradeneeded: null as ((event: any) => void) | null, - onerror: null as ((event: any) => void) | null, - result: null as IDBDatabase | null, - }; - - // Simulate the onupgradeneeded event throwing an error - setTimeout(() => { - if (dbOpen.onupgradeneeded) { - const event = { - oldVersion: 1, - target: dbOpen, - }; - try { - dbOpen.onupgradeneeded(event); - } catch (e) { - if (dbOpen.onerror) { - dbOpen.onerror({ target: { error: e } }); - } - } - } - }, 0); - - return Promise.reject( - new Error("Simulated upgrade failure") - ); - }); - - // Simulate a failure by providing an invalid schema - openProvider( - "indexeddbonupgradehandler", - { - version: 2, - stores: [ - { - name: "test", - primaryKeyPath: "nonexistentKeyPath", // Invalid key path - }, - ], - }, - true, - undefined, - undefined, - upgradeHandler - ) - .catch(() => { - // Expected failure - }) - .finally(() => { - mockOpen.restore(); // Restore the original method - done(); - }); - }); - - // Additional test to ensure upgradeCallback is invoked even when no upgrade steps are required - it("invokes upgradeHandler for no-op upgrade scenario", (done) => { - const upgradeHandler: UpgradeCallback = (upgradeDetails) => { - try { - assert.equal(upgradeDetails.status, "Success"); - assert.equal(upgradeDetails.oldVersion, 1); - assert.equal(upgradeDetails.newVersion, 1); - assert.equal(upgradeDetails.upgradeSteps.length, 0); - done(); - } catch (err) { - done(err); - } - }; - - openProvider( - "indexeddbonupgradehandler", - { - version: 1, - lastUsableVersion: 1, - stores: [ - { - name: "test", - primaryKeyPath: "id", - }, - ], - }, - true, - undefined, - undefined, - upgradeHandler - ).then((prov) => prov.close()); - }); - }); } }); }