summaryrefslogtreecommitdiff
path: root/packages/idb-bridge
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2021-02-19 21:27:49 +0100
committerFlorian Dold <florian@dold.me>2021-02-19 21:27:49 +0100
commite6946694f2e7ae6ff25f490fa76f3da583c44c74 (patch)
tree892369b87d1f667271e588eef44b0dff1d695774 /packages/idb-bridge
parentc800e80138358430b924937b2f4fed69376181ce (diff)
downloadwallet-core-e6946694f2e7ae6ff25f490fa76f3da583c44c74.tar.gz
wallet-core-e6946694f2e7ae6ff25f490fa76f3da583c44c74.tar.bz2
wallet-core-e6946694f2e7ae6ff25f490fa76f3da583c44c74.zip
idb: more tests, fix DB deletion, exception ordering and transaction active checks
Diffstat (limited to 'packages/idb-bridge')
-rw-r--r--packages/idb-bridge/src/MemoryBackend.ts122
-rw-r--r--packages/idb-bridge/src/backend-interface.ts8
-rw-r--r--packages/idb-bridge/src/bridge-idb.ts118
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbcursor-advance-index.test.ts255
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-add-put-exception-order.test.ts104
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts58
6 files changed, 547 insertions, 118 deletions
diff --git a/packages/idb-bridge/src/MemoryBackend.ts b/packages/idb-bridge/src/MemoryBackend.ts
index 0051005ed..53355bf77 100644
--- a/packages/idb-bridge/src/MemoryBackend.ts
+++ b/packages/idb-bridge/src/MemoryBackend.ts
@@ -132,11 +132,6 @@ interface Connection {
modifiedSchema: Schema;
/**
- * Has the underlying database been deleted?
- */
- deleted: boolean;
-
- /**
* Map from the effective name of an object store during
* the transaction to the real name.
*/
@@ -412,13 +407,9 @@ export class MemoryBackend implements Backend {
return dbList;
}
- async deleteDatabase(tx: DatabaseTransaction, name: string): Promise<void> {
+ async deleteDatabase(name: string): Promise<void> {
if (this.enableTracing) {
- console.log("TRACING: deleteDatabase");
- }
- const myConn = this.connectionsByTransaction[tx.transactionCookie];
- if (!myConn) {
- throw Error("no connection associated with transaction");
+ console.log(`TRACING: deleteDatabase(${name})`);
}
const myDb = this.databases[name];
if (!myDb) {
@@ -427,13 +418,13 @@ export class MemoryBackend implements Backend {
if (myDb.committedSchema.databaseName !== name) {
throw Error("name does not match");
}
- if (myDb.txLevel < TransactionLevel.VersionChange) {
- throw new InvalidStateError();
+
+ while (myDb.txLevel !== TransactionLevel.None) {
+ await this.transactionDoneCond.wait();
}
- // if (myDb.connectionCookie !== tx.transactionCookie) {
- // throw new InvalidAccessError();
- // }
+
myDb.deleted = true;
+ delete this.databases[name];
}
async connectDatabase(name: string): Promise<DatabaseConnection> {
@@ -469,7 +460,6 @@ export class MemoryBackend implements Backend {
const myConn: Connection = {
dbName: name,
- deleted: false,
objectStoreMap: this.makeObjectStoreMap(database),
modifiedSchema: structuredClone(database.committedSchema),
};
@@ -560,28 +550,38 @@ export class MemoryBackend implements Backend {
if (!myConn) {
throw Error("connection not found - already closed?");
}
- if (!myConn.deleted) {
- const myDb = this.databases[myConn.dbName];
- // if (myDb.connectionCookies.includes(conn.connectionCookie)) {
- // throw Error("invalid state");
- // }
- // FIXME: what if we're still in a transaction?
- myDb.connectionCookies = myDb.connectionCookies.filter(
- (x) => x != conn.connectionCookie,
- );
- }
+ const myDb = this.databases[myConn.dbName];
+ // FIXME: what if we're still in a transaction?
+ myDb.connectionCookies = myDb.connectionCookies.filter(
+ (x) => x != conn.connectionCookie,
+ );
delete this.connections[conn.connectionCookie];
this.disconnectCond.trigger();
}
+ private requireConnection(dbConn: DatabaseConnection): Connection {
+ const myConn = this.connections[dbConn.connectionCookie];
+ if (!myConn) {
+ throw Error(`unknown connection (${dbConn.connectionCookie})`);
+ }
+ return myConn;
+ }
+
+ private requireConnectionFromTransaction(
+ btx: DatabaseTransaction,
+ ): Connection {
+ const myConn = this.connectionsByTransaction[btx.transactionCookie];
+ if (!myConn) {
+ throw Error(`unknown transaction (${btx.transactionCookie})`);
+ }
+ return myConn;
+ }
+
getSchema(dbConn: DatabaseConnection): Schema {
if (this.enableTracing) {
console.log(`TRACING: getSchema`);
}
- const myConn = this.connections[dbConn.connectionCookie];
- if (!myConn) {
- throw Error("unknown connection");
- }
+ const myConn = this.requireConnection(dbConn);
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
@@ -590,10 +590,7 @@ export class MemoryBackend implements Backend {
}
getCurrentTransactionSchema(btx: DatabaseTransaction): Schema {
- const myConn = this.connectionsByTransaction[btx.transactionCookie];
- if (!myConn) {
- throw Error("unknown connection");
- }
+ const myConn = this.requireConnectionFromTransaction(btx);
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
@@ -602,10 +599,7 @@ export class MemoryBackend implements Backend {
}
getInitialTransactionSchema(btx: DatabaseTransaction): Schema {
- const myConn = this.connectionsByTransaction[btx.transactionCookie];
- if (!myConn) {
- throw Error("unknown connection");
- }
+ const myConn = this.requireConnectionFromTransaction(btx);
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
@@ -622,10 +616,7 @@ export class MemoryBackend implements Backend {
if (this.enableTracing) {
console.log(`TRACING: renameIndex(?, ${oldName}, ${newName})`);
}
- const myConn = this.connectionsByTransaction[btx.transactionCookie];
- if (!myConn) {
- throw Error("unknown connection");
- }
+ const myConn = this.requireConnectionFromTransaction(btx);
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
@@ -664,10 +655,7 @@ export class MemoryBackend implements Backend {
if (this.enableTracing) {
console.log(`TRACING: deleteIndex(${indexName})`);
}
- const myConn = this.connectionsByTransaction[btx.transactionCookie];
- if (!myConn) {
- throw Error("unknown connection");
- }
+ const myConn = this.requireConnectionFromTransaction(btx);
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
@@ -698,10 +686,7 @@ export class MemoryBackend implements Backend {
`TRACING: deleteObjectStore(${name}) in ${btx.transactionCookie}`,
);
}
- const myConn = this.connectionsByTransaction[btx.transactionCookie];
- if (!myConn) {
- throw Error("unknown connection");
- }
+ const myConn = this.requireConnectionFromTransaction(btx);
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
@@ -740,10 +725,7 @@ export class MemoryBackend implements Backend {
console.log(`TRACING: renameObjectStore(?, ${oldName}, ${newName})`);
}
- const myConn = this.connectionsByTransaction[btx.transactionCookie];
- if (!myConn) {
- throw Error("unknown connection");
- }
+ const myConn = this.requireConnectionFromTransaction(btx);
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
@@ -783,10 +765,7 @@ export class MemoryBackend implements Backend {
`TRACING: createObjectStore(${btx.transactionCookie}, ${name})`,
);
}
- const myConn = this.connectionsByTransaction[btx.transactionCookie];
- if (!myConn) {
- throw Error("unknown connection");
- }
+ const myConn = this.requireConnectionFromTransaction(btx);
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
@@ -828,10 +807,7 @@ export class MemoryBackend implements Backend {
if (this.enableTracing) {
console.log(`TRACING: createIndex(${indexName})`);
}
- const myConn = this.connectionsByTransaction[btx.transactionCookie];
- if (!myConn) {
- throw Error("unknown connection");
- }
+ const myConn = this.requireConnectionFromTransaction(btx);
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
@@ -892,10 +868,7 @@ export class MemoryBackend implements Backend {
if (this.enableTracing) {
console.log(`TRACING: deleteRecord from store ${objectStoreName}`);
}
- const myConn = this.connectionsByTransaction[btx.transactionCookie];
- if (!myConn) {
- throw Error("unknown connection");
- }
+ const myConn = this.requireConnectionFromTransaction(btx);
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
@@ -1057,10 +1030,7 @@ export class MemoryBackend implements Backend {
console.log(`TRACING: getRecords`);
console.log("query", req);
}
- const myConn = this.connectionsByTransaction[btx.transactionCookie];
- if (!myConn) {
- throw Error("unknown connection");
- }
+ const myConn = this.requireConnectionFromTransaction(btx);
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
@@ -1388,10 +1358,7 @@ export class MemoryBackend implements Backend {
if (this.enableTracing) {
console.log(`TRACING: storeRecord`);
}
- const myConn = this.connectionsByTransaction[btx.transactionCookie];
- if (!myConn) {
- throw Error("unknown connection");
- }
+ const myConn = this.requireConnectionFromTransaction(btx);
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
@@ -1626,10 +1593,7 @@ export class MemoryBackend implements Backend {
if (this.enableTracing) {
console.log(`TRACING: commit`);
}
- const myConn = this.connectionsByTransaction[btx.transactionCookie];
- if (!myConn) {
- throw Error("unknown connection");
- }
+ const myConn = this.requireConnectionFromTransaction(btx);
const db = this.databases[myConn.dbName];
if (!db) {
throw Error("db not found");
diff --git a/packages/idb-bridge/src/backend-interface.ts b/packages/idb-bridge/src/backend-interface.ts
index 7b74c35e6..164996e77 100644
--- a/packages/idb-bridge/src/backend-interface.ts
+++ b/packages/idb-bridge/src/backend-interface.ts
@@ -21,7 +21,6 @@ import {
IDBValidKey,
} from "./idbtypes";
-
/** @public */
export interface ObjectStoreProperties {
keyPath: string[] | null;
@@ -151,12 +150,7 @@ export interface Backend {
newVersion: number,
): Promise<DatabaseTransaction>;
- /**
- * Even though the standard interface for indexedDB doesn't require
- * the client to run deleteDatabase in a version transaction, there is
- * implicitly one running.
- */
- deleteDatabase(btx: DatabaseTransaction, name: string): Promise<void>;
+ deleteDatabase(name: string): Promise<void>;
close(db: DatabaseConnection): Promise<void>;
diff --git a/packages/idb-bridge/src/bridge-idb.ts b/packages/idb-bridge/src/bridge-idb.ts
index 6ca6633a9..643a98dea 100644
--- a/packages/idb-bridge/src/bridge-idb.ts
+++ b/packages/idb-bridge/src/bridge-idb.ts
@@ -195,7 +195,10 @@ export class BridgeIDBCursor implements IDBCursor {
/**
* https://w3c.github.io/IndexedDB/#iterate-a-cursor
*/
- async _iterate(key?: IDBValidKey, primaryKey?: IDBValidKey): Promise<any> {
+ async _iterate(
+ key?: IDBValidKey,
+ primaryKey?: IDBValidKey,
+ ): Promise<BridgeIDBCursor | null> {
BridgeIDBFactory.enableTracing &&
console.log(
`iterating cursor os=${this._objectStoreName},idx=${this._indexName}`,
@@ -312,6 +315,10 @@ export class BridgeIDBCursor implements IDBCursor {
* http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBCursor-advance-void-unsigned-long-count
*/
public advance(count: number) {
+ if (typeof count !== "number" || count <= 0) {
+ throw TypeError("count must be positive number");
+ }
+
const transaction = this._effectiveObjectStore._transaction;
if (!transaction._active) {
@@ -337,9 +344,11 @@ export class BridgeIDBCursor implements IDBCursor {
}
const operation = async () => {
+ let res: IDBCursor | null = null;
for (let i = 0; i < count; i++) {
- await this._iterate();
+ res = await this._iterate();
}
+ return res;
};
transaction._execRequestAsync({
@@ -527,6 +536,11 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase {
_schema: Schema;
+ /**
+ * Name that can be set to identify the object store in logs.
+ */
+ _debugName: string | undefined = undefined;
+
get name(): string {
return this._schema.databaseName;
}
@@ -686,12 +700,23 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase {
openRequest,
);
this._transactions.push(tx);
- queueTask(() => tx._start());
+
+ queueTask(() => {
+ console.log("TRACE: calling auto-commit", this._getReadableName());
+ tx._start();
+ });
+ if (BridgeIDBFactory.enableTracing) {
+ console.log("TRACE: queued task to auto-commit", this._getReadableName());
+ }
// "When a transaction is created its active flag is initially set."
tx._active = true;
return tx;
}
+ _getReadableName(): string {
+ return `${this.name}(${this._debugName ?? "??"})`;
+ }
+
public transaction(
storeNames: string | string[],
mode?: IDBTransactionMode,
@@ -745,15 +770,7 @@ export class BridgeIDBFactory {
const oldVersion = dbInfo.version;
try {
- const dbconn = await this.backend.connectDatabase(name);
- const backendTransaction = await this.backend.enterVersionChange(
- dbconn,
- 0,
- );
- await this.backend.deleteDatabase(backendTransaction, name);
- await this.backend.commit(backendTransaction);
- await this.backend.close(dbconn);
-
+ await this.backend.deleteDatabase(name);
request.result = undefined;
request.readyState = "done";
@@ -797,15 +814,11 @@ export class BridgeIDBFactory {
let dbconn: DatabaseConnection;
try {
if (BridgeIDBFactory.enableTracing) {
- console.log(
- "TRACE: connecting to database",
- );
+ console.log("TRACE: connecting to database");
}
dbconn = await this.backend.connectDatabase(name);
if (BridgeIDBFactory.enableTracing) {
- console.log(
- "TRACE: connected!",
- );
+ console.log("TRACE: connected!");
}
} catch (err) {
if (BridgeIDBFactory.enableTracing) {
@@ -1385,6 +1398,11 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
_transaction: BridgeIDBTransaction;
+ /**
+ * Name that can be set to identify the object store in logs.
+ */
+ _debugName: string | undefined = undefined;
+
get transaction(): IDBTransaction {
return this._transaction;
}
@@ -1490,8 +1508,15 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
public _store(value: any, key: IDBValidKey | undefined, overwrite: boolean) {
if (BridgeIDBFactory.enableTracing) {
- console.log(`TRACE: IDBObjectStore._store`);
+ console.log(
+ `TRACE: IDBObjectStore._store, db=${this._transaction._db._getReadableName()}`,
+ );
}
+
+ if (!this._transaction._active) {
+ throw new TransactionInactiveError();
+ }
+
if (this._transaction.mode === "readonly") {
throw new ReadOnlyError();
}
@@ -1989,6 +2014,11 @@ export class BridgeIDBTransaction
_objectStoresCache: Map<string, BridgeIDBObjectStore> = new Map();
/**
+ * Name that can be set to identify the transaction in logs.
+ */
+ _debugName: string | undefined = undefined;
+
+ /**
* https://www.w3.org/TR/IndexedDB-2/#transaction-lifetime-concept
*
* When a transaction is committed or aborted, it is said to be finished.
@@ -2074,7 +2104,12 @@ export class BridgeIDBTransaction
console.log("TRACE: aborting transaction");
}
+ if (this._aborted) {
+ return;
+ }
+
this._aborted = true;
+ this._active = false;
if (errName !== null) {
const e = new Error();
@@ -2116,6 +2151,7 @@ export class BridgeIDBTransaction
this._db._schema = this._backend.getInitialTransactionSchema(maybeBtx);
// Only roll back if we actually executed the scheduled operations.
await this._backend.rollback(maybeBtx);
+ this._backendTransaction = undefined;
} else {
this._db._schema = this._backend.getSchema(this._db._backendConnection);
}
@@ -2208,17 +2244,11 @@ export class BridgeIDBTransaction
`TRACE: IDBTransaction._start, ${this._requests.length} queued`,
);
}
- this._started = true;
- if (!this._backendTransaction) {
- this._backendTransaction = await this._backend.beginTransaction(
- this._db._backendConnection,
- Array.from(this._scope),
- this.mode,
- );
- }
+ this._started = true;
- // Remove from request queue - cursor ones will be added back if necessary by cursor.continue and such
+ // Remove from request queue - cursor ones will be added back if necessary
+ // by cursor.continue and such
let operation;
let request;
while (this._requests.length > 0) {
@@ -2233,9 +2263,25 @@ export class BridgeIDBTransaction
}
if (request && operation) {
+ if (!this._backendTransaction && !this._aborted) {
+ if (BridgeIDBFactory.enableTracing) {
+ console.log("beginning backend transaction to process operation");
+ }
+ this._backendTransaction = await this._backend.beginTransaction(
+ this._db._backendConnection,
+ Array.from(this._scope),
+ this.mode,
+ );
+ if (BridgeIDBFactory.enableTracing) {
+ console.log(
+ `started backend transaction (${this._backendTransaction.transactionCookie})`,
+ );
+ }
+ }
+
if (!request._source) {
- // Special requests like indexes that just need to run some code, with error handling already built into
- // operation
+ // Special requests like indexes that just need to run some code,
+ // with error handling already built into operation
await operation();
} else {
let event;
@@ -2311,10 +2357,18 @@ export class BridgeIDBTransaction
if (!this._finished && !this._committed) {
if (BridgeIDBFactory.enableTracing) {
- console.log("finishing transaction");
+ console.log(
+ `setting transaction to inactive, db=${this._db._getReadableName()}`,
+ );
}
- await this._backend.commit(this._backendTransaction);
+ this._active = false;
+
+ // We only have a backend transaction if any requests were placed
+ // against the transactions.
+ if (this._backendTransaction) {
+ await this._backend.commit(this._backendTransaction);
+ }
this._committed = true;
if (!this._error) {
if (BridgeIDBFactory.enableTracing) {
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-advance-index.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-advance-index.test.ts
index a7be31f28..2d449a9ab 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-advance-index.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-advance-index.test.ts
@@ -1,5 +1,7 @@
import test from "ava";
import { BridgeIDBCursor } from "..";
+import { BridgeIDBRequest } from "../bridge-idb";
+import { InvalidStateError } from "../util/errors";
import { createdb } from "./wptsupport";
test("WPT test idbcursor_advance_index.htm", async (t) => {
@@ -34,6 +36,7 @@ test("WPT test idbcursor_advance_index.htm", async (t) => {
cursor_rq.onsuccess = function (e: any) {
var cursor = e.target.result;
t.log(cursor);
+ t.true(e.target instanceof BridgeIDBRequest);
t.true(cursor instanceof BridgeIDBCursor);
switch (count) {
@@ -51,7 +54,259 @@ test("WPT test idbcursor_advance_index.htm", async (t) => {
t.fail("unexpected count");
break;
}
+ };
+ };
+ });
+});
+
+// IDBCursor.advance() - attempt to pass a count parameter that is not a number
+test("WPT test idbcursor_advance_index2.htm", async (t) => {
+ await new Promise<void>((resolve, reject) => {
+ var db: any;
+
+ const records = [
+ { pKey: "primaryKey_0", iKey: "indexKey_0" },
+ { pKey: "primaryKey_1", iKey: "indexKey_1" },
+ ];
+
+ var open_rq = createdb(t);
+ open_rq.onupgradeneeded = function (e: any) {
+ db = e.target.result;
+ var objStore = db.createObjectStore("test", { keyPath: "pKey" });
+
+ objStore.createIndex("index", "iKey");
+
+ for (var i = 0; i < records.length; i++) objStore.add(records[i]);
+ };
+
+ open_rq.onsuccess = function (e) {
+ var cursor_rq = db
+ .transaction("test")
+ .objectStore("test")
+ .index("index")
+ .openCursor();
+
+ cursor_rq.onsuccess = function (e: any) {
+ var cursor = e.target.result;
+
+ t.true(cursor != null, "cursor exist");
+ t.throws(
+ () => {
+ // Original test uses "document".
+ cursor.advance({ foo: 42 });
+ },
+ { instanceOf: TypeError },
+ );
+ resolve();
+ };
+ };
+ });
+});
+
+// IDBCursor.advance() - index - attempt to advance backwards
+test("WPT test idbcursor_advance_index3.htm", async (t) => {
+ await new Promise<void>((resolve, reject) => {
+ var db: any;
+
+ const records = [
+ { pKey: "primaryKey_0", iKey: "indexKey_0" },
+ { pKey: "primaryKey_1", iKey: "indexKey_1" },
+ ];
+
+ var open_rq = createdb(t);
+ open_rq.onupgradeneeded = function (e: any) {
+ db = e.target.result;
+ var objStore = db.createObjectStore("test", { keyPath: "pKey" });
+
+ objStore.createIndex("index", "iKey");
+
+ for (var i = 0; i < records.length; i++) objStore.add(records[i]);
+ };
+
+ open_rq.onsuccess = function (e) {
+ var cursor_rq = db
+ .transaction("test")
+ .objectStore("test")
+ .index("index")
+ .openCursor();
+
+ cursor_rq.onsuccess = function (e: any) {
+ var cursor = e.target.result;
+
+ t.true(cursor != null, "cursor exist");
+ t.throws(
+ () => {
+ cursor.advance(-1);
+ },
+ { instanceOf: TypeError },
+ );
+ resolve();
+ };
+ };
+ });
+});
+
+// IDBCursor.advance() - index - iterate to the next record
+test("WPT test idbcursor_advance_index5.htm", async (t) => {
+ await new Promise<void>((resolve, reject) => {
+ var db: any;
+ let count = 0;
+ const records = [
+ { pKey: "primaryKey_0", iKey: "indexKey_0" },
+ { pKey: "primaryKey_1", iKey: "indexKey_1" },
+ { pKey: "primaryKey_1-2", iKey: "indexKey_1" },
+ ],
+ expected = [
+ { pKey: "primaryKey_0", iKey: "indexKey_0" },
+ { pKey: "primaryKey_1-2", iKey: "indexKey_1" },
+ ];
+
+ var open_rq = createdb(t);
+ open_rq.onupgradeneeded = function (e: any) {
+ db = e.target.result;
+ var objStore = db.createObjectStore("test", { keyPath: "pKey" });
+
+ objStore.createIndex("index", "iKey");
+
+ for (var i = 0; i < records.length; i++) objStore.add(records[i]);
+ };
+
+ open_rq.onsuccess = function (e: any) {
+ var cursor_rq = db
+ .transaction("test")
+ .objectStore("test")
+ .index("index")
+ .openCursor();
+
+ cursor_rq.onsuccess = function (e: any) {
+ var cursor = e.target.result;
+ if (!cursor) {
+ t.deepEqual(count, expected.length, "cursor run count");
+ resolve();
+ }
+
+ var record = cursor.value;
+ t.deepEqual(record.pKey, expected[count].pKey, "primary key");
+ t.deepEqual(record.iKey, expected[count].iKey, "index key");
+
+ cursor.advance(2);
+ count++;
+ };
+ };
+ });
+});
+
+// IDBCursor.advance() - index - throw TransactionInactiveError
+test("WPT test idbcursor_advance_index7.htm", async (t) => {
+ await new Promise<void>((resolve, reject) => {
+ var db: any;
+ const records = [
+ { pKey: "primaryKey_0", iKey: "indexKey_0" },
+ { pKey: "primaryKey_1", iKey: "indexKey_1" },
+ ];
+
+ var open_rq = createdb(t);
+ open_rq.onupgradeneeded = function (event: any) {
+ db = event.target.result;
+ var objStore = db.createObjectStore("store", { keyPath: "pKey" });
+ objStore.createIndex("index", "iKey");
+ for (var i = 0; i < records.length; i++) {
+ objStore.add(records[i]);
+ }
+ var rq = objStore.index("index").openCursor();
+ rq.onsuccess = function (event: any) {
+ var cursor = event.target.result;
+ t.true(cursor instanceof BridgeIDBCursor);
+
+ event.target.transaction.abort();
+ t.throws(
+ () => {
+ cursor.advance(1);
+ },
+ { name: "TransactionInactiveError" },
+ "Calling advance() should throws an exception TransactionInactiveError when the transaction is not active.",
+ );
+ resolve();
+ };
+ };
+ });
+});
+
+// IDBCursor.advance() - index - throw InvalidStateError
+test("WPT test idbcursor_advance_index8.htm", async (t) => {
+ await new Promise<void>((resolve, reject) => {
+ var db: any;
+ const records = [
+ { pKey: "primaryKey_0", iKey: "indexKey_0" },
+ { pKey: "primaryKey_1", iKey: "indexKey_1" },
+ ];
+
+ var open_rq = createdb(t);
+ open_rq.onupgradeneeded = function (event: any) {
+ db = event.target.result;
+ var objStore = db.createObjectStore("store", { keyPath: "pKey" });
+ objStore.createIndex("index", "iKey");
+ for (var i = 0; i < records.length; i++) {
+ objStore.add(records[i]);
}
+ var rq = objStore.index("index").openCursor();
+ let called = false;
+ rq.onsuccess = function (event: any) {
+ if (called) {
+ return;
+ }
+ called = true;
+ var cursor = event.target.result;
+ t.true(cursor instanceof BridgeIDBCursor);
+
+ cursor.advance(1);
+ t.throws(
+ () => {
+ cursor.advance(1);
+ },
+ { name: "InvalidStateError" },
+ "Calling advance() should throw DOMException when the cursor is currently being iterated.",
+ );
+ t.pass();
+ resolve();
+ };
+ };
+ });
+});
+
+// IDBCursor.advance() - index - throw InvalidStateError caused by object store been deleted
+test("WPT test idbcursor_advance_index9.htm", async (t) => {
+ await new Promise<void>((resolve, reject) => {
+ var db: any;
+ const records = [
+ { pKey: "primaryKey_0", iKey: "indexKey_0" },
+ { pKey: "primaryKey_1", iKey: "indexKey_1" },
+ ];
+
+ var open_rq = createdb(t);
+ open_rq.onupgradeneeded = function (event: any) {
+ db = event.target.result;
+ var objStore = db.createObjectStore("store", { keyPath: "pKey" });
+ objStore.createIndex("index", "iKey");
+ for (var i = 0; i < records.length; i++) {
+ objStore.add(records[i]);
+ }
+ var rq = objStore.index("index").openCursor();
+ rq.onsuccess = function (event: any) {
+ var cursor = event.target.result;
+ t.true(cursor instanceof BridgeIDBCursor, "cursor exist");
+
+ db.deleteObjectStore("store");
+ t.throws(
+ () => {
+ cursor.advance(1);
+ },
+ { name: "InvalidStateError" },
+ "If the cursor's source or effective object store has been deleted, the implementation MUST throw a DOMException of type InvalidStateError",
+ );
+
+ resolve();
+ };
};
});
});
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-add-put-exception-order.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-add-put-exception-order.test.ts
new file mode 100644
index 000000000..77c4a9391
--- /dev/null
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-add-put-exception-order.test.ts
@@ -0,0 +1,104 @@
+import test, { ExecutionContext } from "ava";
+import { BridgeIDBCursor } from "..";
+import { BridgeIDBRequest } from "../bridge-idb";
+import { InvalidStateError } from "../util/errors";
+import { createdb, indexeddb_test } from "./wptsupport";
+
+async function t1(t: ExecutionContext, method: string): Promise<void> {
+ await indexeddb_test(
+ t,
+ (done, db) => {
+ const store = db.createObjectStore("s");
+ const store2 = db.createObjectStore("s2");
+
+ db.deleteObjectStore("s2");
+
+ setTimeout(() => {
+ t.throws(
+ () => {
+ (store2 as any)[method]("key", "value");
+ },
+ { name: "InvalidStateError" },
+ '"has been deleted" check (InvalidStateError) should precede ' +
+ '"not active" check (TransactionInactiveError)',
+ );
+ done();
+ }, 0);
+ },
+ (done, db) => {},
+ "t1",
+ );
+}
+
+/**
+ * IDBObjectStore.${method} exception order: 'TransactionInactiveError vs. ReadOnlyError'
+ */
+async function t2(t: ExecutionContext, method: string): Promise<void> {
+ await indexeddb_test(
+ t,
+ (done, db) => {
+ const store = db.createObjectStore("s");
+ },
+ (done, db) => {
+ (db as any)._debugName = method;
+ const tx = db.transaction("s", "readonly");
+ const store = tx.objectStore("s");
+
+ setTimeout(() => {
+ t.throws(
+ () => {
+ console.log(`calling ${method}`);
+ (store as any)[method]("key", "value");
+ },
+ {
+ name: "TransactionInactiveError",
+ },
+ '"not active" check (TransactionInactiveError) should precede ' +
+ '"read only" check (ReadOnlyError)',
+ );
+
+ done();
+ }, 0);
+
+ console.log(`queued task for ${method}`);
+ },
+ "t2",
+ );
+}
+
+/**
+ * IDBObjectStore.${method} exception order: 'ReadOnlyError vs. DataError'
+ */
+async function t3(t: ExecutionContext, method: string): Promise<void> {
+ await indexeddb_test(
+ t,
+ (done, db) => {
+ const store = db.createObjectStore("s");
+ },
+ (done, db) => {
+ const tx = db.transaction("s", "readonly");
+ const store = tx.objectStore("s");
+
+ t.throws(
+ () => {
+ (store as any)[method]({}, "value");
+ },
+ { name: "ReadOnlyError" },
+ '"read only" check (ReadOnlyError) should precede ' +
+ "key/data check (DataError)",
+ );
+
+ done();
+ },
+ "t3",
+ );
+}
+
+test("WPT idbobjectstore-add-put-exception-order.html (add, t1)", t1, "add");
+test("WPT idbobjectstore-add-put-exception-order.html (put, t1)", t1, "put");
+
+test("WPT idbobjectstore-add-put-exception-order.html (add, t2)", t2, "add");
+test("WPT idbobjectstore-add-put-exception-order.html (put, t2)", t2, "put");
+
+test("WPT idbobjectstore-add-put-exception-order.html (add, t3)", t3, "add");
+test("WPT idbobjectstore-add-put-exception-order.html (put, t3)", t3, "put");
diff --git a/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts b/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts
index 4a7205f8d..6777dc122 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts
@@ -422,3 +422,61 @@ export function format_value(val: any, seen?: any): string {
}
}
}
+
+// Usage:
+// indexeddb_test(
+// (test_object, db_connection, upgrade_tx, open_request) => {
+// // Database creation logic.
+// },
+// (test_object, db_connection, open_request) => {
+// // Test logic.
+// test_object.done();
+// },
+// 'Test case description');
+export function indexeddb_test(
+ t: ExecutionContext,
+ upgrade_func: (
+ done: () => void,
+ db: IDBDatabase,
+ tx: IDBTransaction,
+ open: IDBOpenDBRequest,
+ ) => void,
+ open_func: (
+ done: () => void,
+ db: IDBDatabase,
+ open: IDBOpenDBRequest,
+ ) => void,
+ dbsuffix?: string,
+ options?: any,
+): Promise<void> {
+ return new Promise((resolve, reject) => {
+ options = Object.assign({ upgrade_will_abort: false }, options);
+ const dbname =
+ "testdb-" + new Date().getTime() + Math.random() + (dbsuffix ?? "");
+ var del = self.indexedDB.deleteDatabase(dbname);
+ del.onerror = () => t.fail("deleteDatabase should succeed");
+ var open = self.indexedDB.open(dbname, 1);
+ open.onupgradeneeded = function () {
+ var db = open.result;
+ t.teardown(function () {
+ // If open didn't succeed already, ignore the error.
+ open.onerror = function (e) {
+ e.preventDefault();
+ };
+ db.close();
+ self.indexedDB.deleteDatabase(db.name);
+ });
+ var tx = open.transaction!;
+ upgrade_func(resolve, db, tx, open);
+ };
+ if (options.upgrade_will_abort) {
+ open.onsuccess = () => t.fail("open should not succeed");
+ } else {
+ open.onerror = () => t.fail("open should succeed");
+ open.onsuccess = function () {
+ var db = open.result;
+ if (open_func) open_func(resolve, db, open);
+ };
+ }
+ });
+}