more tests, fix event ordering issue

This commit is contained in:
Florian Dold 2021-02-22 14:27:54 +01:00
parent e6946694f2
commit 3eced74a88
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
6 changed files with 224 additions and 6 deletions

View File

@ -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];

View File

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

View File

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

View File

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

View File

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

View File

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