From 4d663d2e595b64e6bf1979eccc701d0f3d55d797 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 16 Feb 2021 14:49:38 +0100 Subject: [PATCH] synchronous schema rollback --- packages/idb-bridge/src/MemoryBackend.ts | 24 ++++ packages/idb-bridge/src/backend-interface.ts | 4 + packages/idb-bridge/src/bridge-idb.ts | 132 +++++++++--------- .../abort-in-initial-upgradeneeded.test.ts | 2 +- .../src/idb-wpt-ported/idbindex_get.test.ts | 56 ++++++++ 5 files changed, 153 insertions(+), 65 deletions(-) diff --git a/packages/idb-bridge/src/MemoryBackend.ts b/packages/idb-bridge/src/MemoryBackend.ts index 7107756a0..4fdcf257c 100644 --- a/packages/idb-bridge/src/MemoryBackend.ts +++ b/packages/idb-bridge/src/MemoryBackend.ts @@ -579,9 +579,33 @@ export class MemoryBackend implements Backend { if (!db) { throw Error("db not found"); } + return db.committedSchema; + } + + getCurrentTransactionSchema(btx: DatabaseTransaction): Schema { + const myConn = this.connectionsByTransaction[btx.transactionCookie]; + if (!myConn) { + throw Error("unknown connection"); + } + const db = this.databases[myConn.dbName]; + if (!db) { + throw Error("db not found"); + } return myConn.modifiedSchema; } + getInitialTransactionSchema(btx: DatabaseTransaction): Schema { + const myConn = this.connectionsByTransaction[btx.transactionCookie]; + if (!myConn) { + throw Error("unknown connection"); + } + const db = this.databases[myConn.dbName]; + if (!db) { + throw Error("db not found"); + } + return db.committedSchema; + } + renameIndex( btx: DatabaseTransaction, objectStoreName: string, diff --git a/packages/idb-bridge/src/backend-interface.ts b/packages/idb-bridge/src/backend-interface.ts index 14b5da64a..7b74c35e6 100644 --- a/packages/idb-bridge/src/backend-interface.ts +++ b/packages/idb-bridge/src/backend-interface.ts @@ -162,6 +162,10 @@ export interface Backend { getSchema(db: DatabaseConnection): Schema; + getCurrentTransactionSchema(btx: DatabaseTransaction): Schema; + + getInitialTransactionSchema(btx: DatabaseTransaction): Schema; + renameIndex( btx: DatabaseTransaction, objectStoreName: string, diff --git a/packages/idb-bridge/src/bridge-idb.ts b/packages/idb-bridge/src/bridge-idb.ts index f518b4768..86ca66b1b 100644 --- a/packages/idb-bridge/src/bridge-idb.ts +++ b/packages/idb-bridge/src/bridge-idb.ts @@ -221,7 +221,7 @@ export class BridgeIDBCursor implements IDBCursor { resultLevel: this._keyOnly ? ResultLevel.OnlyKeys : ResultLevel.Full, }; - const { btx } = this.source._confirmActiveTransaction(); + const { btx } = this.source._confirmStartedBackendTransaction(); let response = await this._backend.getRecords(btx, recordGetRequest); @@ -305,7 +305,7 @@ export class BridgeIDBCursor implements IDBCursor { if (BridgeIDBFactory.enableTracing) { console.log("updating at cursor"); } - const { btx } = this.source._confirmActiveTransaction(); + const { btx } = this.source._confirmStartedBackendTransaction(); await this._backend.storeRecord(btx, storeReq); }; return transaction._execRequestAsync({ @@ -412,7 +412,7 @@ export class BridgeIDBCursor implements IDBCursor { } const operation = async () => { - const { btx } = this.source._confirmActiveTransaction(); + const { btx } = this.source._confirmStartedBackendTransaction(); this._backend.deleteRecord( btx, this._objectStoreName, @@ -535,13 +535,6 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase { } } - /** - * Refresh the schema by querying it from the backend. - */ - _refreshSchema() { - this._schema = this._backend.getSchema(this._backendConnection); - } - constructor(backend: Backend, backendConnection: DatabaseConnection) { super(); @@ -596,7 +589,7 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase { autoIncrement, ); - this._schema = this._backend.getSchema(this._backendConnection); + this._schema = this._backend.getCurrentTransactionSchema(backendTx); return transaction.objectStore(name); } @@ -616,7 +609,6 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase { os._deleted = true; transaction._objectStoresCache.delete(name); } - } public _internalTransaction( @@ -835,6 +827,9 @@ export class BridgeIDBFactory { requestedVersion, ); + // We need to expose the new version number to the upgrade transaction. + db._schema = this.backend.getCurrentTransactionSchema(backendTransaction); + const transaction = db._internalTransaction( [], "versionchange", @@ -911,20 +906,6 @@ export class BridgeIDBFactory { } } -const confirmActiveTransaction = ( - index: BridgeIDBIndex, -): BridgeIDBTransaction => { - if (index._deleted || index._objectStore._deleted) { - throw new InvalidStateError(); - } - - if (!index._objectStore._transaction._active) { - throw new TransactionInactiveError(); - } - - return index._objectStore._transaction; -}; - // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#idl-def-IDBIndex /** @public */ export class BridgeIDBIndex implements IDBIndex { @@ -957,8 +938,12 @@ export class BridgeIDBIndex implements IDBIndex { return this._objectStore._backend; } - _confirmActiveTransaction(): { btx: DatabaseTransaction } { - return this._objectStore._confirmActiveTransaction(); + _confirmStartedBackendTransaction(): { btx: DatabaseTransaction } { + return this._objectStore._confirmStartedBackendTransaction(); + } + + _confirmActiveTransaction(): void { + this._objectStore._confirmActiveTransaction(); } private _name: string; @@ -986,7 +971,7 @@ export class BridgeIDBIndex implements IDBIndex { throw new TransactionInactiveError(); } - const { btx } = this._confirmActiveTransaction(); + const { btx } = this._confirmStartedBackendTransaction(); const oldName = this._name; const newName = String(name); @@ -1008,7 +993,7 @@ export class BridgeIDBIndex implements IDBIndex { range?: BridgeIDBKeyRange | IDBValidKey | null | undefined, direction: IDBCursorDirection = "next", ) { - confirmActiveTransaction(this); + this._confirmActiveTransaction(); if (range === null) { range = undefined; @@ -1047,7 +1032,7 @@ export class BridgeIDBIndex implements IDBIndex { range?: BridgeIDBKeyRange | IDBValidKey | null | undefined, direction: IDBCursorDirection = "next", ) { - confirmActiveTransaction(this); + this._confirmActiveTransaction(); if (range === null) { range = undefined; @@ -1077,7 +1062,6 @@ export class BridgeIDBIndex implements IDBIndex { }); } - private _confirmIndexExists() { const storeSchema = this._schema.objectStores[this._objectStore._name]; if (!storeSchema) { @@ -1089,8 +1073,8 @@ export class BridgeIDBIndex implements IDBIndex { } get(key: BridgeIDBKeyRange | IDBValidKey) { - confirmActiveTransaction(this); this._confirmIndexExists(); + this._confirmActiveTransaction(); if (this._deleted) { throw new InvalidStateError(); } @@ -1109,7 +1093,7 @@ export class BridgeIDBIndex implements IDBIndex { }; const operation = async () => { - const { btx } = this._confirmActiveTransaction(); + const { btx } = this._confirmStartedBackendTransaction(); const result = await this._backend.getRecords(btx, getReq); if (result.count == 0) { return undefined; @@ -1137,7 +1121,7 @@ export class BridgeIDBIndex implements IDBIndex { // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-getKey-IDBRequest-any-key public getKey(key: BridgeIDBKeyRange | IDBValidKey) { - confirmActiveTransaction(this); + this._confirmActiveTransaction(); if (!(key instanceof BridgeIDBKeyRange)) { key = BridgeIDBKeyRange._valueToKeyRange(key); @@ -1153,7 +1137,7 @@ export class BridgeIDBIndex implements IDBIndex { }; const operation = async () => { - const { btx } = this._confirmActiveTransaction(); + const { btx } = this._confirmStartedBackendTransaction(); const result = await this._backend.getRecords(btx, getReq); if (result.count == 0) { return undefined; @@ -1181,7 +1165,7 @@ export class BridgeIDBIndex implements IDBIndex { // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-count-IDBRequest-any-key public count(key: BridgeIDBKeyRange | IDBValidKey | null | undefined) { - confirmActiveTransaction(this); + this._confirmActiveTransaction(); if (key === null) { key = undefined; @@ -1200,7 +1184,7 @@ export class BridgeIDBIndex implements IDBIndex { }; const operation = async () => { - const { btx } = this._confirmActiveTransaction(); + const { btx } = this._confirmStartedBackendTransaction(); const result = await this._backend.getRecords(btx, getReq); return result.count; }; @@ -1380,7 +1364,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { return this._transaction._db._backendConnection; } - _confirmActiveTransaction(): { btx: DatabaseTransaction } { + _confirmStartedBackendTransaction(): { btx: DatabaseTransaction } { const btx = this._transaction._backendTransaction; if (!btx) { throw new InvalidStateError(); @@ -1388,6 +1372,22 @@ export class BridgeIDBObjectStore implements IDBObjectStore { return { btx }; } + /** + * Confirm that requests can currently placed against the + * transaction of this object. + * + * Note that this is independent from the state of the backend + * connection. + */ + _confirmActiveTransaction(): void { + if (!this._transaction._active) { + throw new TransactionInactiveError(); + } + if (this._transaction._aborted) { + throw new TransactionInactiveError(); + } + } + // http://w3c.github.io/IndexedDB/#dom-idbobjectstore-name set name(newName: any) { const transaction = this._transaction; @@ -1396,7 +1396,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { throw new InvalidStateError(); } - let { btx } = this._confirmActiveTransaction(); + let { btx } = this._confirmStartedBackendTransaction(); newName = String(newName); @@ -1407,12 +1407,11 @@ export class BridgeIDBObjectStore implements IDBObjectStore { } this._backend.renameObjectStore(btx, oldName, newName); - this._transaction._db._schema = this._backend.getSchema( - this._backendConnection, + this._transaction._db._schema = this._backend.getCurrentTransactionSchema( + btx, ); } - public _store(value: any, key: IDBValidKey | undefined, overwrite: boolean) { if (BridgeIDBFactory.enableTracing) { console.log(`TRACE: IDBObjectStore._store`); @@ -1428,16 +1427,10 @@ export class BridgeIDBObjectStore implements IDBObjectStore { } // We only call this to synchronously verify the request. - makeStoreKeyValue( - value, - key, - 1, - autoIncrement, - keyPath, - ); + makeStoreKeyValue(value, key, 1, autoIncrement, keyPath); const operation = async () => { - const { btx } = this._confirmActiveTransaction(); + const { btx } = this._confirmStartedBackendTransaction(); const result = await this._backend.storeRecord(btx, { objectStoreName: this._name, key: key, @@ -1457,7 +1450,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore { throw new TypeError(); } if (this._deleted) { - throw new InvalidStateError("tried to call 'put' on a deleted object store"); + throw new InvalidStateError( + "tried to call 'put' on a deleted object store", + ); } return this._store(value, key, true); } @@ -1477,7 +1472,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore { throw new TypeError(); } if (this._deleted) { - throw new InvalidStateError("tried to call 'delete' on a deleted object store"); + throw new InvalidStateError( + "tried to call 'delete' on a deleted object store", + ); } if (this._transaction.mode === "readonly") { throw new ReadOnlyError(); @@ -1492,7 +1489,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { } const operation = async () => { - const { btx } = this._confirmActiveTransaction(); + const { btx } = this._confirmStartedBackendTransaction(); return this._backend.deleteRecord(btx, this._name, keyRange); }; @@ -1512,7 +1509,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore { } if (this._deleted) { - throw new InvalidStateError("tried to call 'delete' on a deleted object store"); + throw new InvalidStateError( + "tried to call 'delete' on a deleted object store", + ); } let keyRange: BridgeIDBKeyRange; @@ -1544,7 +1543,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { if (BridgeIDBFactory.enableTracing) { console.log("running get operation:", recordRequest); } - const { btx } = this._confirmActiveTransaction(); + const { btx } = this._confirmStartedBackendTransaction(); const result = await this._backend.getRecords(btx, recordRequest); if (BridgeIDBFactory.enableTracing) { @@ -1599,7 +1598,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore { direction: IDBCursorDirection = "next", ) { if (this._deleted) { - throw new InvalidStateError("tried to call 'openCursor' on a deleted object store"); + throw new InvalidStateError( + "tried to call 'openCursor' on a deleted object store", + ); } if (range === null) { range = undefined; @@ -1633,7 +1634,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore { direction?: IDBCursorDirection, ) { if (this._deleted) { - throw new InvalidStateError("tried to call 'openKeyCursor' on a deleted object store"); + throw new InvalidStateError( + "tried to call 'openKeyCursor' on a deleted object store", + ); } if (range === null) { range = undefined; @@ -1682,7 +1685,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { throw new InvalidStateError(); } - const { btx } = this._confirmActiveTransaction(); + const { btx } = this._confirmStartedBackendTransaction(); const multiEntry = optionalParameters.multiEntry !== undefined @@ -1750,7 +1753,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { throw new InvalidStateError(); } - const { btx } = this._confirmActiveTransaction(); + const { btx } = this._confirmStartedBackendTransaction(); const index = this._indexesCache.get(indexName); if (index !== undefined) { @@ -1781,7 +1784,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore { }; const operation = async () => { - const { btx } = this._confirmActiveTransaction(); + const { btx } = this._confirmStartedBackendTransaction(); const result = await this._backend.getRecords(btx, recordGetRequest); return result.count; }; @@ -2015,14 +2018,15 @@ export class BridgeIDBTransaction this._db._upgradeTransaction = null; } - // Only roll back if we actually executed the scheduled operations. const maybeBtx = this._backendTransaction; if (maybeBtx) { + this._db._schema = this._backend.getInitialTransactionSchema(maybeBtx); + // Only roll back if we actually executed the scheduled operations. await this._backend.rollback(maybeBtx); + } else { + this._db._schema = this._backend.getSchema(this._db._backendConnection); } - this._db._refreshSchema(); - queueTask(() => { const event = new FakeEvent("abort", { bubbles: true, diff --git a/packages/idb-bridge/src/idb-wpt-ported/abort-in-initial-upgradeneeded.test.ts b/packages/idb-bridge/src/idb-wpt-ported/abort-in-initial-upgradeneeded.test.ts index da9ed2632..70f2f2b8a 100644 --- a/packages/idb-bridge/src/idb-wpt-ported/abort-in-initial-upgradeneeded.test.ts +++ b/packages/idb-bridge/src/idb-wpt-ported/abort-in-initial-upgradeneeded.test.ts @@ -9,7 +9,7 @@ test("WPT test abort-in-initial-upgradeneeded.htm", async (t) => { open_rq.onupgradeneeded = function (e) { const tgt = e.target as any; db = tgt.result; - t.assert(db.version === 2); + t.deepEqual(db.version, 2); var transaction = tgt.transaction; transaction.oncomplete = () => t.fail("unexpected transaction.complete"); transaction.onabort = function (e: any) { diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbindex_get.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbindex_get.test.ts index 8a8cb3129..7601faada 100644 --- a/packages/idb-bridge/src/idb-wpt-ported/idbindex_get.test.ts +++ b/packages/idb-bridge/src/idb-wpt-ported/idbindex_get.test.ts @@ -175,3 +175,59 @@ test("WPT idbindex_get6.htm", async (t) => { }); t.pass(); }); + +// IDBIndex.get() - throw TransactionInactiveError on aborted transaction +test("WPT idbindex_get7.htm", async (t) => { + await new Promise((resolve, reject) => { + var db: any; + + var open_rq = createdb(t); + open_rq.onupgradeneeded = function (e: any) { + const db = e.target.result as IDBDatabase; + var store = db.createObjectStore("store", { keyPath: "key" }); + var index = store.createIndex("index", "indexedProperty"); + store.add({ key: 1, indexedProperty: "data" }); + }; + open_rq.onsuccess = function (e: any) { + const db = e.target.result as IDBDatabase; + var tx = db.transaction("store"); + var index = tx.objectStore("store").index("index"); + tx.abort(); + + t.throws( + function () { + index.get("data"); + }, + { name: "TransactionInactiveError" }, + ); + resolve(); + }; + }); + t.pass(); +}); + +// IDBIndex.get() - throw InvalidStateError on index deleted by aborted upgrade +test("WPT idbindex_get8.htm", async (t) => { + await new Promise((resolve, reject) => { + var db: any; + + var open_rq = createdb(t); + open_rq.onupgradeneeded = function (e: any) { + db = e.target.result; + var store = db.createObjectStore("store", { keyPath: "key" }); + var index = store.createIndex("index", "indexedProperty"); + store.add({ key: 1, indexedProperty: "data" }); + + e.target.transaction.abort(); + + t.throws( + function () { + index.get("data"); + }, + { name: "InvalidStateError" }, + ); + resolve(); + }; + }); + t.pass(); +});