idb: more tests, fix DB deletion, exception ordering and transaction active checks

This commit is contained in:
Florian Dold 2021-02-19 21:27:49 +01:00
parent c800e80138
commit e6946694f2
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
6 changed files with 548 additions and 119 deletions

View File

@ -131,11 +131,6 @@ interface Connection {
modifiedSchema: Schema; modifiedSchema: Schema;
/**
* Has the underlying database been deleted?
*/
deleted: boolean;
/** /**
* Map from the effective name of an object store during * Map from the effective name of an object store during
* the transaction to the real name. * the transaction to the real name.
@ -412,13 +407,9 @@ export class MemoryBackend implements Backend {
return dbList; return dbList;
} }
async deleteDatabase(tx: DatabaseTransaction, name: string): Promise<void> { async deleteDatabase(name: string): Promise<void> {
if (this.enableTracing) { if (this.enableTracing) {
console.log("TRACING: deleteDatabase"); console.log(`TRACING: deleteDatabase(${name})`);
}
const myConn = this.connectionsByTransaction[tx.transactionCookie];
if (!myConn) {
throw Error("no connection associated with transaction");
} }
const myDb = this.databases[name]; const myDb = this.databases[name];
if (!myDb) { if (!myDb) {
@ -427,13 +418,13 @@ export class MemoryBackend implements Backend {
if (myDb.committedSchema.databaseName !== name) { if (myDb.committedSchema.databaseName !== name) {
throw Error("name does not match"); 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; myDb.deleted = true;
delete this.databases[name];
} }
async connectDatabase(name: string): Promise<DatabaseConnection> { async connectDatabase(name: string): Promise<DatabaseConnection> {
@ -469,7 +460,6 @@ export class MemoryBackend implements Backend {
const myConn: Connection = { const myConn: Connection = {
dbName: name, dbName: name,
deleted: false,
objectStoreMap: this.makeObjectStoreMap(database), objectStoreMap: this.makeObjectStoreMap(database),
modifiedSchema: structuredClone(database.committedSchema), modifiedSchema: structuredClone(database.committedSchema),
}; };
@ -560,28 +550,38 @@ export class MemoryBackend implements Backend {
if (!myConn) { if (!myConn) {
throw Error("connection not found - already closed?"); throw Error("connection not found - already closed?");
} }
if (!myConn.deleted) { const myDb = this.databases[myConn.dbName];
const myDb = this.databases[myConn.dbName]; // FIXME: what if we're still in a transaction?
// if (myDb.connectionCookies.includes(conn.connectionCookie)) { myDb.connectionCookies = myDb.connectionCookies.filter(
// throw Error("invalid state"); (x) => x != conn.connectionCookie,
// } );
// FIXME: what if we're still in a transaction?
myDb.connectionCookies = myDb.connectionCookies.filter(
(x) => x != conn.connectionCookie,
);
}
delete this.connections[conn.connectionCookie]; delete this.connections[conn.connectionCookie];
this.disconnectCond.trigger(); 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 { getSchema(dbConn: DatabaseConnection): Schema {
if (this.enableTracing) { if (this.enableTracing) {
console.log(`TRACING: getSchema`); console.log(`TRACING: getSchema`);
} }
const myConn = this.connections[dbConn.connectionCookie]; const myConn = this.requireConnection(dbConn);
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName]; const db = this.databases[myConn.dbName];
if (!db) { if (!db) {
throw Error("db not found"); throw Error("db not found");
@ -590,10 +590,7 @@ export class MemoryBackend implements Backend {
} }
getCurrentTransactionSchema(btx: DatabaseTransaction): Schema { getCurrentTransactionSchema(btx: DatabaseTransaction): Schema {
const myConn = this.connectionsByTransaction[btx.transactionCookie]; const myConn = this.requireConnectionFromTransaction(btx);
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName]; const db = this.databases[myConn.dbName];
if (!db) { if (!db) {
throw Error("db not found"); throw Error("db not found");
@ -602,10 +599,7 @@ export class MemoryBackend implements Backend {
} }
getInitialTransactionSchema(btx: DatabaseTransaction): Schema { getInitialTransactionSchema(btx: DatabaseTransaction): Schema {
const myConn = this.connectionsByTransaction[btx.transactionCookie]; const myConn = this.requireConnectionFromTransaction(btx);
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName]; const db = this.databases[myConn.dbName];
if (!db) { if (!db) {
throw Error("db not found"); throw Error("db not found");
@ -622,10 +616,7 @@ export class MemoryBackend implements Backend {
if (this.enableTracing) { if (this.enableTracing) {
console.log(`TRACING: renameIndex(?, ${oldName}, ${newName})`); console.log(`TRACING: renameIndex(?, ${oldName}, ${newName})`);
} }
const myConn = this.connectionsByTransaction[btx.transactionCookie]; const myConn = this.requireConnectionFromTransaction(btx);
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName]; const db = this.databases[myConn.dbName];
if (!db) { if (!db) {
throw Error("db not found"); throw Error("db not found");
@ -664,10 +655,7 @@ export class MemoryBackend implements Backend {
if (this.enableTracing) { if (this.enableTracing) {
console.log(`TRACING: deleteIndex(${indexName})`); console.log(`TRACING: deleteIndex(${indexName})`);
} }
const myConn = this.connectionsByTransaction[btx.transactionCookie]; const myConn = this.requireConnectionFromTransaction(btx);
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName]; const db = this.databases[myConn.dbName];
if (!db) { if (!db) {
throw Error("db not found"); throw Error("db not found");
@ -698,10 +686,7 @@ export class MemoryBackend implements Backend {
`TRACING: deleteObjectStore(${name}) in ${btx.transactionCookie}`, `TRACING: deleteObjectStore(${name}) in ${btx.transactionCookie}`,
); );
} }
const myConn = this.connectionsByTransaction[btx.transactionCookie]; const myConn = this.requireConnectionFromTransaction(btx);
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName]; const db = this.databases[myConn.dbName];
if (!db) { if (!db) {
throw Error("db not found"); throw Error("db not found");
@ -740,10 +725,7 @@ export class MemoryBackend implements Backend {
console.log(`TRACING: renameObjectStore(?, ${oldName}, ${newName})`); console.log(`TRACING: renameObjectStore(?, ${oldName}, ${newName})`);
} }
const myConn = this.connectionsByTransaction[btx.transactionCookie]; const myConn = this.requireConnectionFromTransaction(btx);
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName]; const db = this.databases[myConn.dbName];
if (!db) { if (!db) {
throw Error("db not found"); throw Error("db not found");
@ -783,10 +765,7 @@ export class MemoryBackend implements Backend {
`TRACING: createObjectStore(${btx.transactionCookie}, ${name})`, `TRACING: createObjectStore(${btx.transactionCookie}, ${name})`,
); );
} }
const myConn = this.connectionsByTransaction[btx.transactionCookie]; const myConn = this.requireConnectionFromTransaction(btx);
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName]; const db = this.databases[myConn.dbName];
if (!db) { if (!db) {
throw Error("db not found"); throw Error("db not found");
@ -828,10 +807,7 @@ export class MemoryBackend implements Backend {
if (this.enableTracing) { if (this.enableTracing) {
console.log(`TRACING: createIndex(${indexName})`); console.log(`TRACING: createIndex(${indexName})`);
} }
const myConn = this.connectionsByTransaction[btx.transactionCookie]; const myConn = this.requireConnectionFromTransaction(btx);
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName]; const db = this.databases[myConn.dbName];
if (!db) { if (!db) {
throw Error("db not found"); throw Error("db not found");
@ -892,10 +868,7 @@ export class MemoryBackend implements Backend {
if (this.enableTracing) { if (this.enableTracing) {
console.log(`TRACING: deleteRecord from store ${objectStoreName}`); console.log(`TRACING: deleteRecord from store ${objectStoreName}`);
} }
const myConn = this.connectionsByTransaction[btx.transactionCookie]; const myConn = this.requireConnectionFromTransaction(btx);
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName]; const db = this.databases[myConn.dbName];
if (!db) { if (!db) {
throw Error("db not found"); throw Error("db not found");
@ -1057,10 +1030,7 @@ export class MemoryBackend implements Backend {
console.log(`TRACING: getRecords`); console.log(`TRACING: getRecords`);
console.log("query", req); console.log("query", req);
} }
const myConn = this.connectionsByTransaction[btx.transactionCookie]; const myConn = this.requireConnectionFromTransaction(btx);
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName]; const db = this.databases[myConn.dbName];
if (!db) { if (!db) {
throw Error("db not found"); throw Error("db not found");
@ -1388,10 +1358,7 @@ export class MemoryBackend implements Backend {
if (this.enableTracing) { if (this.enableTracing) {
console.log(`TRACING: storeRecord`); console.log(`TRACING: storeRecord`);
} }
const myConn = this.connectionsByTransaction[btx.transactionCookie]; const myConn = this.requireConnectionFromTransaction(btx);
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName]; const db = this.databases[myConn.dbName];
if (!db) { if (!db) {
throw Error("db not found"); throw Error("db not found");
@ -1626,10 +1593,7 @@ export class MemoryBackend implements Backend {
if (this.enableTracing) { if (this.enableTracing) {
console.log(`TRACING: commit`); console.log(`TRACING: commit`);
} }
const myConn = this.connectionsByTransaction[btx.transactionCookie]; const myConn = this.requireConnectionFromTransaction(btx);
if (!myConn) {
throw Error("unknown connection");
}
const db = this.databases[myConn.dbName]; const db = this.databases[myConn.dbName];
if (!db) { if (!db) {
throw Error("db not found"); throw Error("db not found");

View File

@ -21,7 +21,6 @@ import {
IDBValidKey, IDBValidKey,
} from "./idbtypes"; } from "./idbtypes";
/** @public */ /** @public */
export interface ObjectStoreProperties { export interface ObjectStoreProperties {
keyPath: string[] | null; keyPath: string[] | null;
@ -151,12 +150,7 @@ export interface Backend {
newVersion: number, newVersion: number,
): Promise<DatabaseTransaction>; ): Promise<DatabaseTransaction>;
/** deleteDatabase(name: string): Promise<void>;
* 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>;
close(db: DatabaseConnection): Promise<void>; close(db: DatabaseConnection): Promise<void>;

View File

@ -195,7 +195,10 @@ export class BridgeIDBCursor implements IDBCursor {
/** /**
* https://w3c.github.io/IndexedDB/#iterate-a-cursor * 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 && BridgeIDBFactory.enableTracing &&
console.log( console.log(
`iterating cursor os=${this._objectStoreName},idx=${this._indexName}`, `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 * http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBCursor-advance-void-unsigned-long-count
*/ */
public advance(count: number) { public advance(count: number) {
if (typeof count !== "number" || count <= 0) {
throw TypeError("count must be positive number");
}
const transaction = this._effectiveObjectStore._transaction; const transaction = this._effectiveObjectStore._transaction;
if (!transaction._active) { if (!transaction._active) {
@ -337,9 +344,11 @@ export class BridgeIDBCursor implements IDBCursor {
} }
const operation = async () => { const operation = async () => {
let res: IDBCursor | null = null;
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
await this._iterate(); res = await this._iterate();
} }
return res;
}; };
transaction._execRequestAsync({ transaction._execRequestAsync({
@ -527,6 +536,11 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase {
_schema: Schema; _schema: Schema;
/**
* Name that can be set to identify the object store in logs.
*/
_debugName: string | undefined = undefined;
get name(): string { get name(): string {
return this._schema.databaseName; return this._schema.databaseName;
} }
@ -686,12 +700,23 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase {
openRequest, openRequest,
); );
this._transactions.push(tx); 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." // "When a transaction is created its active flag is initially set."
tx._active = true; tx._active = true;
return tx; return tx;
} }
_getReadableName(): string {
return `${this.name}(${this._debugName ?? "??"})`;
}
public transaction( public transaction(
storeNames: string | string[], storeNames: string | string[],
mode?: IDBTransactionMode, mode?: IDBTransactionMode,
@ -745,15 +770,7 @@ export class BridgeIDBFactory {
const oldVersion = dbInfo.version; const oldVersion = dbInfo.version;
try { try {
const dbconn = await this.backend.connectDatabase(name); await this.backend.deleteDatabase(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);
request.result = undefined; request.result = undefined;
request.readyState = "done"; request.readyState = "done";
@ -797,15 +814,11 @@ export class BridgeIDBFactory {
let dbconn: DatabaseConnection; let dbconn: DatabaseConnection;
try { try {
if (BridgeIDBFactory.enableTracing) { if (BridgeIDBFactory.enableTracing) {
console.log( console.log("TRACE: connecting to database");
"TRACE: connecting to database",
);
} }
dbconn = await this.backend.connectDatabase(name); dbconn = await this.backend.connectDatabase(name);
if (BridgeIDBFactory.enableTracing) { if (BridgeIDBFactory.enableTracing) {
console.log( console.log("TRACE: connected!");
"TRACE: connected!",
);
} }
} catch (err) { } catch (err) {
if (BridgeIDBFactory.enableTracing) { if (BridgeIDBFactory.enableTracing) {
@ -1385,6 +1398,11 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
_transaction: BridgeIDBTransaction; _transaction: BridgeIDBTransaction;
/**
* Name that can be set to identify the object store in logs.
*/
_debugName: string | undefined = undefined;
get transaction(): IDBTransaction { get transaction(): IDBTransaction {
return this._transaction; return this._transaction;
} }
@ -1490,8 +1508,15 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
public _store(value: any, key: IDBValidKey | undefined, overwrite: boolean) { public _store(value: any, key: IDBValidKey | undefined, overwrite: boolean) {
if (BridgeIDBFactory.enableTracing) { 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") { if (this._transaction.mode === "readonly") {
throw new ReadOnlyError(); throw new ReadOnlyError();
} }
@ -1988,6 +2013,11 @@ export class BridgeIDBTransaction
_aborted: boolean = false; _aborted: boolean = false;
_objectStoresCache: Map<string, BridgeIDBObjectStore> = new Map(); _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 * https://www.w3.org/TR/IndexedDB-2/#transaction-lifetime-concept
* *
@ -2074,7 +2104,12 @@ export class BridgeIDBTransaction
console.log("TRACE: aborting transaction"); console.log("TRACE: aborting transaction");
} }
if (this._aborted) {
return;
}
this._aborted = true; this._aborted = true;
this._active = false;
if (errName !== null) { if (errName !== null) {
const e = new Error(); const e = new Error();
@ -2116,6 +2151,7 @@ export class BridgeIDBTransaction
this._db._schema = this._backend.getInitialTransactionSchema(maybeBtx); this._db._schema = this._backend.getInitialTransactionSchema(maybeBtx);
// Only roll back if we actually executed the scheduled operations. // Only roll back if we actually executed the scheduled operations.
await this._backend.rollback(maybeBtx); await this._backend.rollback(maybeBtx);
this._backendTransaction = undefined;
} else { } else {
this._db._schema = this._backend.getSchema(this._db._backendConnection); this._db._schema = this._backend.getSchema(this._db._backendConnection);
} }
@ -2208,17 +2244,11 @@ export class BridgeIDBTransaction
`TRACE: IDBTransaction._start, ${this._requests.length} queued`, `TRACE: IDBTransaction._start, ${this._requests.length} queued`,
); );
} }
this._started = true; this._started = true;
if (!this._backendTransaction) { // Remove from request queue - cursor ones will be added back if necessary
this._backendTransaction = await this._backend.beginTransaction( // by cursor.continue and such
this._db._backendConnection,
Array.from(this._scope),
this.mode,
);
}
// Remove from request queue - cursor ones will be added back if necessary by cursor.continue and such
let operation; let operation;
let request; let request;
while (this._requests.length > 0) { while (this._requests.length > 0) {
@ -2233,9 +2263,25 @@ export class BridgeIDBTransaction
} }
if (request && operation) { 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) { if (!request._source) {
// Special requests like indexes that just need to run some code, with error handling already built into // Special requests like indexes that just need to run some code,
// operation // with error handling already built into operation
await operation(); await operation();
} else { } else {
let event; let event;
@ -2311,10 +2357,18 @@ export class BridgeIDBTransaction
if (!this._finished && !this._committed) { if (!this._finished && !this._committed) {
if (BridgeIDBFactory.enableTracing) { 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; this._committed = true;
if (!this._error) { if (!this._error) {
if (BridgeIDBFactory.enableTracing) { if (BridgeIDBFactory.enableTracing) {

View File

@ -1,5 +1,7 @@
import test from "ava"; import test from "ava";
import { BridgeIDBCursor } from ".."; import { BridgeIDBCursor } from "..";
import { BridgeIDBRequest } from "../bridge-idb";
import { InvalidStateError } from "../util/errors";
import { createdb } from "./wptsupport"; import { createdb } from "./wptsupport";
test("WPT test idbcursor_advance_index.htm", async (t) => { 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) { cursor_rq.onsuccess = function (e: any) {
var cursor = e.target.result; var cursor = e.target.result;
t.log(cursor); t.log(cursor);
t.true(e.target instanceof BridgeIDBRequest);
t.true(cursor instanceof BridgeIDBCursor); t.true(cursor instanceof BridgeIDBCursor);
switch (count) { switch (count) {
@ -51,7 +54,259 @@ test("WPT test idbcursor_advance_index.htm", async (t) => {
t.fail("unexpected count"); t.fail("unexpected count");
break; 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();
};
}; };
}); });
}); });

View File

@ -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");

View File

@ -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);
};
}
});
}