aboutsummaryrefslogtreecommitdiff
path: root/packages/idb-bridge/src
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2021-02-22 14:27:54 +0100
committerFlorian Dold <florian@dold.me>2021-02-22 14:27:54 +0100
commit3eced74a88de43ab9afe542fcce20a8db8e3fe60 (patch)
tree8ef74cfd5cf3bea8fe90cd20746e4fadb0afc349 /packages/idb-bridge/src
parente6946694f2e7ae6ff25f490fa76f3da583c44c74 (diff)
more tests, fix event ordering issue
Diffstat (limited to 'packages/idb-bridge/src')
-rw-r--r--packages/idb-bridge/src/bridge-idb.ts44
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/event-dispatch-active-flag.test.ts57
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbobjectstore-add-put-exception-order.test.ts1
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbtransaction-oncomplete.test.ts49
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/wptsupport.ts64
-rw-r--r--packages/idb-bridge/src/util/queueTask.ts15
6 files changed, 224 insertions, 6 deletions
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<void> {
+ return new Promise<void>((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<void> {
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<void>((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;