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": { diff --git a/src/IndexedDbProvider.ts b/src/IndexedDbProvider.ts index 3e87e3b..04b2a3d 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,23 @@ 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, } @@ -710,7 +701,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( @@ -751,6 +742,9 @@ 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(",") ); @@ -759,10 +753,17 @@ class IndexedDbTransaction implements DbTransaction { lockHelper.transactionFailed( this._transToken, - "IndexedDbTransaction OnError: " + - (this._trans.error ? this._trans.error.message : undefined) + - ", History: " + - history.join(",") + new Error( + "IndexedDbTransaction OnError" + + (this._trans.error?.name !== undefined + ? ", ErrorName: " + this._trans.error.name + : "") + + (this._trans.error?.message !== undefined + ? ", ErrorMessage: " + this._trans.error.message + : "") + + ", History: " + + history.join(",") + ) ); }; @@ -775,6 +776,9 @@ 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(",") ); @@ -783,10 +787,17 @@ class IndexedDbTransaction implements DbTransaction { lockHelper.transactionFailed( this._transToken, - "IndexedDbTransaction Aborted, Error: " + - (this._trans.error ? this._trans.error.message : undefined) + - ", History: " + - history.join(",") + new Error( + "IndexedDbTransaction Aborted" + + (this._trans.error?.name !== undefined + ? ", ErrorName: " + this._trans.error.name + : "") + + (this._trans.error?.message !== undefined + ? ", ErrorMessage: " + this._trans.error.message + : "") + + ", 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..d263350 100644 --- a/src/tests/ObjectStoreProvider.spec.ts +++ b/src/tests/ObjectStoreProvider.spec.ts @@ -16,8 +16,13 @@ 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"; @@ -5430,4 +5435,220 @@ 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("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( + "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; + }); + }); + }); });