diff --git a/packages/idb-bridge/src/MemoryBackend.ts b/packages/idb-bridge/src/MemoryBackend.ts index 4fdcf257c..0051005ed 100644 --- a/packages/idb-bridge/src/MemoryBackend.ts +++ b/packages/idb-bridge/src/MemoryBackend.ts @@ -46,11 +46,10 @@ type Key = IDBValidKey; type Value = unknown; enum TransactionLevel { - Disconnected = 0, - Connected = 1, - Read = 2, - Write = 3, - VersionChange = 4, + None = 0, + Read = 1, + Write = 2, + VersionChange = 3, } interface ObjectStore { @@ -83,12 +82,18 @@ interface Database { txLevel: TransactionLevel; + txOwnerConnectionCookie?: string; + txOwnerTransactionCookie?: string; + /** * Object stores that the transaction is allowed to access. */ txRestrictObjectStores: string[] | undefined; - connectionCookie: string | undefined; + /** + * Connection cookies of current connections. + */ + connectionCookies: string[]; } /** @public */ @@ -245,7 +250,7 @@ export class MemoryBackend implements Backend { private disconnectCond: AsyncCondition = new AsyncCondition(); /** - * Conditation that is triggered whenever a transaction finishes. + * Condition that is triggered whenever a transaction finishes. */ private transactionDoneCond: AsyncCondition = new AsyncCondition(); @@ -327,8 +332,8 @@ export class MemoryBackend implements Backend { deleted: false, committedObjectStores: objectStores, committedSchema: structuredClone(schema), - connectionCookie: undefined, - txLevel: TransactionLevel.Disconnected, + connectionCookies: [], + txLevel: TransactionLevel.None, txRestrictObjectStores: undefined, }; this.databases[dbName] = db; @@ -425,9 +430,9 @@ export class MemoryBackend implements Backend { if (myDb.txLevel < TransactionLevel.VersionChange) { throw new InvalidStateError(); } - if (myDb.connectionCookie !== tx.transactionCookie) { - throw new InvalidAccessError(); - } + // if (myDb.connectionCookie !== tx.transactionCookie) { + // throw new InvalidAccessError(); + // } myDb.deleted = true; } @@ -449,20 +454,18 @@ export class MemoryBackend implements Backend { committedSchema: schema, deleted: false, committedObjectStores: {}, - txLevel: TransactionLevel.Disconnected, - connectionCookie: undefined, + txLevel: TransactionLevel.None, + connectionCookies: [], txRestrictObjectStores: undefined, }; this.databases[name] = database; } - while (database.txLevel !== TransactionLevel.Disconnected) { - await this.disconnectCond.wait(); + if (database.connectionCookies.includes(connectionCookie)) { + throw Error("already connected"); } - database.txLevel = TransactionLevel.Connected; - database.txRestrictObjectStores = undefined; - database.connectionCookie = connectionCookie; + database.connectionCookies.push(connectionCookie); const myConn: Connection = { dbName: name, @@ -494,7 +497,7 @@ export class MemoryBackend implements Backend { throw Error("db not found"); } - while (myDb.txLevel !== TransactionLevel.Connected) { + while (myDb.txLevel !== TransactionLevel.None) { if (this.enableTracing) { console.log(`TRACING: beginTransaction -- waiting for others to close`); } @@ -533,11 +536,13 @@ export class MemoryBackend implements Backend { throw Error("db not found"); } - while (myDb.txLevel !== TransactionLevel.Connected) { + while (myDb.txLevel !== TransactionLevel.None) { await this.transactionDoneCond.wait(); } myDb.txLevel = TransactionLevel.VersionChange; + myDb.txOwnerConnectionCookie = conn.connectionCookie; + myDb.txOwnerTransactionCookie = transactionCookie; myDb.txRestrictObjectStores = undefined; this.connectionsByTransaction[transactionCookie] = myConn; @@ -557,11 +562,13 @@ export class MemoryBackend implements Backend { } if (!myConn.deleted) { const myDb = this.databases[myConn.dbName]; - if (myDb.txLevel != TransactionLevel.Connected) { - throw Error("invalid state"); - } - myDb.txLevel = TransactionLevel.Disconnected; - myDb.txRestrictObjectStores = undefined; + // 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(); @@ -1390,7 +1397,7 @@ export class MemoryBackend implements Backend { throw Error("db not found"); } if (db.txLevel < TransactionLevel.Write) { - throw Error("only allowed while running a transaction"); + throw Error("store operation only allowed while running a transaction"); } if ( db.txRestrictObjectStores && @@ -1588,9 +1595,9 @@ export class MemoryBackend implements Backend { throw Error("db not found"); } if (db.txLevel < TransactionLevel.Read) { - throw Error("only allowed while running a transaction"); + throw Error("rollback is only allowed while running a transaction"); } - db.txLevel = TransactionLevel.Connected; + db.txLevel = TransactionLevel.None; db.txRestrictObjectStores = undefined; myConn.modifiedSchema = structuredClone(db.committedSchema); myConn.objectStoreMap = this.makeObjectStoreMap(db); @@ -1633,7 +1640,7 @@ export class MemoryBackend implements Backend { } db.committedSchema = structuredClone(myConn.modifiedSchema); - db.txLevel = TransactionLevel.Connected; + db.txLevel = TransactionLevel.None; db.txRestrictObjectStores = undefined; db.committedObjectStores = {}; diff --git a/packages/idb-bridge/src/bridge-idb.ts b/packages/idb-bridge/src/bridge-idb.ts index e23c78d4a..6ca6633a9 100644 --- a/packages/idb-bridge/src/bridge-idb.ts +++ b/packages/idb-bridge/src/bridge-idb.ts @@ -312,7 +312,43 @@ 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) { - throw Error("not implemented"); + const transaction = this._effectiveObjectStore._transaction; + + if (!transaction._active) { + throw new TransactionInactiveError(); + } + + if (this._effectiveObjectStore._deleted) { + throw new InvalidStateError(); + } + if ( + !(this.source instanceof BridgeIDBObjectStore) && + this.source._deleted + ) { + throw new InvalidStateError(); + } + + if (!this._gotValue) { + throw new InvalidStateError(); + } + + if (this._request) { + this._request.readyState = "pending"; + } + + const operation = async () => { + for (let i = 0; i < count; i++) { + await this._iterate(); + } + }; + + transaction._execRequestAsync({ + operation, + request: this._request, + source: this.source, + }); + + this._gotValue = false; } /** @@ -760,8 +796,23 @@ export class BridgeIDBFactory { queueTask(async () => { let dbconn: DatabaseConnection; try { + if (BridgeIDBFactory.enableTracing) { + console.log( + "TRACE: connecting to database", + ); + } dbconn = await this.backend.connectDatabase(name); + if (BridgeIDBFactory.enableTracing) { + console.log( + "TRACE: connected!", + ); + } } catch (err) { + if (BridgeIDBFactory.enableTracing) { + console.log( + "TRACE: caught exception while trying to connect with backend", + ); + } request._finishWithError(err); return; } @@ -796,11 +847,24 @@ export class BridgeIDBFactory { cancelable: false, }); event2.eventPath = []; + if (BridgeIDBFactory.enableTracing) { + console.log( + "open() requested same version, dispatching 'success' event on transaction", + ); + } request.dispatchEvent(event2); } else if (existingVersion < requestedVersion) { // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-running-a-versionchange-transaction for (const otherConn of this.connections) { + if (otherConn._closePending) { + continue; + } + if (BridgeIDBFactory.enableTracing) { + console.log( + "dispatching 'versionchange' event to other connection", + ); + } const event = new BridgeIDBVersionChangeEvent("versionchange", { newVersion: version, oldVersion: existingVersion, @@ -809,6 +873,11 @@ export class BridgeIDBFactory { } if (this._anyOpen()) { + if (BridgeIDBFactory.enableTracing) { + console.log( + "other connections are still open, dispatching 'blocked' event to other connection", + ); + } const event = new BridgeIDBVersionChangeEvent("blocked", { newVersion: version, oldVersion: existingVersion, @@ -835,6 +904,10 @@ export class BridgeIDBFactory { db._upgradeTransaction = transaction; + if (BridgeIDBFactory.enableTracing) { + console.log("dispatching upgradeneeded event"); + } + const event = new BridgeIDBVersionChangeEvent("upgradeneeded", { newVersion: version, oldVersion: existingVersion, @@ -866,6 +939,10 @@ export class BridgeIDBFactory { event2.eventPath = []; request.dispatchEvent(event2); } else { + if (BridgeIDBFactory.enableTracing) { + console.log("dispatching 'success' event for opening db"); + } + const event2 = new FakeEvent("success", { bubbles: false, cancelable: false, @@ -1801,10 +1878,10 @@ export class BridgeIDBRequest extends FakeEventTarget implements IDBRequest { _result: any = null; _error: Error | null | undefined = null; get source(): IDBObjectStore | IDBIndex | IDBCursor { - if (this._source) { - return this._source; + if (!this._source) { + throw Error("internal invariant failed: source is null"); } - throw Error("source is null"); + return this._source; } _source: | BridgeIDBCursor @@ -1875,6 +1952,16 @@ export class BridgeIDBOpenDBRequest public onupgradeneeded: EventListener | null = null; public onblocked: EventListener | null = null; + get source(): IDBObjectStore | IDBIndex | IDBCursor { + // This is a type safety violation, but it is required by the + // IndexedDB standard. + // On the one hand, IDBOpenDBRequest implements IDBRequest. + // But that's technically impossible, as the "source" of the + // IDBOpenDB request may be null, while the one from IDBRequest + // may not be null. + return this._source as any; + } + constructor() { super(); // https://www.w3.org/TR/IndexedDB/#open-requests diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbfactory-open.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbfactory-open.test.ts index 4ba7caa6f..0d1f24c4b 100644 --- a/packages/idb-bridge/src/idb-wpt-ported/idbfactory-open.test.ts +++ b/packages/idb-bridge/src/idb-wpt-ported/idbfactory-open.test.ts @@ -20,7 +20,7 @@ test("WPT idbfactory-open.htm", async (t) => { // IDBFactory.open() - database 'name' and 'version' are correctly set test("WPT idbfactory-open2.htm", async (t) => { await new Promise((resolve, reject) => { - var database_name = __filename + "-database_name"; + var database_name = t.title + "-database_name"; var open_rq = createdb(t, database_name, 13); open_rq.onupgradeneeded = function (e) {}; @@ -28,7 +28,7 @@ test("WPT idbfactory-open2.htm", async (t) => { var db = e.target.result; t.deepEqual(db.name, database_name, "db.name"); t.deepEqual(db.version, 13, "db.version"); - resolve; + resolve(); }; }); t.pass(); @@ -63,7 +63,7 @@ test("WPT idbfactory-open3.htm", async (t) => { test("WPT idbfactory-open4.htm", async (t) => { const indexedDB = idbFactory; await new Promise((resolve, reject) => { - var open_rq = createdb(t, __filename + "-database_name"); + var open_rq = createdb(t, t.title + "-database_name"); open_rq.onupgradeneeded = function (e: any) { t.deepEqual(e.target.result.version, 1, "db.version"); @@ -80,7 +80,7 @@ test("WPT idbfactory-open4.htm", async (t) => { test("WPT idbfactory-open5.htm", async (t) => { const indexedDB = idbFactory; await new Promise((resolve, reject) => { - var open_rq = createdb(t, __filename + "-database_name"); + var open_rq = createdb(t, t.title + "-database_name"); open_rq.onupgradeneeded = function () {}; open_rq.onsuccess = function (e: any) { @@ -100,7 +100,6 @@ test("WPT idbfactory-open6.htm", async (t) => { const indexedDB = idbFactory; await new Promise((resolve, reject) => { var open_rq = createdb(t, undefined, 13); - var did_upgrade = false; var open_rq2: any; open_rq.onupgradeneeded = function () {}; @@ -115,8 +114,10 @@ test("WPT idbfactory-open6.htm", async (t) => { }; function open_previous_db(e: any) { + t.log("opening previous DB"); var open_rq3 = indexedDB.open(e.target.result.name, 13); open_rq3.onerror = function (e: any) { + t.log("got open error"); t.deepEqual(e.target.error.name, "VersionError", "e.target.error.name"); open_rq2.result.close(); resolve(); @@ -506,6 +507,7 @@ test("WPT idbfactory-open12.htm", async (t) => { * Second test */ db.onversionchange = function () { + t.log("onversionchange called"); db.close(); };