idb: more tests, fix DB deletion, exception ordering and transaction active checks
This commit is contained in:
parent
c800e80138
commit
e6946694f2
@ -131,11 +131,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,
|
||||
);
|
||||
}
|
||||
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");
|
||||
|
@ -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>;
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
@ -1988,6 +2013,11 @@ export class BridgeIDBTransaction
|
||||
_aborted: boolean = false;
|
||||
_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
|
||||
*
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
// 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()}`,
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -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();
|
||||
};
|
||||
};
|
||||
});
|
||||
});
|
||||
|
@ -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");
|
@ -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);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user