From f78d63844a479ba0400aabed0ddb5cf553c550ab Mon Sep 17 00:00:00 2001 From: michaellaw Date: Wed, 1 Apr 2026 11:01:40 -0700 Subject: [PATCH 1/4] initial commit --- src/IndexedDbProvider.ts | 38 ++++----- src/TransactionLockHelper.ts | 6 +- src/tests/ObjectStoreProvider.spec.ts | 108 ++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 26 deletions(-) diff --git a/src/IndexedDbProvider.ts b/src/IndexedDbProvider.ts index 3e87e3b..bacf863 100644 --- a/src/IndexedDbProvider.ts +++ b/src/IndexedDbProvider.ts @@ -159,7 +159,7 @@ export class IndexedDbProvider extends DbProvider { resolve(req.result); }; req.onerror = (ev) => { - reject(ev); + reject((ev.target as IDBRequest)?.error ?? ev); }; }); } @@ -566,32 +566,22 @@ export class IndexedDbProvider extends DbProvider { isCopyRequired: false, upgradeSteps, ...upgradeMetadata, - errorName: err?.target?.error?.name || "Unknown", - errorMessage: err - ? `${err?.message} ${err?.target?.error} ${err?.target?.error?.name}` - : "Unknown error occurred during upgrade", + errorName: err?.name || "Unknown", + errorMessage: err?.message || "Unknown error occurred during upgrade", }); } - if ( - err && - err.type === "error" && - err.target && - err.target.error && - err.target.error.name === "VersionError" - ) { + if (err instanceof DOMException && err.name === "VersionError") { if (!wipeIfExists) { this.logWriter.log( - `Database version too new, Wiping: ${ - err.target.error.message || err.target.error.name - }` + `Database version too new, Wiping: ${err.message || err.name}` ); return this.open(dbName, schema, true, verbose); } } this.logWriter.error( - `Error opening db, message: ${err?.message} ${err?.target?.error} ${err?.target?.error?.name}`, + `Error opening db, message: ${err?.message}, name: ${err?.name}`, { dbName, } @@ -759,10 +749,10 @@ class IndexedDbTransaction implements DbTransaction { lockHelper.transactionFailed( this._transToken, - "IndexedDbTransaction OnError: " + - (this._trans.error ? this._trans.error.message : undefined) + - ", History: " + - history.join(",") + this._trans.error ?? + new Error( + "IndexedDbTransaction OnError, History: " + history.join(",") + ) ); }; @@ -783,10 +773,10 @@ class IndexedDbTransaction implements DbTransaction { lockHelper.transactionFailed( this._transToken, - "IndexedDbTransaction Aborted, Error: " + - (this._trans.error ? this._trans.error.message : undefined) + - ", History: " + - history.join(",") + this._trans.error ?? + new Error( + "IndexedDbTransaction Aborted, History: " + history.join(",") + ) ); }; } diff --git a/src/TransactionLockHelper.ts b/src/TransactionLockHelper.ts index 280825f..ef3d6b4 100644 --- a/src/TransactionLockHelper.ts +++ b/src/TransactionLockHelper.ts @@ -170,7 +170,7 @@ export class TransactionLockHelper { this._cleanTransaction(token); } - transactionFailed(token: TransactionToken, message: string) { + transactionFailed(token: TransactionToken, message: string | Error) { const pendingTransIndex = findIndex( this._pendingTransactions, (trans) => trans.token === token @@ -183,7 +183,9 @@ export class TransactionLockHelper { const toResolve = pendingTrans.completionDefer; this._pendingTransactions.splice(pendingTransIndex, 1); pendingTrans.completionDefer = undefined; - toResolve.reject(new Error(message)); + toResolve.reject( + message instanceof Error ? message : new Error(message) + ); } else { throw new Error( "Failing a transaction that has already been completed. Stores: " + diff --git a/src/tests/ObjectStoreProvider.spec.ts b/src/tests/ObjectStoreProvider.spec.ts index cbafb91..6c461b6 100644 --- a/src/tests/ObjectStoreProvider.spec.ts +++ b/src/tests/ObjectStoreProvider.spec.ts @@ -5430,4 +5430,112 @@ describe("ObjectStoreProvider", function () { } }); }); + + describe("IndexedDbProvider WrapRequest error surfacing", () => { + it("resolves with req.result on success", (done) => { + const mockRequest = {} as IDBRequest; + (mockRequest as any).result = "test-value"; + + IndexedDbProvider.WrapRequest(mockRequest).then( + (result) => { + try { + assert.equal(result, "test-value"); + done(); + } catch (e) { + done(e); + } + }, + () => done(new Error("Expected promise to resolve")) + ); + + mockRequest.onsuccess!({} as any); + }); + + it("rejects with the actual IDBRequest.error DOMException when onerror fires", (done) => { + const mockError = new DOMException( + "Quota exceeded", + "QuotaExceededError" + ); + const mockRequest = {} as IDBRequest; + + IndexedDbProvider.WrapRequest(mockRequest).then( + () => done(new Error("Expected promise to reject")), + (err) => { + try { + assert.strictEqual(err, mockError); + assert.equal(err.name, "QuotaExceededError"); + done(); + } catch (e) { + done(e); + } + } + ); + + mockRequest.onerror!({ target: { error: mockError } } as any); + }); + + it("falls back to the raw event when IDBRequest.error is null", (done) => { + const mockRequest = {} as IDBRequest; + const mockEvent = { target: { error: null } } as any; + + IndexedDbProvider.WrapRequest(mockRequest).then( + () => done(new Error("Expected promise to reject")), + (err) => { + try { + assert.strictEqual(err, mockEvent); + done(); + } catch (e) { + done(e); + } + } + ); + + mockRequest.onerror!(mockEvent); + }); + }); + + describe("IndexedDbProvider put DOMException propagation", () => { + it("put rejects with the DOMException surfaced from WrapRequest", (done) => { + const quotaError = new DOMException( + "Quota exceeded", + "QuotaExceededError" + ); + const originalWrapRequest = + IndexedDbProviderModule.IndexedDbProvider.WrapRequest; + + openProvider( + "indexeddb", + { version: 1, stores: [{ name: "test", primaryKeyPath: "id" }] }, + true + ) + .then((prov) => { + // Mock only after a successful open so the open itself is unaffected + IndexedDbProviderModule.IndexedDbProvider.WrapRequest = + function (): Promise { + return Promise.reject(quotaError); + }; + return prov.put("test", { id: "abc", val: "hello" }).then( + () => { + prov.close(); + done(new Error("Expected put to reject")); + }, + (err) => { + prov.close(); + try { + assert.strictEqual(err, quotaError); + assert.equal(err.name, "QuotaExceededError"); + done(); + } catch (e) { + done(e); + } + } + ); + }) + .catch((err) => done(err)) + .finally(() => { + IndexedDbProviderModule.IndexedDbProvider.WrapRequest = + originalWrapRequest; + }); + }); + }); }); From be9d5aa59d72e21f9ec6dc0f90dca1e4bba30f5f Mon Sep 17 00:00:00 2001 From: michaellaw Date: Fri, 3 Apr 2026 11:17:57 -0700 Subject: [PATCH 2/4] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1da7e26..77a50c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/objectstoreprovider", - "version": "0.9.0", + "version": "0.9.1", "description": "A cross-browser object store library", "author": "DataStack Team eleretzk@microsoft.com", "scripts": { From 1691e00b154f63c909bd5fe14faa3776f95cf857 Mon Sep 17 00:00:00 2001 From: michaellaw Date: Tue, 7 Apr 2026 12:56:46 -0700 Subject: [PATCH 3/4] log error.name --- src/IndexedDbProvider.ts | 4 +- src/tests/ObjectStoreProvider.spec.ts | 112 +++++++++++++++++++++++++- 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/src/IndexedDbProvider.ts b/src/IndexedDbProvider.ts index bacf863..d159777 100644 --- a/src/IndexedDbProvider.ts +++ b/src/IndexedDbProvider.ts @@ -700,7 +700,7 @@ export class IndexedDbProvider extends DbProvider { } // DbTransaction implementation for the IndexedDB DbProvider. -class IndexedDbTransaction implements DbTransaction { +export class IndexedDbTransaction implements DbTransaction { private _stores: IDBObjectStore[]; constructor( @@ -741,6 +741,7 @@ class IndexedDbTransaction implements DbTransaction { this.logWriter.warn( "IndexedDbTransaction Errored after Resolution, Swallowing. Error: " + (this._trans.error ? this._trans.error.message : undefined) + + (this._trans.error?.name !== undefined ? ", ErrorName: " + this._trans.error.name : "") + ", History: " + history.join(",") ); @@ -765,6 +766,7 @@ class IndexedDbTransaction implements DbTransaction { this.logWriter.warn( "IndexedDbTransaction Aborted after Resolution, Swallowing. Error: " + (this._trans.error ? this._trans.error.message : undefined) + + (this._trans.error?.name !== undefined ? ", ErrorName: " + this._trans.error.name : "") + ", History: " + history.join(",") ); diff --git a/src/tests/ObjectStoreProvider.spec.ts b/src/tests/ObjectStoreProvider.spec.ts index 6c461b6..bc76986 100644 --- a/src/tests/ObjectStoreProvider.spec.ts +++ b/src/tests/ObjectStoreProvider.spec.ts @@ -16,8 +16,10 @@ import { } from "../ObjectStoreProvider"; import { InMemoryProvider } from "../InMemoryProvider"; -import { IndexedDbProvider } from "../IndexedDbProvider"; +import { IndexedDbProvider, IndexedDbTransaction } from "../IndexedDbProvider"; import * as IndexedDbProviderModule from "../IndexedDbProvider"; +import { TransactionToken, TransactionLockHelper } from "../TransactionLockHelper"; +import { LogWriter } from "../LogWriter"; import { serializeValueToOrderableString } from "../ObjectStoreProviderUtils"; @@ -5494,6 +5496,114 @@ describe("ObjectStoreProvider", function () { }); }); + describe("IndexedDbTransaction after-Resolution warn logging", () => { + function makeTransactionFixture() { + const warnMessages: string[] = []; + const captureLogger = { + log: () => {}, + error: () => {}, + warn: (msg: string) => warnMessages.push(msg), + }; + const logWriter = new LogWriter(captureLogger); + + const mockTrans = { + objectStore: () => ({} as IDBObjectStore), + oncomplete: null as ((ev: Event) => any) | null, + onerror: null as ((ev: Event) => any) | null, + onabort: null as ((ev: Event) => any) | null, + error: null as DOMException | null, + } as unknown as IDBTransaction; + + const mockToken: TransactionToken = { + completionPromise: Promise.resolve(), + storeNames: [], + exclusive: false, + }; + + const mockLockHelper = { + transactionComplete: () => {}, + transactionFailed: () => {}, + } as unknown as TransactionLockHelper; + + new IndexedDbTransaction( + mockTrans, + mockLockHelper, + mockToken, + { version: 1, stores: [] }, + false, + logWriter + ); + + return { mockTrans: mockTrans as any, warnMessages }; + } + + it("logs ErrorName and message on onerror after oncomplete", () => { + const { mockTrans, warnMessages } = makeTransactionFixture(); + const domError = new DOMException("Disk full", "QuotaExceededError"); + + mockTrans.oncomplete(); + mockTrans.error = domError; + mockTrans.onerror(); + + assert.equal(warnMessages.length, 1); + assert.include( + warnMessages[0], + "IndexedDbTransaction Errored after Resolution, Swallowing" + ); + assert.include(warnMessages[0], "Error: Disk full"); + assert.include(warnMessages[0], "ErrorName: QuotaExceededError"); + }); + + it("logs ErrorName and message on onabort after oncomplete", () => { + const { mockTrans, warnMessages } = makeTransactionFixture(); + const domError = new DOMException("Disk full", "QuotaExceededError"); + + mockTrans.oncomplete(); + mockTrans.error = domError; + mockTrans.onabort(); + + assert.equal(warnMessages.length, 1); + assert.include( + warnMessages[0], + "IndexedDbTransaction Aborted after Resolution, Swallowing" + ); + assert.include(warnMessages[0], "Error: Disk full"); + assert.include(warnMessages[0], "ErrorName: QuotaExceededError"); + }); + + it("omits ErrorName when trans.error is null on onerror after oncomplete", () => { + const { mockTrans, warnMessages } = makeTransactionFixture(); + + mockTrans.oncomplete(); + // error stays null + mockTrans.onerror(); + + assert.equal(warnMessages.length, 1); + assert.include( + warnMessages[0], + "IndexedDbTransaction Errored after Resolution, Swallowing" + ); + assert.include(warnMessages[0], "Error: undefined"); + assert.notInclude(warnMessages[0], "ErrorName:"); + }); + + it("omits ErrorName when trans.error is null on onabort after oncomplete", () => { + const { mockTrans, warnMessages } = makeTransactionFixture(); + + mockTrans.oncomplete(); + // error stays null + mockTrans.onabort(); + + assert.equal(warnMessages.length, 1); + assert.include( + warnMessages[0], + "IndexedDbTransaction Aborted after Resolution, Swallowing" + ); + assert.include(warnMessages[0], "Error: undefined"); + assert.notInclude(warnMessages[0], "ErrorName:"); + }); + }); + describe("IndexedDbProvider put DOMException propagation", () => { it("put rejects with the DOMException surfaced from WrapRequest", (done) => { const quotaError = new DOMException( From 10b464893086861942d6a9f05e96244afebda925 Mon Sep 17 00:00:00 2001 From: michaellaw Date: Tue, 7 Apr 2026 13:03:03 -0700 Subject: [PATCH 4/4] prettier formatting --- src/IndexedDbProvider.ts | 11 ++++++++--- src/tests/ObjectStoreProvider.spec.ts | 5 ++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/IndexedDbProvider.ts b/src/IndexedDbProvider.ts index d159777..fe67a4f 100644 --- a/src/IndexedDbProvider.ts +++ b/src/IndexedDbProvider.ts @@ -567,7 +567,8 @@ export class IndexedDbProvider extends DbProvider { upgradeSteps, ...upgradeMetadata, errorName: err?.name || "Unknown", - errorMessage: err?.message || "Unknown error occurred during upgrade", + errorMessage: + err?.message || "Unknown error occurred during upgrade", }); } @@ -741,7 +742,9 @@ export class IndexedDbTransaction implements DbTransaction { this.logWriter.warn( "IndexedDbTransaction Errored after Resolution, Swallowing. Error: " + (this._trans.error ? this._trans.error.message : undefined) + - (this._trans.error?.name !== undefined ? ", ErrorName: " + this._trans.error.name : "") + + (this._trans.error?.name !== undefined + ? ", ErrorName: " + this._trans.error.name + : "") + ", History: " + history.join(",") ); @@ -766,7 +769,9 @@ export class IndexedDbTransaction implements DbTransaction { this.logWriter.warn( "IndexedDbTransaction Aborted after Resolution, Swallowing. Error: " + (this._trans.error ? this._trans.error.message : undefined) + - (this._trans.error?.name !== undefined ? ", ErrorName: " + this._trans.error.name : "") + + (this._trans.error?.name !== undefined + ? ", ErrorName: " + this._trans.error.name + : "") + ", History: " + history.join(",") ); diff --git a/src/tests/ObjectStoreProvider.spec.ts b/src/tests/ObjectStoreProvider.spec.ts index bc76986..d263350 100644 --- a/src/tests/ObjectStoreProvider.spec.ts +++ b/src/tests/ObjectStoreProvider.spec.ts @@ -18,7 +18,10 @@ import { import { InMemoryProvider } from "../InMemoryProvider"; import { IndexedDbProvider, IndexedDbTransaction } from "../IndexedDbProvider"; import * as IndexedDbProviderModule from "../IndexedDbProvider"; -import { TransactionToken, TransactionLockHelper } from "../TransactionLockHelper"; +import { + TransactionToken, + TransactionLockHelper, +} from "../TransactionLockHelper"; import { LogWriter } from "../LogWriter"; import { serializeValueToOrderableString } from "../ObjectStoreProviderUtils";