Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
47 changes: 22 additions & 25 deletions src/IndexedDbProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export class IndexedDbProvider extends DbProvider {
resolve(req.result);
};
req.onerror = (ev) => {
reject(ev);
reject((ev.target as IDBRequest)?.error ?? ev);
};
});
}
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(",")
);
Expand All @@ -759,10 +753,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(",")
)
);
};

Expand All @@ -775,6 +769,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(",")
);
Expand All @@ -783,10 +780,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(",")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the this._trans.error.name here?

)
);
};
}
Expand Down
6 changes: 4 additions & 2 deletions src/TransactionLockHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: " +
Expand Down
223 changes: 222 additions & 1 deletion src/tests/ObjectStoreProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -5430,4 +5435,220 @@ describe("ObjectStoreProvider", function () {
}
});
});

describe("IndexedDbProvider WrapRequest error surfacing", () => {
it("resolves with req.result on success", (done) => {
const mockRequest = {} as IDBRequest<string>;
(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<any>;

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<any>;
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<any> {
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;
});
});
});
});
Loading