diff options
author | Florian Dold <florian.dold@gmail.com> | 2019-06-15 22:44:54 +0200 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2019-06-15 22:44:54 +0200 |
commit | 2ee9431f1ba5bf67546bbf85758a01991c40673f (patch) | |
tree | 4581c4f3c966d742c66ea7f4bae4f9a3f8e2f5ff /packages/idb-bridge/src/BridgeIDBTransaction.ts | |
parent | 65eb8b96f894491d406f91070df53ccbd43d19c9 (diff) | |
download | wallet-core-2ee9431f1ba5bf67546bbf85758a01991c40673f.tar.xz |
idb wip
Diffstat (limited to 'packages/idb-bridge/src/BridgeIDBTransaction.ts')
-rw-r--r-- | packages/idb-bridge/src/BridgeIDBTransaction.ts | 301 |
1 files changed, 301 insertions, 0 deletions
diff --git a/packages/idb-bridge/src/BridgeIDBTransaction.ts b/packages/idb-bridge/src/BridgeIDBTransaction.ts new file mode 100644 index 000000000..a7057e297 --- /dev/null +++ b/packages/idb-bridge/src/BridgeIDBTransaction.ts @@ -0,0 +1,301 @@ +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"; + +// 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; + } + + public async _start() { + 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 defaultAction; + let event; + try { + const result = await operation(); + 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, + }); + } catch (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, + }); + + defaultAction = this._abort.bind(this, err.name); + } + + try { + event.eventPath = [this.db, this]; + request.dispatchEvent(event); + } catch (err) { + if (this._state !== "committing") { + this._abort("AbortError"); + } + throw err; + } + + // Default action of event + if (!event.canceled) { + if (defaultAction) { + defaultAction(); + } + } + } + + // 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; + } + + // Check if transaction complete event needs to be fired + if (this._state !== "finished") { + // Either aborted or committed already + this._state = "finished"; + + if (!this.error) { + const event = new FakeEvent("complete"); + 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"; + } + + public toString() { + return "[object IDBRequest]"; + } + + _waitDone(): Promise<void> { + return this._waitPromise; + } +} + +export default BridgeIDBTransaction; |