get IDB tests to pass again, add new one

This commit is contained in:
Florian Dold 2021-02-16 10:31:55 +01:00
parent d384bd5c62
commit d1f00aeaa2
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
8 changed files with 239 additions and 119 deletions

View File

@ -17,7 +17,6 @@
import { BridgeIDBDatabaseInfo, BridgeIDBKeyRange } from "./bridge-idb";
import {
IDBCursorDirection,
IDBKeyPath,
IDBTransactionMode,
IDBValidKey,
} from "./idbtypes";

View File

@ -61,7 +61,11 @@ import FakeEventTarget from "./util/FakeEventTarget";
import { normalizeKeyPath } from "./util/normalizeKeyPath";
import openPromise from "./util/openPromise";
import queueTask from "./util/queueTask";
import { structuredClone, structuredEncapsulate, structuredRevive } from "./util/structuredClone";
import {
structuredClone,
structuredEncapsulate,
structuredRevive,
} from "./util/structuredClone";
import validateKeyPath from "./util/validateKeyPath";
import valueToKey from "./util/valueToKey";
@ -266,7 +270,7 @@ export class BridgeIDBCursor implements IDBCursor {
const transaction = this._effectiveObjectStore._transaction;
if (transaction._state !== "active") {
if (!transaction._active) {
throw new TransactionInactiveError();
}
@ -322,7 +326,7 @@ export class BridgeIDBCursor implements IDBCursor {
public continue(key?: IDBValidKey) {
const transaction = this._effectiveObjectStore._transaction;
if (transaction._state !== "active") {
if (!transaction._active) {
throw new TransactionInactiveError();
}
@ -384,7 +388,7 @@ export class BridgeIDBCursor implements IDBCursor {
public delete() {
const transaction = this._effectiveObjectStore._transaction;
if (transaction._state !== "active") {
if (!transaction._active) {
throw new TransactionInactiveError();
}
@ -455,7 +459,7 @@ export class BridgeIDBCursorWithValue extends BridgeIDBCursor {
* Ensure that an active version change transaction is currently running.
*/
const confirmActiveVersionchangeTransaction = (database: BridgeIDBDatabase) => {
if (!database._runningVersionchangeTransaction) {
if (!database._upgradeTransaction) {
throw new InvalidStateError();
}
@ -467,11 +471,11 @@ const confirmActiveVersionchangeTransaction = (database: BridgeIDBDatabase) => {
);
const transaction = transactions[transactions.length - 1];
if (!transaction || transaction._state === "finished") {
if (!transaction || transaction._finished) {
throw new InvalidStateError();
}
if (transaction._state !== "active") {
if (!transaction._active) {
throw new TransactionInactiveError();
}
@ -480,12 +484,13 @@ const confirmActiveVersionchangeTransaction = (database: BridgeIDBDatabase) => {
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#database-interface
/** @public */
export class BridgeIDBDatabase extends FakeEventTarget {
export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase {
_closePending = false;
_closed = false;
_runningVersionchangeTransaction = false;
_transactions: Array<BridgeIDBTransaction> = [];
_upgradeTransaction: BridgeIDBTransaction | null = null;
_backendConnection: DatabaseConnection;
_backend: Backend;
@ -499,8 +504,10 @@ export class BridgeIDBDatabase extends FakeEventTarget {
return this._schema.databaseVersion;
}
get objectStoreNames(): FakeDOMStringList {
return fakeDOMStringList(Object.keys(this._schema.objectStores)).sort();
get objectStoreNames(): DOMStringList {
return fakeDOMStringList(
Object.keys(this._schema.objectStores),
).sort() as DOMStringList;
}
/**
@ -509,9 +516,11 @@ export class BridgeIDBDatabase extends FakeEventTarget {
_closeConnection() {
this._closePending = true;
// Spec is unclear what "complete" means, we assume it's
// the same as "finished".
const transactionsComplete = this._transactions.every(
(transaction: BridgeIDBTransaction) => {
return transaction._state === "finished";
return transaction._finished;
},
);
@ -525,6 +534,13 @@ export class BridgeIDBDatabase extends FakeEventTarget {
}
}
/**
* Refresh the schema by querying it from the backend.
*/
_refreshSchema() {
this._schema = this._backend.getSchema(this._backendConnection);
}
constructor(backend: Backend, backendConnection: DatabaseConnection) {
super();
@ -537,7 +553,10 @@ export class BridgeIDBDatabase extends FakeEventTarget {
// http://w3c.github.io/IndexedDB/#dom-idbdatabase-createobjectstore
public createObjectStore(
name: string,
options: { autoIncrement?: boolean; keyPath?: IDBKeyPath } | null = {},
options: {
autoIncrement?: boolean;
keyPath?: null | IDBKeyPath | IDBKeyPath[];
} | null = {},
): BridgeIDBObjectStore {
if (name === undefined) {
throw new TypeError();
@ -572,7 +591,7 @@ export class BridgeIDBDatabase extends FakeEventTarget {
transaction._backend.createObjectStore(
backendTx,
name,
(keyPath !== null) ? normalizeKeyPath(keyPath) : null,
keyPath !== null ? normalizeKeyPath(keyPath) : null,
autoIncrement,
);
@ -593,6 +612,7 @@ export class BridgeIDBDatabase extends FakeEventTarget {
storeNames: string | string[],
mode?: IDBTransactionMode,
backendTransaction?: DatabaseTransaction,
openRequest?: BridgeIDBOpenDBRequest,
): BridgeIDBTransaction {
mode = mode !== undefined ? mode : "readonly";
if (
@ -603,16 +623,7 @@ export class BridgeIDBDatabase extends FakeEventTarget {
throw new TypeError("Invalid mode: " + mode);
}
const hasActiveVersionchange = this._transactions.some(
(transaction: BridgeIDBTransaction) => {
return (
transaction._state === "active" &&
transaction.mode === "versionchange" &&
transaction._db === this
);
},
);
if (hasActiveVersionchange) {
if (this._upgradeTransaction) {
throw new InvalidStateError();
}
@ -627,7 +638,7 @@ export class BridgeIDBDatabase extends FakeEventTarget {
throw new InvalidAccessError();
}
for (const storeName of storeNames) {
if (this.objectStoreNames.indexOf(storeName) < 0) {
if (!this.objectStoreNames.contains(storeName)) {
throw new NotFoundError(
"No objectStore named " + storeName + " in this database",
);
@ -639,9 +650,12 @@ export class BridgeIDBDatabase extends FakeEventTarget {
mode,
this,
backendTransaction,
openRequest,
);
this._transactions.push(tx);
queueTask(() => tx._start());
// "When a transaction is created its active flag is initially set."
tx._active = true;
return tx;
}
@ -809,20 +823,25 @@ export class BridgeIDBFactory {
dbconn,
requestedVersion,
);
db._runningVersionchangeTransaction = true;
const transaction = db._internalTransaction(
[],
"versionchange",
backendTransaction,
request,
);
db._upgradeTransaction = transaction;
const event = new BridgeIDBVersionChangeEvent("upgradeneeded", {
newVersion: version,
oldVersion: existingVersion,
});
request.result = db;
transaction._active = true;
request.readyState = "done";
request.result = db;
request.transaction = transaction;
request.dispatchEvent(event);
@ -832,15 +851,30 @@ export class BridgeIDBFactory {
// We don't explicitly exit the versionchange transaction,
// since this is already done by the BridgeIDBTransaction.
db._runningVersionchangeTransaction = false;
db._upgradeTransaction = null;
const event2 = new FakeEvent("success", {
bubbles: false,
cancelable: false,
});
event2.eventPath = [request];
// We re-use the same transaction (as per spec) here.
transaction._active = true;
if (transaction._aborted) {
request.result = undefined;
request.error = new AbortError();
request.readyState = "done";
const event2 = new FakeEvent("error", {
bubbles: false,
cancelable: false,
});
event2.eventPath = [request];
request.dispatchEvent(event2);
} else {
console.log(`dispatching success event, _active=${transaction._active}`);
const event2 = new FakeEvent("success", {
bubbles: false,
cancelable: false,
});
event2.eventPath = [request];
request.dispatchEvent(event2);
request.dispatchEvent(event2);
}
}
this.connections.push(db);
@ -871,7 +905,7 @@ const confirmActiveTransaction = (
throw new InvalidStateError();
}
if (index._objectStore._transaction._state !== "active") {
if (!index._objectStore._transaction._active) {
throw new TransactionInactiveError();
}
@ -931,11 +965,11 @@ export class BridgeIDBIndex implements IDBIndex {
set name(name: any) {
const transaction = this._objectStore._transaction;
if (!transaction._db._runningVersionchangeTransaction) {
if (!transaction._db._upgradeTransaction) {
throw new InvalidStateError();
}
if (transaction._state !== "active") {
if (!transaction._active) {
throw new TransactionInactiveError();
}
@ -1282,7 +1316,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
get _indexNames(): FakeDOMStringList {
return fakeDOMStringList(
Object.keys(this._schema.objectStores[this._name].indexes),
).sort()
).sort();
}
get indexNames(): DOMStringList {
@ -1330,7 +1364,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
set name(newName: any) {
const transaction = this._transaction;
if (!transaction._db._runningVersionchangeTransaction) {
if (!transaction._db._upgradeTransaction) {
throw new InvalidStateError();
}
@ -1581,7 +1615,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
throw new TypeError();
}
if (!this._transaction._db._runningVersionchangeTransaction) {
if (!this._transaction._db._upgradeTransaction) {
throw new InvalidStateError();
}
@ -1628,7 +1662,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
throw new TypeError();
}
if (this._transaction._state === "finished") {
if (this._transaction._finished) {
throw new InvalidStateError();
}
@ -1649,7 +1683,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
throw new InvalidStateError();
}
if (!this._transaction._db._runningVersionchangeTransaction) {
if (!this._transaction._db._upgradeTransaction) {
throw new InvalidStateError();
}
@ -1755,6 +1789,7 @@ export class BridgeIDBRequest extends FakeEventTarget implements IDBRequest {
cancelable: true,
});
event.eventPath = [];
this.dispatchEvent(event);
}
@ -1791,24 +1826,41 @@ export class BridgeIDBOpenDBRequest
export class BridgeIDBTransaction
extends FakeEventTarget
implements IDBTransaction {
public _state: "active" | "inactive" | "committing" | "finished" = "active";
public _started = false;
public _objectStoresCache: Map<string, BridgeIDBObjectStore> = new Map();
_committed: boolean = false;
/**
* A transaction is active as long as new operations can be
* placed against it.
*/
_active: boolean = false;
_started: boolean = false;
_aborted: boolean = false;
_objectStoresCache: Map<string, BridgeIDBObjectStore> = new Map();
public _backendTransaction?: DatabaseTransaction;
/**
* https://www.w3.org/TR/IndexedDB-2/#transaction-lifetime-concept
*
* When a transaction is committed or aborted, it is said to be finished.
*/
get _finished(): boolean {
return this._committed || this._aborted;
}
public _objectStoreNames: FakeDOMStringList;
_openRequest: BridgeIDBOpenDBRequest | null = null;
_backendTransaction?: DatabaseTransaction;
_objectStoreNames: FakeDOMStringList;
get objectStoreNames(): DOMStringList {
return this._objectStoreNames as DOMStringList;
}
public mode: IDBTransactionMode;
public _db: BridgeIDBDatabase;
mode: IDBTransactionMode;
_db: BridgeIDBDatabase;
get db(): IDBDatabase {
return this.db;
return this._db;
}
public _error: Error | null = null;
_error: Error | null = null;
get error(): DOMException {
return this._error as DOMException;
@ -1823,7 +1875,7 @@ export class BridgeIDBTransaction
public _scope: Set<string>;
private _requests: Array<{
operation: () => void;
operation: () => Promise<void>;
request: BridgeIDBRequest;
}> = [];
@ -1836,6 +1888,7 @@ export class BridgeIDBTransaction
mode: IDBTransactionMode,
db: BridgeIDBDatabase,
backendTransaction?: DatabaseTransaction,
openRequest?: BridgeIDBOpenDBRequest,
) {
super();
@ -1850,11 +1903,17 @@ export class BridgeIDBTransaction
this._objectStoreNames = fakeDOMStringList(Array.from(this._scope).sort());
this._db._transactions.push(this);
this._openRequest = openRequest ?? null;
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-aborting-a-transaction
async _abort(errName: string | null) {
this._state = "finished";
if (BridgeIDBFactory.enableTracing) {
console.log("TRACE: aborting transaction");
}
this._aborted = true;
if (errName !== null) {
const e = new Error();
@ -1862,30 +1921,45 @@ export class BridgeIDBTransaction
this._error = e;
}
if (BridgeIDBFactory.enableTracing) {
console.log(`TRACE: aborting ${this._requests.length} requests`);
}
// Should this directly remove from _requests?
for (const { request } of this._requests) {
console.log("ready state:", request.readyState);
if (request.readyState !== "done") {
request.readyState = "done"; // This will cancel execution of this request's operation
if (request._source) {
request.result = undefined;
request.error = new AbortError();
const event = new FakeEvent("error", {
bubbles: true,
cancelable: true,
});
event.eventPath = [this._db, this];
request.dispatchEvent(event);
// This will cancel execution of this request's operation
request.readyState = "done";
if (BridgeIDBFactory.enableTracing) {
console.log("dispatching error event");
}
request.result = undefined;
request.error = new AbortError();
const event = new FakeEvent("error", {
bubbles: true,
cancelable: true,
});
event.eventPath = [request, this, this._db];
console.log("dispatching error event for request after abort");
request.dispatchEvent(event);
}
}
// ("abort a transaction", step 5.1)
if (this._openRequest) {
this._db._upgradeTransaction = null;
}
// Only roll back if we actually executed the scheduled operations.
const maybeBtx = this._backendTransaction;
if (maybeBtx) {
await this._backend.rollback(maybeBtx);
}
this._db._refreshSchema();
queueTask(() => {
const event = new FakeEvent("abort", {
bubbles: true,
@ -1894,20 +1968,24 @@ export class BridgeIDBTransaction
event.eventPath = [this._db];
this.dispatchEvent(event);
});
if (this._openRequest) {
this._openRequest.transaction = null;
this._openRequest.result = undefined;
this._openRequest.readyState = "pending";
}
}
public abort() {
if (this._state === "committing" || this._state === "finished") {
if (this._finished) {
throw new InvalidStateError();
}
this._state = "active";
this._abort(null);
}
// http://w3c.github.io/IndexedDB/#dom-idbtransaction-objectstore
public objectStore(name: string): BridgeIDBObjectStore {
if (this._state !== "active") {
if (!this._active) {
throw new InvalidStateError();
}
@ -1925,7 +2003,7 @@ export class BridgeIDBTransaction
const operation = obj.operation;
let request = obj.hasOwnProperty("request") ? obj.request : null;
if (this._state !== "active") {
if (!this._active) {
throw new TransactionInactiveError();
}
@ -2001,10 +2079,8 @@ export class BridgeIDBTransaction
request.result = result;
request.error = undefined;
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-fire-a-success-event
if (this._state === "inactive") {
this._state = "active";
}
// https://www.w3.org/TR/IndexedDB-2/#fire-error-event
this._active = true;
event = new FakeEvent("success", {
bubbles: false,
cancelable: false,
@ -2014,9 +2090,11 @@ export class BridgeIDBTransaction
event.eventPath = [request, this, this._db];
request.dispatchEvent(event);
} catch (err) {
if (this._state !== "committing") {
this._abort("AbortError");
if (BridgeIDBFactory.enableTracing) {
console.log("TRACING: caught error in transaction success event handler");
}
this._abort("AbortError");
this._active = false;
throw err;
}
} catch (err) {
@ -2028,9 +2106,7 @@ export class BridgeIDBTransaction
request.error = err;
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-fire-an-error-event
if (this._state === "inactive") {
this._state = "active";
}
this._active = true;
event = new FakeEvent("error", {
bubbles: true,
cancelable: true,
@ -2040,9 +2116,7 @@ export class BridgeIDBTransaction
event.eventPath = [this._db, this];
request.dispatchEvent(event);
} catch (err) {
if (this._state !== "committing") {
this._abort("AbortError");
}
this._abort("AbortError");
throw err;
}
if (!event.canceled) {
@ -2061,17 +2135,13 @@ export class BridgeIDBTransaction
return;
}
if (this._state !== "finished" && this._state !== "committing") {
if (!this._finished && !this._committed) {
if (BridgeIDBFactory.enableTracing) {
console.log("finishing transaction");
}
this._state = "committing";
await this._backend.commit(this._backendTransaction);
this._state = "finished";
this._committed = true;
if (!this._error) {
if (BridgeIDBFactory.enableTracing) {
console.log("dispatching 'complete' event on transaction");
@ -2089,15 +2159,19 @@ export class BridgeIDBTransaction
this._resolveWait();
}
if (this._aborted) {
this._resolveWait();
}
}
public commit() {
if (this._state !== "active") {
// The current spec doesn't even have an explicit commit method.
// We still support it, effectively as a "no-operation" that
// prevents new operations from being scheduled.
if (!this._active) {
throw new InvalidStateError();
}
this._state = "committing";
// We now just wait for auto-commit ...
this._active = false;
}
public toString() {

View File

@ -0,0 +1,34 @@
import test from "ava";
import { createdb } from "./wptsupport";
test("WPT test abort-in-initial-upgradeneeded.htm", async (t) => {
await new Promise<void>((resolve, reject) => {
var db: any;
var open_rq = createdb(t, undefined, 2);
open_rq.onupgradeneeded = function (e) {
const tgt = e.target as any;
db = tgt.result;
t.assert(db.version === 2);
var transaction = tgt.transaction;
transaction.oncomplete = () => t.fail("unexpected transaction.complete");
transaction.onabort = function (e: any) {
console.log(`version: ${e.target.db.version}`);
t.deepEqual(e.target.db.version, 0);
};
db.onabort = function () {};
transaction.abort();
};
open_rq.onerror = function (e) {
const tgt = e.target as any;
t.deepEqual(open_rq, e.target);
t.deepEqual(tgt.result, undefined);
t.deepEqual(tgt.error.name, "AbortError");
console.log(`version (onerror): ${db.version}`);
t.deepEqual(db.version, 0);
t.deepEqual(open_rq.transaction, null);
resolve();
};
});
});

View File

@ -24,23 +24,24 @@ test.cb("WPT test value.htm, array", (t) => {
});
test.cb("WPT test value.htm, date", (t) => {
const value = new Date();
const _instanceof = Date;
t.plan(1);
createdb(t).onupgradeneeded = function (e: IDBVersionChangeEvent) {
(e.target as any).result.createObjectStore("store").add(value, 1);
(e.target as any).onsuccess = (e: any) => {
console.log("in first onsuccess");
e.target.result
.transaction("store")
.objectStore("store")
.get(1).onsuccess = (e: any) => {
t.assert(e.target.result instanceof _instanceof, "instanceof");
t.end();
};
const value = new Date();
const _instanceof = Date;
t.plan(1);
createdb(t).onupgradeneeded = function (e: IDBVersionChangeEvent) {
(e.target as any).result.createObjectStore("store").add(value, 1);
(e.target as any).onsuccess = (e: any) => {
console.log("in first onsuccess");
e.target.result
.transaction("store")
.objectStore("store")
.get(1).onsuccess = (e: any) => {
console.log("target", e.target);
console.log("result", e.target.result);
t.assert(e.target.result instanceof _instanceof, "instanceof");
t.end();
};
};
});
};
});

View File

@ -23,8 +23,18 @@ export function createdb(
return rq_open;
}
export function assert_key_equals(actual: any, expected: any, description?: string) {
export function assert_key_equals(
actual: any,
expected: any,
description?: string,
) {
if (0 != compareKeys(actual, expected)) {
throw Error("expected keys to be the same");
}
}
export function assert_equals(actual: any, expected: any) {
if (actual !== expected) {
throw Error("assert_equals failed");
}
}

View File

@ -97,13 +97,14 @@ abstract class FakeEventTarget implements EventTarget {
public readonly listeners: Listener[] = [];
// These will be overridden in individual subclasses and made not readonly
public readonly onabort: EventListener | null | undefined;
public readonly onblocked: EventListener | null | undefined;
public readonly oncomplete: EventListener | null | undefined;
public readonly onerror: EventListener | null | undefined;
public readonly onsuccess: EventListener | null | undefined;
public readonly onupgradeneeded: EventListener | null | undefined;
public readonly onversionchange: EventListener | null | undefined;
public readonly onabort: EventListener | null = null;
public readonly onblocked: EventListener | null = null;
public readonly oncomplete: EventListener | null = null;
public readonly onerror: EventListener | null = null;
public readonly onsuccess: EventListener | null = null;
public readonly onclose: EventListener | null = null;
public readonly onupgradeneeded: EventListener | null = null;
public readonly onversionchange: EventListener | null = null;
static enableTracing: boolean = false;

View File

@ -17,7 +17,7 @@
import { IDBKeyPath } from "../idbtypes";
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-valid-key-path
const validateKeyPath = (keyPath: IDBKeyPath, parent?: "array" | "string") => {
const validateKeyPath = (keyPath: IDBKeyPath | IDBKeyPath[], parent?: "array" | "string") => {
// This doesn't make sense to me based on the spec, but it is needed to pass the W3C KeyPath tests (see same
// comment in extractKey)
let myKeyPath: IDBKeyPath | IDBKeyPath[] = keyPath;

View File

@ -5,6 +5,7 @@
"module": "ESNext",
"moduleResolution": "node",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"outDir": "lib",
"declaration": true,