wallet-core/packages/idb-bridge/src/BridgeIDBTransaction.ts

333 lines
9.2 KiB
TypeScript

import BridgeIDBDatabase from "./BridgeIDBDatabase";
import BridgeIDBObjectStore from "./BridgeIDBObjectStore";
import BridgeIDBRequest from "./BridgeIDBRequest";
import {
AbortError,
InvalidStateError,
NotFoundError,
TransactionInactiveError,
} from "./util/errors";
import fakeDOMStringList from "./util/fakeDOMStringList";
import FakeEvent from "./util/FakeEvent";
import FakeEventTarget from "./util/FakeEventTarget";
import {
EventCallback,
FakeDOMStringList,
RequestObj,
TransactionMode,
} from "./util/types";
import queueTask from "./util/queueTask";
import openPromise from "./util/openPromise";
import { DatabaseTransaction, Backend } from "./backend-interface";
import { array } from "prop-types";
import BridgeIDBFactory from "./BridgeIDBFactory";
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#transaction
class BridgeIDBTransaction extends FakeEventTarget {
public _state: "active" | "inactive" | "committing" | "finished" = "active";
public _started = false;
public _objectStoresCache: Map<string, BridgeIDBObjectStore> = new Map();
public _backendTransaction?: DatabaseTransaction;
public objectStoreNames: FakeDOMStringList;
public mode: TransactionMode;
public db: BridgeIDBDatabase;
public error: Error | null = null;
public onabort: EventCallback | null = null;
public oncomplete: EventCallback | null = null;
public onerror: EventCallback | null = null;
private _waitPromise: Promise<void>;
private _resolveWait: () => void;
public _scope: Set<string>;
private _requests: Array<{
operation: () => void;
request: BridgeIDBRequest;
}> = [];
get _backend(): Backend {
return this.db._backend;
}
constructor(
storeNames: string[],
mode: TransactionMode,
db: BridgeIDBDatabase,
backendTransaction?: DatabaseTransaction,
) {
super();
const myOpenPromise = openPromise<void>();
this._waitPromise = myOpenPromise.promise;
this._resolveWait = myOpenPromise.resolve;
this._scope = new Set(storeNames);
this._backendTransaction = backendTransaction;
this.mode = mode;
this.db = db;
this.objectStoreNames = fakeDOMStringList(Array.from(this._scope).sort());
this.db._transactions.push(this);
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-aborting-a-transaction
async _abort(errName: string | null) {
this._state = "finished";
if (errName !== null) {
const e = new Error();
e.name = errName;
this.error = e;
}
// Should this directly remove from _requests?
for (const { request } of this._requests) {
if (request.readyState !== "done") {
request.readyState = "done"; // This will cancel execution of this request's operation
if (request.source) {
request.result = undefined;
request.error = new AbortError();
const event = new FakeEvent("error", {
bubbles: true,
cancelable: true,
});
event.eventPath = [this.db, this];
request.dispatchEvent(event);
}
}
}
// Only roll back if we actually executed the scheduled operations.
const maybeBtx = this._backendTransaction;
if (maybeBtx) {
await this._backend.rollback(maybeBtx);
}
queueTask(() => {
const event = new FakeEvent("abort", {
bubbles: true,
cancelable: false,
});
event.eventPath = [this.db];
this.dispatchEvent(event);
});
}
public abort() {
if (this._state === "committing" || this._state === "finished") {
throw new InvalidStateError();
}
this._state = "active";
this._abort(null);
}
// http://w3c.github.io/IndexedDB/#dom-idbtransaction-objectstore
public objectStore(name: string) {
if (this._state !== "active") {
throw new InvalidStateError();
}
const objectStore = this._objectStoresCache.get(name);
if (objectStore !== undefined) {
return objectStore;
}
return new BridgeIDBObjectStore(this, name);
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-asynchronously-executing-a-request
public _execRequestAsync(obj: RequestObj) {
const source = obj.source;
const operation = obj.operation;
let request = obj.hasOwnProperty("request") ? obj.request : null;
if (this._state !== "active") {
throw new TransactionInactiveError();
}
// Request should only be passed for cursors
if (!request) {
if (!source) {
// Special requests like indexes that just need to run some code
request = new BridgeIDBRequest();
} else {
request = new BridgeIDBRequest();
request.source = source;
request.transaction = (source as any).transaction;
}
}
this._requests.push({
operation,
request,
});
return request;
}
/**
* Actually execute the scheduled work for this transaction.
*/
public async _start() {
if (BridgeIDBFactory.enableTracing) {
console.log(
`TRACE: IDBTransaction._start, ${this._requests.length} queued`,
);
}
this._started = true;
if (!this._backendTransaction) {
this._backendTransaction = await this._backend.beginTransaction(
this.db._backendConnection,
Array.from(this._scope),
this.mode,
);
}
// Remove from request queue - cursor ones will be added back if necessary by cursor.continue and such
let operation;
let request;
while (this._requests.length > 0) {
const r = this._requests.shift();
// This should only be false if transaction was aborted
if (r && r.request.readyState !== "done") {
request = r.request;
operation = r.operation;
break;
}
}
if (request && operation) {
if (!request.source) {
// Special requests like indexes that just need to run some code, with error handling already built into
// operation
await operation();
} else {
let event;
try {
BridgeIDBFactory.enableTracing &&
console.log("TRACE: running operation in transaction");
const result = await operation();
BridgeIDBFactory.enableTracing &&
console.log(
"TRACE: operation in transaction finished with success",
);
request.readyState = "done";
request.result = result;
request.error = undefined;
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-fire-a-success-event
if (this._state === "inactive") {
this._state = "active";
}
event = new FakeEvent("success", {
bubbles: false,
cancelable: false,
});
try {
event.eventPath = [request, this, this.db];
request.dispatchEvent(event);
} catch (err) {
if (this._state !== "committing") {
this._abort("AbortError");
}
throw err;
}
} catch (err) {
if (BridgeIDBFactory.enableTracing) {
console.log("TRACING: error during operation: ", err);
}
request.readyState = "done";
request.result = undefined;
request.error = err;
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-fire-an-error-event
if (this._state === "inactive") {
this._state = "active";
}
event = new FakeEvent("error", {
bubbles: true,
cancelable: true,
});
try {
event.eventPath = [this.db, this];
request.dispatchEvent(event);
} catch (err) {
if (this._state !== "committing") {
this._abort("AbortError");
}
throw err;
}
if (!event.canceled) {
this._abort(err.name);
}
}
}
// On to the next one
if (this._requests.length > 0) {
this._start();
} else {
// Give it another chance for new handlers to be set before finishing
queueTask(() => this._start());
}
return;
}
if (this._state !== "finished" && this._state !== "committing") {
if (BridgeIDBFactory.enableTracing) {
console.log("finishing transaction");
}
this._state = "committing";
await this._backend.commit(this._backendTransaction);
this._state = "finished";
if (!this.error) {
if (BridgeIDBFactory.enableTracing) {
console.log("dispatching 'complete' event on transaction");
}
const event = new FakeEvent("complete");
event.eventPath = [this, this.db];
this.dispatchEvent(event);
}
const idx = this.db._transactions.indexOf(this);
if (idx < 0) {
throw Error("invariant failed");
}
this.db._transactions.splice(idx, 1);
this._resolveWait();
}
}
public commit() {
if (this._state !== "active") {
throw new InvalidStateError();
}
this._state = "committing";
// We now just wait for auto-commit ...
}
public toString() {
return "[object IDBRequest]";
}
_waitDone(): Promise<void> {
return this._waitPromise;
}
}
export default BridgeIDBTransaction;