synchronous schema rollback

This commit is contained in:
Florian Dold 2021-02-16 14:49:38 +01:00
parent 987f22de02
commit 4d663d2e59
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
5 changed files with 153 additions and 65 deletions

View File

@ -579,9 +579,33 @@ export class MemoryBackend implements Backend {
if (!db) { if (!db) {
throw Error("db not found"); 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; 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( renameIndex(
btx: DatabaseTransaction, btx: DatabaseTransaction,
objectStoreName: string, objectStoreName: string,

View File

@ -162,6 +162,10 @@ export interface Backend {
getSchema(db: DatabaseConnection): Schema; getSchema(db: DatabaseConnection): Schema;
getCurrentTransactionSchema(btx: DatabaseTransaction): Schema;
getInitialTransactionSchema(btx: DatabaseTransaction): Schema;
renameIndex( renameIndex(
btx: DatabaseTransaction, btx: DatabaseTransaction,
objectStoreName: string, objectStoreName: string,

View File

@ -221,7 +221,7 @@ export class BridgeIDBCursor implements IDBCursor {
resultLevel: this._keyOnly ? ResultLevel.OnlyKeys : ResultLevel.Full, 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); let response = await this._backend.getRecords(btx, recordGetRequest);
@ -305,7 +305,7 @@ export class BridgeIDBCursor implements IDBCursor {
if (BridgeIDBFactory.enableTracing) { if (BridgeIDBFactory.enableTracing) {
console.log("updating at cursor"); console.log("updating at cursor");
} }
const { btx } = this.source._confirmActiveTransaction(); const { btx } = this.source._confirmStartedBackendTransaction();
await this._backend.storeRecord(btx, storeReq); await this._backend.storeRecord(btx, storeReq);
}; };
return transaction._execRequestAsync({ return transaction._execRequestAsync({
@ -412,7 +412,7 @@ export class BridgeIDBCursor implements IDBCursor {
} }
const operation = async () => { const operation = async () => {
const { btx } = this.source._confirmActiveTransaction(); const { btx } = this.source._confirmStartedBackendTransaction();
this._backend.deleteRecord( this._backend.deleteRecord(
btx, btx,
this._objectStoreName, 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) { constructor(backend: Backend, backendConnection: DatabaseConnection) {
super(); super();
@ -596,7 +589,7 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase {
autoIncrement, autoIncrement,
); );
this._schema = this._backend.getSchema(this._backendConnection); this._schema = this._backend.getCurrentTransactionSchema(backendTx);
return transaction.objectStore(name); return transaction.objectStore(name);
} }
@ -616,7 +609,6 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase {
os._deleted = true; os._deleted = true;
transaction._objectStoresCache.delete(name); transaction._objectStoresCache.delete(name);
} }
} }
public _internalTransaction( public _internalTransaction(
@ -835,6 +827,9 @@ export class BridgeIDBFactory {
requestedVersion, requestedVersion,
); );
// We need to expose the new version number to the upgrade transaction.
db._schema = this.backend.getCurrentTransactionSchema(backendTransaction);
const transaction = db._internalTransaction( const transaction = db._internalTransaction(
[], [],
"versionchange", "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 // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#idl-def-IDBIndex
/** @public */ /** @public */
export class BridgeIDBIndex implements IDBIndex { export class BridgeIDBIndex implements IDBIndex {
@ -957,8 +938,12 @@ export class BridgeIDBIndex implements IDBIndex {
return this._objectStore._backend; return this._objectStore._backend;
} }
_confirmActiveTransaction(): { btx: DatabaseTransaction } { _confirmStartedBackendTransaction(): { btx: DatabaseTransaction } {
return this._objectStore._confirmActiveTransaction(); return this._objectStore._confirmStartedBackendTransaction();
}
_confirmActiveTransaction(): void {
this._objectStore._confirmActiveTransaction();
} }
private _name: string; private _name: string;
@ -986,7 +971,7 @@ export class BridgeIDBIndex implements IDBIndex {
throw new TransactionInactiveError(); throw new TransactionInactiveError();
} }
const { btx } = this._confirmActiveTransaction(); const { btx } = this._confirmStartedBackendTransaction();
const oldName = this._name; const oldName = this._name;
const newName = String(name); const newName = String(name);
@ -1008,7 +993,7 @@ export class BridgeIDBIndex implements IDBIndex {
range?: BridgeIDBKeyRange | IDBValidKey | null | undefined, range?: BridgeIDBKeyRange | IDBValidKey | null | undefined,
direction: IDBCursorDirection = "next", direction: IDBCursorDirection = "next",
) { ) {
confirmActiveTransaction(this); this._confirmActiveTransaction();
if (range === null) { if (range === null) {
range = undefined; range = undefined;
@ -1047,7 +1032,7 @@ export class BridgeIDBIndex implements IDBIndex {
range?: BridgeIDBKeyRange | IDBValidKey | null | undefined, range?: BridgeIDBKeyRange | IDBValidKey | null | undefined,
direction: IDBCursorDirection = "next", direction: IDBCursorDirection = "next",
) { ) {
confirmActiveTransaction(this); this._confirmActiveTransaction();
if (range === null) { if (range === null) {
range = undefined; range = undefined;
@ -1077,7 +1062,6 @@ export class BridgeIDBIndex implements IDBIndex {
}); });
} }
private _confirmIndexExists() { private _confirmIndexExists() {
const storeSchema = this._schema.objectStores[this._objectStore._name]; const storeSchema = this._schema.objectStores[this._objectStore._name];
if (!storeSchema) { if (!storeSchema) {
@ -1089,8 +1073,8 @@ export class BridgeIDBIndex implements IDBIndex {
} }
get(key: BridgeIDBKeyRange | IDBValidKey) { get(key: BridgeIDBKeyRange | IDBValidKey) {
confirmActiveTransaction(this);
this._confirmIndexExists(); this._confirmIndexExists();
this._confirmActiveTransaction();
if (this._deleted) { if (this._deleted) {
throw new InvalidStateError(); throw new InvalidStateError();
} }
@ -1109,7 +1093,7 @@ export class BridgeIDBIndex implements IDBIndex {
}; };
const operation = async () => { const operation = async () => {
const { btx } = this._confirmActiveTransaction(); const { btx } = this._confirmStartedBackendTransaction();
const result = await this._backend.getRecords(btx, getReq); const result = await this._backend.getRecords(btx, getReq);
if (result.count == 0) { if (result.count == 0) {
return undefined; 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 // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-getKey-IDBRequest-any-key
public getKey(key: BridgeIDBKeyRange | IDBValidKey) { public getKey(key: BridgeIDBKeyRange | IDBValidKey) {
confirmActiveTransaction(this); this._confirmActiveTransaction();
if (!(key instanceof BridgeIDBKeyRange)) { if (!(key instanceof BridgeIDBKeyRange)) {
key = BridgeIDBKeyRange._valueToKeyRange(key); key = BridgeIDBKeyRange._valueToKeyRange(key);
@ -1153,7 +1137,7 @@ export class BridgeIDBIndex implements IDBIndex {
}; };
const operation = async () => { const operation = async () => {
const { btx } = this._confirmActiveTransaction(); const { btx } = this._confirmStartedBackendTransaction();
const result = await this._backend.getRecords(btx, getReq); const result = await this._backend.getRecords(btx, getReq);
if (result.count == 0) { if (result.count == 0) {
return undefined; 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 // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-count-IDBRequest-any-key
public count(key: BridgeIDBKeyRange | IDBValidKey | null | undefined) { public count(key: BridgeIDBKeyRange | IDBValidKey | null | undefined) {
confirmActiveTransaction(this); this._confirmActiveTransaction();
if (key === null) { if (key === null) {
key = undefined; key = undefined;
@ -1200,7 +1184,7 @@ export class BridgeIDBIndex implements IDBIndex {
}; };
const operation = async () => { const operation = async () => {
const { btx } = this._confirmActiveTransaction(); const { btx } = this._confirmStartedBackendTransaction();
const result = await this._backend.getRecords(btx, getReq); const result = await this._backend.getRecords(btx, getReq);
return result.count; return result.count;
}; };
@ -1380,7 +1364,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
return this._transaction._db._backendConnection; return this._transaction._db._backendConnection;
} }
_confirmActiveTransaction(): { btx: DatabaseTransaction } { _confirmStartedBackendTransaction(): { btx: DatabaseTransaction } {
const btx = this._transaction._backendTransaction; const btx = this._transaction._backendTransaction;
if (!btx) { if (!btx) {
throw new InvalidStateError(); throw new InvalidStateError();
@ -1388,6 +1372,22 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
return { btx }; 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 // http://w3c.github.io/IndexedDB/#dom-idbobjectstore-name
set name(newName: any) { set name(newName: any) {
const transaction = this._transaction; const transaction = this._transaction;
@ -1396,7 +1396,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
throw new InvalidStateError(); throw new InvalidStateError();
} }
let { btx } = this._confirmActiveTransaction(); let { btx } = this._confirmStartedBackendTransaction();
newName = String(newName); newName = String(newName);
@ -1407,12 +1407,11 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
} }
this._backend.renameObjectStore(btx, oldName, newName); this._backend.renameObjectStore(btx, oldName, newName);
this._transaction._db._schema = this._backend.getSchema( this._transaction._db._schema = this._backend.getCurrentTransactionSchema(
this._backendConnection, btx,
); );
} }
public _store(value: any, key: IDBValidKey | undefined, overwrite: boolean) { public _store(value: any, key: IDBValidKey | undefined, overwrite: boolean) {
if (BridgeIDBFactory.enableTracing) { if (BridgeIDBFactory.enableTracing) {
console.log(`TRACE: IDBObjectStore._store`); console.log(`TRACE: IDBObjectStore._store`);
@ -1428,16 +1427,10 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
} }
// We only call this to synchronously verify the request. // We only call this to synchronously verify the request.
makeStoreKeyValue( makeStoreKeyValue(value, key, 1, autoIncrement, keyPath);
value,
key,
1,
autoIncrement,
keyPath,
);
const operation = async () => { const operation = async () => {
const { btx } = this._confirmActiveTransaction(); const { btx } = this._confirmStartedBackendTransaction();
const result = await this._backend.storeRecord(btx, { const result = await this._backend.storeRecord(btx, {
objectStoreName: this._name, objectStoreName: this._name,
key: key, key: key,
@ -1457,7 +1450,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
throw new TypeError(); throw new TypeError();
} }
if (this._deleted) { 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); return this._store(value, key, true);
} }
@ -1477,7 +1472,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
throw new TypeError(); throw new TypeError();
} }
if (this._deleted) { 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") { if (this._transaction.mode === "readonly") {
throw new ReadOnlyError(); throw new ReadOnlyError();
@ -1492,7 +1489,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
} }
const operation = async () => { const operation = async () => {
const { btx } = this._confirmActiveTransaction(); const { btx } = this._confirmStartedBackendTransaction();
return this._backend.deleteRecord(btx, this._name, keyRange); return this._backend.deleteRecord(btx, this._name, keyRange);
}; };
@ -1512,7 +1509,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
} }
if (this._deleted) { 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; let keyRange: BridgeIDBKeyRange;
@ -1544,7 +1543,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
if (BridgeIDBFactory.enableTracing) { if (BridgeIDBFactory.enableTracing) {
console.log("running get operation:", recordRequest); console.log("running get operation:", recordRequest);
} }
const { btx } = this._confirmActiveTransaction(); const { btx } = this._confirmStartedBackendTransaction();
const result = await this._backend.getRecords(btx, recordRequest); const result = await this._backend.getRecords(btx, recordRequest);
if (BridgeIDBFactory.enableTracing) { if (BridgeIDBFactory.enableTracing) {
@ -1599,7 +1598,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
direction: IDBCursorDirection = "next", direction: IDBCursorDirection = "next",
) { ) {
if (this._deleted) { 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) { if (range === null) {
range = undefined; range = undefined;
@ -1633,7 +1634,9 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
direction?: IDBCursorDirection, direction?: IDBCursorDirection,
) { ) {
if (this._deleted) { 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) { if (range === null) {
range = undefined; range = undefined;
@ -1682,7 +1685,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
throw new InvalidStateError(); throw new InvalidStateError();
} }
const { btx } = this._confirmActiveTransaction(); const { btx } = this._confirmStartedBackendTransaction();
const multiEntry = const multiEntry =
optionalParameters.multiEntry !== undefined optionalParameters.multiEntry !== undefined
@ -1750,7 +1753,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
throw new InvalidStateError(); throw new InvalidStateError();
} }
const { btx } = this._confirmActiveTransaction(); const { btx } = this._confirmStartedBackendTransaction();
const index = this._indexesCache.get(indexName); const index = this._indexesCache.get(indexName);
if (index !== undefined) { if (index !== undefined) {
@ -1781,7 +1784,7 @@ export class BridgeIDBObjectStore implements IDBObjectStore {
}; };
const operation = async () => { const operation = async () => {
const { btx } = this._confirmActiveTransaction(); const { btx } = this._confirmStartedBackendTransaction();
const result = await this._backend.getRecords(btx, recordGetRequest); const result = await this._backend.getRecords(btx, recordGetRequest);
return result.count; return result.count;
}; };
@ -2015,14 +2018,15 @@ export class BridgeIDBTransaction
this._db._upgradeTransaction = null; this._db._upgradeTransaction = null;
} }
// Only roll back if we actually executed the scheduled operations.
const maybeBtx = this._backendTransaction; const maybeBtx = this._backendTransaction;
if (maybeBtx) { if (maybeBtx) {
this._db._schema = this._backend.getInitialTransactionSchema(maybeBtx);
// Only roll back if we actually executed the scheduled operations.
await this._backend.rollback(maybeBtx); await this._backend.rollback(maybeBtx);
} else {
this._db._schema = this._backend.getSchema(this._db._backendConnection);
} }
this._db._refreshSchema();
queueTask(() => { queueTask(() => {
const event = new FakeEvent("abort", { const event = new FakeEvent("abort", {
bubbles: true, bubbles: true,

View File

@ -9,7 +9,7 @@ test("WPT test abort-in-initial-upgradeneeded.htm", async (t) => {
open_rq.onupgradeneeded = function (e) { open_rq.onupgradeneeded = function (e) {
const tgt = e.target as any; const tgt = e.target as any;
db = tgt.result; db = tgt.result;
t.assert(db.version === 2); t.deepEqual(db.version, 2);
var transaction = tgt.transaction; var transaction = tgt.transaction;
transaction.oncomplete = () => t.fail("unexpected transaction.complete"); transaction.oncomplete = () => t.fail("unexpected transaction.complete");
transaction.onabort = function (e: any) { transaction.onabort = function (e: any) {

View File

@ -175,3 +175,59 @@ test("WPT idbindex_get6.htm", async (t) => {
}); });
t.pass(); t.pass();
}); });
// IDBIndex.get() - throw TransactionInactiveError on aborted transaction
test("WPT idbindex_get7.htm", async (t) => {
await new Promise<void>((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<void>((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();
});