From 3eced74a88de43ab9afe542fcce20a8db8e3fe60 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 22 Feb 2021 14:27:54 +0100 Subject: more tests, fix event ordering issue --- packages/idb-bridge/src/bridge-idb.ts | 44 ++++++++++++++- .../event-dispatch-active-flag.test.ts | 57 +++++++++++++++++++ .../idbobjectstore-add-put-exception-order.test.ts | 1 - .../idbtransaction-oncomplete.test.ts | 49 +++++++++++++++++ .../idb-bridge/src/idb-wpt-ported/wptsupport.ts | 64 +++++++++++++++++++++- packages/idb-bridge/src/util/queueTask.ts | 15 ++++- 6 files changed, 224 insertions(+), 6 deletions(-) create mode 100644 packages/idb-bridge/src/idb-wpt-ported/event-dispatch-active-flag.test.ts create mode 100644 packages/idb-bridge/src/idb-wpt-ported/idbtransaction-oncomplete.test.ts diff --git a/packages/idb-bridge/src/bridge-idb.ts b/packages/idb-bridge/src/bridge-idb.ts index 643a98dea..ceba618db 100644 --- a/packages/idb-bridge/src/bridge-idb.ts +++ b/packages/idb-bridge/src/bridge-idb.ts @@ -1609,6 +1609,10 @@ export class BridgeIDBObjectStore implements IDBObjectStore { throw new TypeError(); } + if (!this._transaction._active) { + throw new TransactionInactiveError(); + } + if (this._deleted) { throw new InvalidStateError( "tried to call 'delete' on a deleted object store", @@ -1918,6 +1922,8 @@ export class BridgeIDBRequest extends FakeEventTarget implements IDBRequest { onsuccess: EventListener | null = null; onerror: EventListener | null = null; + _debugName: string | undefined; + get error() { if (this.readyState === "pending") { throw new InvalidStateError(); @@ -1998,6 +2004,25 @@ export class BridgeIDBOpenDBRequest } } +function waitMacroQueue(): Promise { + return new Promise((resolve, reject) => { + let immCalled = false; + let timeoutCalled = false; + setImmediate(() => { + immCalled = true; + if (immCalled && timeoutCalled) { + resolve(); + } + }); + setTimeout(() => { + timeoutCalled = true; + if (immCalled && timeoutCalled) { + resolve(); + } + }, 0); + }); +} + // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#transaction /** @public */ export class BridgeIDBTransaction @@ -2182,7 +2207,7 @@ export class BridgeIDBTransaction // http://w3c.github.io/IndexedDB/#dom-idbtransaction-objectstore public objectStore(name: string): BridgeIDBObjectStore { if (!this._active) { - throw new InvalidStateError(); + throw new TransactionInactiveError(); } if (!this._db._schema.objectStores[name]) { @@ -2279,6 +2304,8 @@ export class BridgeIDBTransaction } } + await waitMacroQueue(); + if (!request._source) { // Special requests like indexes that just need to run some code, // with error handling already built into operation @@ -2289,9 +2316,12 @@ export class BridgeIDBTransaction BridgeIDBFactory.enableTracing && console.log("TRACE: running operation in transaction"); const result = await operation(); + // Wait until setTimeout/setImmediate tasks are run BridgeIDBFactory.enableTracing && console.log( - "TRACE: operation in transaction finished with success", + `TRACE: request (${ + request._debugName ?? "??" + }) in transaction finished with success`, ); request.readyState = "done"; request.result = result; @@ -2304,6 +2334,10 @@ export class BridgeIDBTransaction cancelable: false, }); + queueTask(() => { + this._active = false; + }); + try { event.eventPath = [this._db, this]; request.dispatchEvent(event); @@ -2372,7 +2406,11 @@ export class BridgeIDBTransaction this._committed = true; if (!this._error) { if (BridgeIDBFactory.enableTracing) { - console.log("dispatching 'complete' event on transaction"); + console.log( + `dispatching 'complete' event on transaction (${ + this._debugName ?? "??" + })`, + ); } const event = new FakeEvent("complete"); event.eventPath = [this._db, this]; diff --git a/packages/idb-bridge/src/idb-wpt-ported/event-dispatch-active-flag.test.ts b/packages/idb-bridge/src/idb-wpt-ported/event-dispatch-active-flag.test.ts new file mode 100644 index 000000000..f5668c90b --- /dev/null +++ b/packages/idb-bridge/src/idb-wpt-ported/event-dispatch-active-flag.test.ts @@ -0,0 +1,57 @@ +import test from "ava"; +import { BridgeIDBRequest } from ".."; +import { + createdb, + indexeddb_test, + is_transaction_active, + keep_alive, +} from "./wptsupport"; + +test("WPT test abort-in-initial-upgradeneeded.htm", async (t) => { + // Transactions are active during success handlers + await indexeddb_test( + t, + (done, db, tx) => { + db.createObjectStore("store"); + }, + (done, db) => { + const tx = db.transaction("store"); + const release_tx = keep_alive(t, tx, "store"); + + t.assert( + is_transaction_active(t, tx, "store"), + "Transaction should be active after creation", + ); + + const request = tx.objectStore("store").get(4242); + (request as BridgeIDBRequest)._debugName = "req-main"; + request.onerror = () => t.fail("request should succeed"); + request.onsuccess = () => { + + t.true( + is_transaction_active(t, tx, "store"), + "Transaction should be active during success handler", + ); + + let saw_handler_promise = false; + Promise.resolve().then(() => { + saw_handler_promise = true; + t.true( + is_transaction_active(t, tx, "store"), + "Transaction should be active in handler's microtasks", + ); + }); + + setTimeout(() => { + t.true(saw_handler_promise); + t.false( + is_transaction_active(t, tx, "store"), + "Transaction should be inactive in next task", + ); + release_tx(); + done(); + }, 0); + }; + }, + ); +}); 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 index 77c4a9391..a3aead9db 100644 --- 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 @@ -40,7 +40,6 @@ async function t2(t: ExecutionContext, method: string): Promise { const store = db.createObjectStore("s"); }, (done, db) => { - (db as any)._debugName = method; const tx = db.transaction("s", "readonly"); const store = tx.objectStore("s"); diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbtransaction-oncomplete.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbtransaction-oncomplete.test.ts new file mode 100644 index 000000000..8e0b43877 --- /dev/null +++ b/packages/idb-bridge/src/idb-wpt-ported/idbtransaction-oncomplete.test.ts @@ -0,0 +1,49 @@ +import test from "ava"; +import { createdb } from "./wptsupport"; + +// IDBTransaction - complete event +test("WPT idbtransaction-oncomplete.htm", async (t) => { + await new Promise((resolve, reject) => { + var db: any; + var store: any; + let open_rq = createdb(t); + let stages: any[] = []; + + open_rq.onupgradeneeded = function (e: any) { + stages.push("upgradeneeded"); + + db = e.target.result; + store = db.createObjectStore("store"); + + e.target.transaction.oncomplete = function () { + stages.push("complete"); + }; + }; + + open_rq.onsuccess = function (e) { + stages.push("success"); + + // Making a totally new transaction to check + db + .transaction("store") + .objectStore("store") + .count().onsuccess = function (e: any) { + t.deepEqual(stages, ["upgradeneeded", "complete", "success"]); + resolve(); + }; + // XXX: Make one with real transactions, not only open() versionchange one + + /*db.transaction.objectStore('store').openCursor().onsuccess = function(e) { + stages.push("opencursor1"); + } + store.openCursor().onsuccess = function(e) { + stages.push("opencursor2"); + } + e.target.transaction.objectStore('store').openCursor().onsuccess = function(e) { + stages.push("opencursor3"); + } + */ + }; + }); + t.pass(); +}); diff --git a/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts b/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts index 6777dc122..9ec46c765 100644 --- a/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts +++ b/packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts @@ -1,5 +1,5 @@ import test, { ExecutionContext } from "ava"; -import { BridgeIDBFactory } from ".."; +import { BridgeIDBFactory, BridgeIDBRequest } from ".."; import { IDBDatabase, IDBIndex, @@ -480,3 +480,65 @@ export function indexeddb_test( } }); } + +/** + * Keeps the passed transaction alive indefinitely (by making requests + * against the named store). Returns a function that asserts that the + * transaction has not already completed and then ends the request loop so that + * the transaction may autocommit and complete. + */ +export function keep_alive( + t: ExecutionContext, + tx: IDBTransaction, + store_name: string, +) { + let completed = false; + tx.addEventListener("complete", () => { + completed = true; + }); + + let keepSpinning = true; + let spinCount = 0; + + function spin() { + console.log("spinning"); + if (!keepSpinning) return; + const request = tx.objectStore(store_name).get(0); + (request as BridgeIDBRequest)._debugName = `req-spin-${spinCount}`; + spinCount++; + request.onsuccess = spin; + } + spin(); + + return () => { + t.log("stopping spin"); + t.false(completed, "Transaction completed while kept alive"); + keepSpinning = false; + }; +} + +// Checks to see if the passed transaction is active (by making +// requests against the named store). +export function is_transaction_active( + t: ExecutionContext, + tx: IDBTransaction, + store_name: string, +) { + try { + const request = tx.objectStore(store_name).get(0); + request.onerror = (e) => { + e.preventDefault(); + e.stopPropagation(); + }; + return true; + } catch (ex) { + console.log(ex.stack); + t.deepEqual( + ex.name, + "TransactionInactiveError", + "Active check should either not throw anything, or throw " + + "TransactionInactiveError", + ); + return false; + } +} diff --git a/packages/idb-bridge/src/util/queueTask.ts b/packages/idb-bridge/src/util/queueTask.ts index 53563ffd2..297602c67 100644 --- a/packages/idb-bridge/src/util/queueTask.ts +++ b/packages/idb-bridge/src/util/queueTask.ts @@ -15,7 +15,20 @@ */ export function queueTask(fn: () => void) { - setImmediate(fn); + let called = false; + const callFirst = () => { + if (called) { + return; + } + called = true; + fn(); + }; + // We must schedule both of these, + // since on node, there is no guarantee + // that a setImmediate function that is registered + // before a setTimeout function is called first. + setImmediate(callFirst); + setTimeout(callFirst, 0); } export default queueTask; -- cgit v1.2.3