diff options
Diffstat (limited to 'packages/idb-bridge/src/bridge-idb.ts')
-rw-r--r-- | packages/idb-bridge/src/bridge-idb.ts | 2053 |
1 files changed, 2053 insertions, 0 deletions
diff --git a/packages/idb-bridge/src/bridge-idb.ts b/packages/idb-bridge/src/bridge-idb.ts new file mode 100644 index 000000000..2bced800d --- /dev/null +++ b/packages/idb-bridge/src/bridge-idb.ts @@ -0,0 +1,2053 @@ +/* + Copyright 2017 Jeremy Scheff + Copyright 2019-2021 Taler Systems S.A. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + or implied. See the License for the specific language governing + permissions and limitations under the License. + */ + +import { + Backend, + DatabaseConnection, + DatabaseTransaction, + RecordGetRequest, + RecordStoreRequest, + ResultLevel, + Schema, + StoreLevel, +} from "./backend-interface"; +import { EventListener, IDBCursorDirection, IDBKeyPath, IDBKeyRange, IDBTransactionMode, IDBValidKey } from "./idbtypes"; +import compareKeys from "./util/cmp"; +import enforceRange from "./util/enforceRange"; +import { + AbortError, + ConstraintError, + DataError, + InvalidAccessError, + InvalidStateError, + NotFoundError, + ReadOnlyError, + TransactionInactiveError, + VersionError, +} from "./util/errors"; +import { fakeDOMStringList } from "./util/fakeDOMStringList"; +import FakeEvent from "./util/FakeEvent"; +import FakeEventTarget from "./util/FakeEventTarget"; +import openPromise from "./util/openPromise"; +import queueTask from "./util/queueTask"; +import structuredClone from "./util/structuredClone"; +import validateKeyPath from "./util/validateKeyPath"; +import valueToKey from "./util/valueToKey"; + +/** @public */ +export type CursorSource = BridgeIDBIndex | BridgeIDBObjectStore; + +/** @public */ +export interface FakeDOMStringList extends Array<string> { + contains: (value: string) => boolean; + item: (i: number) => string | undefined; +} + +/** @public */ +export interface RequestObj { + operation: () => Promise<any>; + request?: BridgeIDBRequest | undefined; + source?: any; +} + +/** @public */ +export interface BridgeIDBDatabaseInfo { + name: string; + version: number; +} + +function simplifyRange( + r: IDBValidKey | IDBKeyRange | undefined | null, +): IDBKeyRange | null { + if (r && typeof r === "object" && "lower" in r) { + return r; + } + if (r === undefined || r === null) { + return null; + } + return BridgeIDBKeyRange.bound(r, r, false, false); +} + +/** + * http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#cursor + * + * @public + */ +export class BridgeIDBCursor { + _request: BridgeIDBRequest | undefined; + + private _gotValue: boolean = false; + private _range: IDBValidKey | IDBKeyRange | undefined | null; + private _indexPosition = undefined; // Key of previously returned record + private _objectStorePosition = undefined; + private _keyOnly: boolean; + + private _source: CursorSource; + private _direction: IDBCursorDirection; + private _key: IDBValidKey | undefined = undefined; + private _primaryKey: IDBValidKey | undefined = undefined; + private _indexName: string | undefined; + private _objectStoreName: string; + + protected _value: any = undefined; + + constructor( + source: CursorSource, + objectStoreName: string, + indexName: string | undefined, + range: IDBValidKey | IDBKeyRange | null | undefined, + direction: IDBCursorDirection, + request: BridgeIDBRequest, + keyOnly: boolean, + ) { + this._indexName = indexName; + this._objectStoreName = objectStoreName; + this._range = range; + this._source = source; + this._direction = direction; + this._request = request; + this._keyOnly = keyOnly; + } + + get _effectiveObjectStore(): BridgeIDBObjectStore { + if (this.source instanceof BridgeIDBObjectStore) { + return this.source; + } + return this.source.objectStore; + } + + get _backend(): Backend { + return this._source._backend; + } + + // Read only properties + get source() { + return this._source; + } + set source(val) { + /* For babel */ + } + + get direction() { + return this._direction; + } + set direction(val) { + /* For babel */ + } + + get key() { + return this._key; + } + set key(val) { + /* For babel */ + } + + get primaryKey() { + return this._primaryKey; + } + + set primaryKey(val) { + /* For babel */ + } + + protected get _isValueCursor(): boolean { + return false; + } + + /** + * https://w3c.github.io/IndexedDB/#iterate-a-cursor + */ + async _iterate(key?: IDBValidKey, primaryKey?: IDBValidKey): Promise<any> { + BridgeIDBFactory.enableTracing && + console.log( + `iterating cursor os=${this._objectStoreName},idx=${this._indexName}`, + ); + BridgeIDBFactory.enableTracing && + console.log("cursor type ", this.toString()); + const recordGetRequest: RecordGetRequest = { + direction: this.direction, + indexName: this._indexName, + lastIndexPosition: this._indexPosition, + lastObjectStorePosition: this._objectStorePosition, + limit: 1, + range: simplifyRange(this._range), + objectStoreName: this._objectStoreName, + advanceIndexKey: key, + advancePrimaryKey: primaryKey, + resultLevel: this._keyOnly ? ResultLevel.OnlyKeys : ResultLevel.Full, + }; + + const { btx } = this.source._confirmActiveTransaction(); + + let response = await this._backend.getRecords(btx, recordGetRequest); + + if (response.count === 0) { + if (BridgeIDBFactory.enableTracing) { + console.log("cursor is returning empty result"); + } + this._gotValue = false; + return null; + } + + if (response.count !== 1) { + throw Error("invariant failed"); + } + + if (BridgeIDBFactory.enableTracing) { + console.log("request is:", JSON.stringify(recordGetRequest)); + console.log("get response is:", JSON.stringify(response)); + } + + if (this._indexName !== undefined) { + this._key = response.indexKeys![0]; + } else { + this._key = response.primaryKeys![0]; + } + + this._primaryKey = response.primaryKeys![0]; + + if (!this._keyOnly) { + this._value = response.values![0]; + } + + this._gotValue = true; + this._objectStorePosition = structuredClone(response.primaryKeys![0]); + if (response.indexKeys !== undefined && response.indexKeys.length > 0) { + this._indexPosition = structuredClone(response.indexKeys[0]); + } + + return this; + } + + // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBCursor-update-IDBRequest-any-value + public update(value: any) { + if (value === undefined) { + throw new TypeError(); + } + + const transaction = this._effectiveObjectStore.transaction; + + if (transaction._state !== "active") { + throw new TransactionInactiveError(); + } + + if (transaction.mode === "readonly") { + throw new ReadOnlyError(); + } + + if (this._effectiveObjectStore._deleted) { + throw new InvalidStateError(); + } + + if ( + !(this.source instanceof BridgeIDBObjectStore) && + this.source._deleted + ) { + throw new InvalidStateError(); + } + + if (!this._gotValue || !this._isValueCursor) { + throw new InvalidStateError(); + } + + const storeReq: RecordStoreRequest = { + key: this._primaryKey, + value: value, + objectStoreName: this._objectStoreName, + storeLevel: StoreLevel.UpdateExisting, + }; + + const operation = async () => { + if (BridgeIDBFactory.enableTracing) { + console.log("updating at cursor"); + } + const { btx } = this.source._confirmActiveTransaction(); + await this._backend.storeRecord(btx, storeReq); + }; + return transaction._execRequestAsync({ + operation, + source: this, + }); + } + + /** + * http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBCursor-advance-void-unsigned-long-count + */ + public advance(count: number) { + throw Error("not implemented"); + } + + /** + * http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBCursor-continue-void-any-key + */ + public continue(key?: IDBValidKey) { + const transaction = this._effectiveObjectStore.transaction; + + if (transaction._state !== "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 (key !== undefined) { + key = valueToKey(key); + let lastKey = + this._indexName === undefined + ? this._objectStorePosition + : this._indexPosition; + + const cmpResult = compareKeys(key, lastKey); + + if ( + (cmpResult <= 0 && + (this.direction === "next" || this.direction === "nextunique")) || + (cmpResult >= 0 && + (this.direction === "prev" || this.direction === "prevunique")) + ) { + throw new DataError(); + } + } + + if (this._request) { + this._request.readyState = "pending"; + } + + const operation = async () => { + return this._iterate(key); + }; + + transaction._execRequestAsync({ + operation, + request: this._request, + source: this.source, + }); + + this._gotValue = false; + } + + // https://w3c.github.io/IndexedDB/#dom-idbcursor-continueprimarykey + public continuePrimaryKey(key: IDBValidKey, primaryKey: IDBValidKey) { + throw Error("not implemented"); + } + + public delete() { + const transaction = this._effectiveObjectStore.transaction; + + if (transaction._state !== "active") { + throw new TransactionInactiveError(); + } + + if (transaction.mode === "readonly") { + throw new ReadOnlyError(); + } + + if (this._effectiveObjectStore._deleted) { + throw new InvalidStateError(); + } + if ( + !(this.source instanceof BridgeIDBObjectStore) && + this.source._deleted + ) { + throw new InvalidStateError(); + } + + if (!this._gotValue || !this._isValueCursor) { + throw new InvalidStateError(); + } + + const operation = async () => { + const { btx } = this.source._confirmActiveTransaction(); + this._backend.deleteRecord( + btx, + this._objectStoreName, + BridgeIDBKeyRange._valueToKeyRange(this._primaryKey), + ); + }; + + return transaction._execRequestAsync({ + operation, + source: this, + }); + } + + public toString() { + return "[object IDBCursor]"; + } +} + +export class BridgeIDBCursorWithValue extends BridgeIDBCursor { + get value(): any { + return this._value; + } + + protected get _isValueCursor(): boolean { + return true; + } + + constructor( + source: CursorSource, + objectStoreName: string, + indexName: string | undefined, + range: IDBValidKey | IDBKeyRange | undefined | null, + direction: IDBCursorDirection, + request?: any, + ) { + super(source, objectStoreName, indexName, range, direction, request, false); + } + + public toString() { + return "[object IDBCursorWithValue]"; + } +} + +/** + * Ensure that an active version change transaction is currently running. + */ +const confirmActiveVersionchangeTransaction = (database: BridgeIDBDatabase) => { + if (!database._runningVersionchangeTransaction) { + throw new InvalidStateError(); + } + + // Find the latest versionchange transaction + const transactions = database._transactions.filter( + (tx: BridgeIDBTransaction) => { + return tx.mode === "versionchange"; + }, + ); + const transaction = transactions[transactions.length - 1]; + + if (!transaction || transaction._state === "finished") { + throw new InvalidStateError(); + } + + if (transaction._state !== "active") { + throw new TransactionInactiveError(); + } + + return transaction; +}; + +// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#database-interface +/** @public */ +export class BridgeIDBDatabase extends FakeEventTarget { + _closePending = false; + _closed = false; + _runningVersionchangeTransaction = false; + _transactions: Array<BridgeIDBTransaction> = []; + + _backendConnection: DatabaseConnection; + _backend: Backend; + + _schema: Schema; + + get name(): string { + return this._schema.databaseName; + } + + get version(): number { + return this._schema.databaseVersion; + } + + get objectStoreNames(): FakeDOMStringList { + return fakeDOMStringList(Object.keys(this._schema.objectStores)).sort(); + } + + /** + * http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#database-closing-steps + */ + _closeConnection() { + this._closePending = true; + + const transactionsComplete = this._transactions.every( + (transaction: BridgeIDBTransaction) => { + return transaction._state === "finished"; + }, + ); + + if (transactionsComplete) { + this._closed = true; + this._backend.close(this._backendConnection); + } else { + queueTask(() => { + this._closeConnection(); + }); + } + } + + constructor(backend: Backend, backendConnection: DatabaseConnection) { + super(); + + this._schema = backend.getSchema(backendConnection); + + this._backend = backend; + this._backendConnection = backendConnection; + } + + // http://w3c.github.io/IndexedDB/#dom-idbdatabase-createobjectstore + public createObjectStore( + name: string, + options: { autoIncrement?: boolean; keyPath?: IDBKeyPath } | null = {}, + ): BridgeIDBObjectStore { + if (name === undefined) { + throw new TypeError(); + } + const transaction = confirmActiveVersionchangeTransaction(this); + const backendTx = transaction._backendTransaction; + if (!backendTx) { + throw Error("invariant violated"); + } + + const keyPath = + options !== null && options.keyPath !== undefined + ? options.keyPath + : null; + const autoIncrement = + options !== null && options.autoIncrement !== undefined + ? options.autoIncrement + : false; + + if (keyPath !== null) { + validateKeyPath(keyPath); + } + + if (Object.keys(this._schema.objectStores).includes(name)) { + throw new ConstraintError(); + } + + if (autoIncrement && (keyPath === "" || Array.isArray(keyPath))) { + throw new InvalidAccessError(); + } + + transaction._backend.createObjectStore( + backendTx, + name, + keyPath, + autoIncrement, + ); + + this._schema = this._backend.getSchema(this._backendConnection); + + return transaction.objectStore(name); + } + + public deleteObjectStore(name: string): void { + if (name === undefined) { + throw new TypeError(); + } + const transaction = confirmActiveVersionchangeTransaction(this); + transaction._objectStoresCache.delete(name); + } + + public _internalTransaction( + storeNames: string | string[], + mode?: IDBTransactionMode, + backendTransaction?: DatabaseTransaction, + ): BridgeIDBTransaction { + mode = mode !== undefined ? mode : "readonly"; + if ( + mode !== "readonly" && + mode !== "readwrite" && + mode !== "versionchange" + ) { + throw new TypeError("Invalid mode: " + mode); + } + + const hasActiveVersionchange = this._transactions.some( + (transaction: BridgeIDBTransaction) => { + return ( + transaction._state === "active" && + transaction.mode === "versionchange" && + transaction.db === this + ); + }, + ); + if (hasActiveVersionchange) { + throw new InvalidStateError(); + } + + if (this._closePending) { + throw new InvalidStateError(); + } + + if (!Array.isArray(storeNames)) { + storeNames = [storeNames]; + } + if (storeNames.length === 0 && mode !== "versionchange") { + throw new InvalidAccessError(); + } + for (const storeName of storeNames) { + if (this.objectStoreNames.indexOf(storeName) < 0) { + throw new NotFoundError( + "No objectStore named " + storeName + " in this database", + ); + } + } + + const tx = new BridgeIDBTransaction( + storeNames, + mode, + this, + backendTransaction, + ); + this._transactions.push(tx); + queueTask(() => tx._start()); + return tx; + } + + public transaction( + storeNames: string | string[], + mode?: IDBTransactionMode, + ): BridgeIDBTransaction { + if (mode === "versionchange") { + throw new TypeError("Invalid mode: " + mode); + } + return this._internalTransaction(storeNames, mode); + } + + public close() { + this._closeConnection(); + } + + public toString() { + return "[object IDBDatabase]"; + } +} + +/** @public */ +export type DatabaseList = Array<{ name: string; version: number }>; + +/** @public */ +export class BridgeIDBFactory { + public cmp = compareKeys; + private backend: Backend; + private connections: BridgeIDBDatabase[] = []; + static enableTracing: boolean = false; + + public constructor(backend: Backend) { + this.backend = backend; + } + + // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBFactory-deleteDatabase-IDBOpenDBRequest-DOMString-name + public deleteDatabase(name: string): BridgeIDBOpenDBRequest { + const request = new BridgeIDBOpenDBRequest(); + request.source = null; + + queueTask(async () => { + const databases = await this.backend.getDatabases(); + const dbInfo = databases.find((x) => x.name == name); + if (!dbInfo) { + // Database already doesn't exist, success! + const event = new BridgeIDBVersionChangeEvent("success", { + newVersion: null, + oldVersion: 0, + }); + request.dispatchEvent(event); + return; + } + const oldVersion = dbInfo.version; + + try { + const dbconn = await this.backend.connectDatabase(name); + const backendTransaction = await this.backend.enterVersionChange( + dbconn, + 0, + ); + await this.backend.deleteDatabase(backendTransaction, name); + await this.backend.commit(backendTransaction); + await this.backend.close(dbconn); + + request.result = undefined; + request.readyState = "done"; + + const event2 = new BridgeIDBVersionChangeEvent("success", { + newVersion: null, + oldVersion, + }); + request.dispatchEvent(event2); + } catch (err) { + request.error = new Error(); + request.error.name = err.name; + request.readyState = "done"; + + const event = new FakeEvent("error", { + bubbles: true, + cancelable: true, + }); + event.eventPath = []; + request.dispatchEvent(event); + } + }); + + return request; + } + + // tslint:disable-next-line max-line-length + // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBFactory-open-IDBOpenDBRequest-DOMString-name-unsigned-long-long-version + public open(name: string, version?: number) { + if (arguments.length > 1 && version !== undefined) { + // Based on spec, not sure why "MAX_SAFE_INTEGER" instead of "unsigned long long", but it's needed to pass + // tests + version = enforceRange(version, "MAX_SAFE_INTEGER"); + } + if (version === 0) { + throw new TypeError(); + } + + const request = new BridgeIDBOpenDBRequest(); + + queueTask(async () => { + let dbconn: DatabaseConnection; + try { + dbconn = await this.backend.connectDatabase(name); + } catch (err) { + request._finishWithError(err); + return; + } + + const schema = this.backend.getSchema(dbconn); + const existingVersion = schema.databaseVersion; + + if (version === undefined) { + version = existingVersion !== 0 ? existingVersion : 1; + } + + const requestedVersion = version; + + BridgeIDBFactory.enableTracing && + console.log( + `TRACE: existing version ${existingVersion}, requested version ${requestedVersion}`, + ); + + if (existingVersion > requestedVersion) { + request._finishWithError(new VersionError()); + return; + } + + const db = new BridgeIDBDatabase(this.backend, dbconn); + + if (existingVersion == requestedVersion) { + request.result = db; + request.readyState = "done"; + + const event2 = new FakeEvent("success", { + bubbles: false, + cancelable: false, + }); + event2.eventPath = [request]; + request.dispatchEvent(event2); + } + + 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) { + const event = new BridgeIDBVersionChangeEvent("versionchange", { + newVersion: version, + oldVersion: existingVersion, + }); + otherConn.dispatchEvent(event); + } + + if (this._anyOpen()) { + const event = new BridgeIDBVersionChangeEvent("blocked", { + newVersion: version, + oldVersion: existingVersion, + }); + request.dispatchEvent(event); + } + + const backendTransaction = await this.backend.enterVersionChange( + dbconn, + requestedVersion, + ); + db._runningVersionchangeTransaction = true; + + const transaction = db._internalTransaction( + [], + "versionchange", + backendTransaction, + ); + const event = new BridgeIDBVersionChangeEvent("upgradeneeded", { + newVersion: version, + oldVersion: existingVersion, + }); + + request.result = db; + request.readyState = "done"; + request.transaction = transaction; + request.dispatchEvent(event); + + await transaction._waitDone(); + + // We don't explicitly exit the versionchange transaction, + // since this is already done by the BridgeIDBTransaction. + db._runningVersionchangeTransaction = false; + + const event2 = new FakeEvent("success", { + bubbles: false, + cancelable: false, + }); + event2.eventPath = [request]; + + request.dispatchEvent(event2); + } + + this.connections.push(db); + return db; + }); + + return request; + } + + // https://w3c.github.io/IndexedDB/#dom-idbfactory-databases + public databases(): Promise<DatabaseList> { + return this.backend.getDatabases(); + } + + public toString(): string { + return "[object IDBFactory]"; + } + + private _anyOpen(): boolean { + return this.connections.some((c) => !c._closed && !c._closePending); + } +} + +const confirmActiveTransaction = ( + index: BridgeIDBIndex, +): BridgeIDBTransaction => { + if (index._deleted || index.objectStore._deleted) { + throw new InvalidStateError(); + } + + if (index.objectStore.transaction._state !== "active") { + throw new TransactionInactiveError(); + } + + return index.objectStore.transaction; +}; + +// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#idl-def-IDBIndex +/** @public */ +export class BridgeIDBIndex { + objectStore: BridgeIDBObjectStore; + + get _schema(): Schema { + return this.objectStore.transaction.db._schema; + } + + get keyPath(): IDBKeyPath { + return this._schema.objectStores[this.objectStore.name].indexes[this._name] + .keyPath; + } + + get multiEntry(): boolean { + return this._schema.objectStores[this.objectStore.name].indexes[this._name] + .multiEntry; + } + + get unique(): boolean { + return this._schema.objectStores[this.objectStore.name].indexes[this._name] + .unique; + } + + get _backend(): Backend { + return this.objectStore._backend; + } + + _confirmActiveTransaction(): { btx: DatabaseTransaction } { + return this.objectStore._confirmActiveTransaction(); + } + + private _name: string; + + public _deleted: boolean = false; + + constructor(objectStore: BridgeIDBObjectStore, name: string) { + this._name = name; + this.objectStore = objectStore; + } + + get name() { + return this._name; + } + + // https://w3c.github.io/IndexedDB/#dom-idbindex-name + set name(name: any) { + const transaction = this.objectStore.transaction; + + if (!transaction.db._runningVersionchangeTransaction) { + throw new InvalidStateError(); + } + + if (transaction._state !== "active") { + throw new TransactionInactiveError(); + } + + const { btx } = this._confirmActiveTransaction(); + + const oldName = this._name; + const newName = String(name); + + if (newName === oldName) { + return; + } + + this._backend.renameIndex(btx, this.objectStore.name, oldName, newName); + + if (this.objectStore.indexNames.indexOf(name) >= 0) { + throw new ConstraintError(); + } + } + + // tslint:disable-next-line max-line-length + // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-openCursor-IDBRequest-any-range-IDBCursorDirection-direction + public openCursor( + range?: BridgeIDBKeyRange | IDBValidKey | null | undefined, + direction: IDBCursorDirection = "next", + ) { + confirmActiveTransaction(this); + + if (range === null) { + range = undefined; + } + if (range !== undefined && !(range instanceof BridgeIDBKeyRange)) { + range = BridgeIDBKeyRange.only(valueToKey(range)); + } + + const request = new BridgeIDBRequest(); + request.source = this; + request.transaction = this.objectStore.transaction; + + const cursor = new BridgeIDBCursorWithValue( + this, + this.objectStore.name, + this._name, + range, + direction, + request, + ); + + const operation = async () => { + return cursor._iterate(); + }; + + return this.objectStore.transaction._execRequestAsync({ + operation, + request, + source: this, + }); + } + + // tslint:disable-next-line max-line-length + // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-openKeyCursor-IDBRequest-any-range-IDBCursorDirection-direction + public openKeyCursor( + range?: BridgeIDBKeyRange | IDBValidKey | null | undefined, + direction: IDBCursorDirection = "next", + ) { + confirmActiveTransaction(this); + + if (range === null) { + range = undefined; + } + if (range !== undefined && !(range instanceof BridgeIDBKeyRange)) { + range = BridgeIDBKeyRange.only(valueToKey(range)); + } + + const request = new BridgeIDBRequest(); + request.source = this; + request.transaction = this.objectStore.transaction; + + const cursor = new BridgeIDBCursor( + this, + this.objectStore.name, + this._name, + range, + direction, + request, + true, + ); + + return this.objectStore.transaction._execRequestAsync({ + operation: cursor._iterate.bind(cursor), + request, + source: this, + }); + } + + public get(key: BridgeIDBKeyRange | IDBValidKey) { + confirmActiveTransaction(this); + + if (!(key instanceof BridgeIDBKeyRange)) { + key = BridgeIDBKeyRange._valueToKeyRange(key); + } + + const getReq: RecordGetRequest = { + direction: "next", + indexName: this._name, + limit: 1, + range: key, + objectStoreName: this.objectStore._name, + resultLevel: ResultLevel.Full, + }; + + const operation = async () => { + const { btx } = this._confirmActiveTransaction(); + const result = await this._backend.getRecords(btx, getReq); + if (result.count == 0) { + return undefined; + } + const values = result.values; + if (!values) { + throw Error("invariant violated"); + } + return values[0]; + }; + + return this.objectStore.transaction._execRequestAsync({ + operation, + source: this, + }); + } + + // http://w3c.github.io/IndexedDB/#dom-idbindex-getall + public getAll(query?: BridgeIDBKeyRange | IDBValidKey, count?: number) { + throw Error("not implemented"); + } + + // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-getKey-IDBRequest-any-key + public getKey(key: BridgeIDBKeyRange | IDBValidKey) { + confirmActiveTransaction(this); + + if (!(key instanceof BridgeIDBKeyRange)) { + key = BridgeIDBKeyRange._valueToKeyRange(key); + } + + const getReq: RecordGetRequest = { + direction: "next", + indexName: this._name, + limit: 1, + range: key, + objectStoreName: this.objectStore._name, + resultLevel: ResultLevel.OnlyKeys, + }; + + const operation = async () => { + const { btx } = this._confirmActiveTransaction(); + const result = await this._backend.getRecords(btx, getReq); + if (result.count == 0) { + return undefined; + } + const primaryKeys = result.primaryKeys; + if (!primaryKeys) { + throw Error("invariant violated"); + } + return primaryKeys[0]; + }; + + return this.objectStore.transaction._execRequestAsync({ + operation, + source: this, + }); + } + + // http://w3c.github.io/IndexedDB/#dom-idbindex-getallkeys + public getAllKeys(query?: BridgeIDBKeyRange | IDBValidKey, count?: number) { + throw Error("not implemented"); + } + + // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-count-IDBRequest-any-key + public count(key: BridgeIDBKeyRange | IDBValidKey | null | undefined) { + confirmActiveTransaction(this); + + if (key === null) { + key = undefined; + } + if (key !== undefined && !(key instanceof BridgeIDBKeyRange)) { + key = BridgeIDBKeyRange.only(valueToKey(key)); + } + + const getReq: RecordGetRequest = { + direction: "next", + indexName: this._name, + limit: 1, + range: key, + objectStoreName: this.objectStore._name, + resultLevel: ResultLevel.OnlyCount, + }; + + const operation = async () => { + const { btx } = this._confirmActiveTransaction(); + const result = await this._backend.getRecords(btx, getReq); + return result.count; + }; + + return this.objectStore.transaction._execRequestAsync({ + operation, + source: this, + }); + } + + public toString() { + return "[object IDBIndex]"; + } +} + +// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#range-concept +/** @public */ +export class BridgeIDBKeyRange { + public static only(value: IDBValidKey) { + if (arguments.length === 0) { + throw new TypeError(); + } + value = valueToKey(value); + return new BridgeIDBKeyRange(value, value, false, false); + } + + static lowerBound(lower: IDBValidKey, open: boolean = false) { + if (arguments.length === 0) { + throw new TypeError(); + } + lower = valueToKey(lower); + return new BridgeIDBKeyRange(lower, undefined, open, true); + } + + static upperBound(upper: IDBValidKey, open: boolean = false) { + if (arguments.length === 0) { + throw new TypeError(); + } + upper = valueToKey(upper); + return new BridgeIDBKeyRange(undefined, upper, true, open); + } + + static bound( + lower: IDBValidKey, + upper: IDBValidKey, + lowerOpen: boolean = false, + upperOpen: boolean = false, + ) { + if (arguments.length < 2) { + throw new TypeError(); + } + + const cmpResult = compareKeys(lower, upper); + if (cmpResult === 1 || (cmpResult === 0 && (lowerOpen || upperOpen))) { + throw new DataError(); + } + + lower = valueToKey(lower); + upper = valueToKey(upper); + return new BridgeIDBKeyRange(lower, upper, lowerOpen, upperOpen); + } + + readonly lower: IDBValidKey | undefined; + readonly upper: IDBValidKey | undefined; + readonly lowerOpen: boolean; + readonly upperOpen: boolean; + + constructor( + lower: IDBValidKey | undefined, + upper: IDBValidKey | undefined, + lowerOpen: boolean, + upperOpen: boolean, + ) { + this.lower = lower; + this.upper = upper; + this.lowerOpen = lowerOpen; + this.upperOpen = upperOpen; + } + + // https://w3c.github.io/IndexedDB/#dom-idbkeyrange-includes + includes(key: IDBValidKey) { + if (arguments.length === 0) { + throw new TypeError(); + } + key = valueToKey(key); + + if (this.lower !== undefined) { + const cmpResult = compareKeys(this.lower, key); + + if (cmpResult === 1 || (cmpResult === 0 && this.lowerOpen)) { + return false; + } + } + if (this.upper !== undefined) { + const cmpResult = compareKeys(this.upper, key); + + if (cmpResult === -1 || (cmpResult === 0 && this.upperOpen)) { + return false; + } + } + return true; + } + + toString() { + return "[object IDBKeyRange]"; + } + + static _valueToKeyRange(value: any, nullDisallowedFlag: boolean = false) { + if (value instanceof BridgeIDBKeyRange) { + return value; + } + + if (value === null || value === undefined) { + if (nullDisallowedFlag) { + throw new DataError(); + } + return new BridgeIDBKeyRange(undefined, undefined, false, false); + } + + const key = valueToKey(value); + + return BridgeIDBKeyRange.only(key); + } +} + +// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#object-store +/** @public */ +export class BridgeIDBObjectStore { + _indexesCache: Map<string, BridgeIDBIndex> = new Map(); + + transaction: BridgeIDBTransaction; + + get autoIncrement(): boolean { + return this._schema.objectStores[this._name].autoIncrement; + } + + get indexNames(): FakeDOMStringList { + return fakeDOMStringList( + Object.keys(this._schema.objectStores[this._name].indexes), + ).sort(); + } + + get keyPath(): IDBKeyPath | null { + return this._schema.objectStores[this._name].keyPath; + } + + _name: string; + + get _schema(): Schema { + return this.transaction.db._schema; + } + + _deleted: boolean = false; + + constructor(transaction: BridgeIDBTransaction, name: string) { + this._name = name; + this.transaction = transaction; + } + + get name() { + return this._name; + } + + get _backend(): Backend { + return this.transaction.db._backend; + } + + get _backendConnection(): DatabaseConnection { + return this.transaction.db._backendConnection; + } + + _confirmActiveTransaction(): { btx: DatabaseTransaction } { + const btx = this.transaction._backendTransaction; + if (!btx) { + throw new InvalidStateError(); + } + return { btx }; + } + + // http://w3c.github.io/IndexedDB/#dom-idbobjectstore-name + set name(newName: any) { + const transaction = this.transaction; + + if (!transaction.db._runningVersionchangeTransaction) { + throw new InvalidStateError(); + } + + let { btx } = this._confirmActiveTransaction(); + + newName = String(newName); + + const oldName = this._name; + + if (newName === oldName) { + return; + } + + this._backend.renameObjectStore(btx, oldName, newName); + this.transaction.db._schema = this._backend.getSchema( + this._backendConnection, + ); + } + + public _store(value: any, key: IDBValidKey | undefined, overwrite: boolean) { + if (BridgeIDBFactory.enableTracing) { + console.log(`TRACE: IDBObjectStore._store`); + } + if (this.transaction.mode === "readonly") { + throw new ReadOnlyError(); + } + const operation = async () => { + const { btx } = this._confirmActiveTransaction(); + const result = await this._backend.storeRecord(btx, { + objectStoreName: this._name, + key: key, + value: value, + storeLevel: overwrite + ? StoreLevel.AllowOverwrite + : StoreLevel.NoOverwrite, + }); + return result.key; + }; + + return this.transaction._execRequestAsync({ operation, source: this }); + } + + public put(value: any, key?: IDBValidKey) { + if (arguments.length === 0) { + throw new TypeError(); + } + return this._store(value, key, true); + } + + public add(value: any, key?: IDBValidKey) { + if (arguments.length === 0) { + throw new TypeError(); + } + return this._store(value, key, false); + } + + public delete(key: IDBValidKey | BridgeIDBKeyRange) { + if (arguments.length === 0) { + throw new TypeError(); + } + + if (this.transaction.mode === "readonly") { + throw new ReadOnlyError(); + } + + let keyRange: BridgeIDBKeyRange; + + if (key instanceof BridgeIDBKeyRange) { + keyRange = key; + } else { + keyRange = BridgeIDBKeyRange.only(valueToKey(key)); + } + + const operation = async () => { + const { btx } = this._confirmActiveTransaction(); + return this._backend.deleteRecord(btx, this._name, keyRange); + }; + + return this.transaction._execRequestAsync({ + operation, + source: this, + }); + } + + public get(key?: BridgeIDBKeyRange | IDBValidKey) { + if (BridgeIDBFactory.enableTracing) { + console.log(`getting from object store ${this._name} key ${key}`); + } + + if (arguments.length === 0) { + throw new TypeError(); + } + + let keyRange: BridgeIDBKeyRange; + + if (key instanceof BridgeIDBKeyRange) { + keyRange = key; + } else { + try { + keyRange = BridgeIDBKeyRange.only(valueToKey(key)); + } catch (e) { + throw Error( + `invalid key (type ${typeof key}) for object store ${this._name}`, + ); + } + } + + const recordRequest: RecordGetRequest = { + objectStoreName: this._name, + indexName: undefined, + lastIndexPosition: undefined, + lastObjectStorePosition: undefined, + direction: "next", + limit: 1, + resultLevel: ResultLevel.Full, + range: keyRange, + }; + + const operation = async () => { + if (BridgeIDBFactory.enableTracing) { + console.log("running get operation:", recordRequest); + } + const { btx } = this._confirmActiveTransaction(); + const result = await this._backend.getRecords(btx, recordRequest); + + if (BridgeIDBFactory.enableTracing) { + console.log("get operation result count:", result.count); + } + + if (result.count === 0) { + return undefined; + } + const values = result.values; + if (!values) { + throw Error("invariant violated"); + } + return values[0]; + }; + + return this.transaction._execRequestAsync({ + operation, + source: this, + }); + } + + // http://w3c.github.io/IndexedDB/#dom-idbobjectstore-getall + public getAll(query?: BridgeIDBKeyRange | IDBValidKey, count?: number) { + throw Error("not implemented"); + } + + // http://w3c.github.io/IndexedDB/#dom-idbobjectstore-getkey + public getKey(key?: BridgeIDBKeyRange | IDBValidKey) { + throw Error("not implemented"); + } + + // http://w3c.github.io/IndexedDB/#dom-idbobjectstore-getallkeys + public getAllKeys(query?: BridgeIDBKeyRange | IDBValidKey, count?: number) { + throw Error("not implemented"); + } + + public clear() { + throw Error("not implemented"); + } + + public openCursor( + range?: IDBKeyRange | IDBValidKey, + direction: IDBCursorDirection = "next", + ) { + if (range === null) { + range = undefined; + } + if (range !== undefined && !(range instanceof BridgeIDBKeyRange)) { + range = BridgeIDBKeyRange.only(valueToKey(range)); + } + + const request = new BridgeIDBRequest(); + request.source = this; + request.transaction = this.transaction; + + const cursor = new BridgeIDBCursorWithValue( + this, + this._name, + undefined, + range, + direction, + request, + ); + + return this.transaction._execRequestAsync({ + operation: () => cursor._iterate(), + request, + source: this, + }); + } + + public openKeyCursor( + range?: BridgeIDBKeyRange | IDBValidKey, + direction?: IDBCursorDirection, + ) { + if (range === null) { + range = undefined; + } + if (range !== undefined && !(range instanceof BridgeIDBKeyRange)) { + range = BridgeIDBKeyRange.only(valueToKey(range)); + } + + if (!direction) { + direction = "next"; + } + + const request = new BridgeIDBRequest(); + request.source = this; + request.transaction = this.transaction; + + const cursor = new BridgeIDBCursor( + this, + this._name, + undefined, + range, + direction, + request, + true, + ); + + return this.transaction._execRequestAsync({ + operation: cursor._iterate.bind(cursor), + request, + source: this, + }); + } + + // tslint:disable-next-line max-line-length + // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBObjectStore-createIndex-IDBIndex-DOMString-name-DOMString-sequence-DOMString--keyPath-IDBIndexParameters-optionalParameters + public createIndex( + indexName: string, + keyPath: IDBKeyPath, + optionalParameters: { multiEntry?: boolean; unique?: boolean } = {}, + ) { + if (arguments.length < 2) { + throw new TypeError(); + } + + if (!this.transaction.db._runningVersionchangeTransaction) { + throw new InvalidStateError(); + } + + const { btx } = this._confirmActiveTransaction(); + + const multiEntry = + optionalParameters.multiEntry !== undefined + ? optionalParameters.multiEntry + : false; + const unique = + optionalParameters.unique !== undefined + ? optionalParameters.unique + : false; + + if (this.transaction.mode !== "versionchange") { + throw new InvalidStateError(); + } + + if (this.indexNames.indexOf(indexName) >= 0) { + throw new ConstraintError(); + } + + validateKeyPath(keyPath); + + if (Array.isArray(keyPath) && multiEntry) { + throw new InvalidAccessError(); + } + + this._backend.createIndex( + btx, + indexName, + this._name, + keyPath, + multiEntry, + unique, + ); + + return new BridgeIDBIndex(this, indexName); + } + + // https://w3c.github.io/IndexedDB/#dom-idbobjectstore-index + public index(name: string) { + if (arguments.length === 0) { + throw new TypeError(); + } + + if (this.transaction._state === "finished") { + throw new InvalidStateError(); + } + + const index = this._indexesCache.get(name); + if (index !== undefined) { + return index; + } + + return new BridgeIDBIndex(this, name); + } + + public deleteIndex(indexName: string) { + if (arguments.length === 0) { + throw new TypeError(); + } + + if (this.transaction.mode !== "versionchange") { + throw new InvalidStateError(); + } + + if (!this.transaction.db._runningVersionchangeTransaction) { + throw new InvalidStateError(); + } + + const { btx } = this._confirmActiveTransaction(); + + const index = this._indexesCache.get(indexName); + if (index !== undefined) { + index._deleted = true; + } + + this._backend.deleteIndex(btx, this._name, indexName); + } + + // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBObjectStore-count-IDBRequest-any-key + public count(key?: IDBValidKey | BridgeIDBKeyRange) { + if (key === null) { + key = undefined; + } + if (key !== undefined && !(key instanceof BridgeIDBKeyRange)) { + key = BridgeIDBKeyRange.only(valueToKey(key)); + } + + const recordGetRequest: RecordGetRequest = { + direction: "next", + indexName: undefined, + lastIndexPosition: undefined, + limit: -1, + objectStoreName: this._name, + lastObjectStorePosition: undefined, + range: key, + resultLevel: ResultLevel.OnlyCount, + }; + + const operation = async () => { + const { btx } = this._confirmActiveTransaction(); + const result = await this._backend.getRecords(btx, recordGetRequest); + return result.count; + }; + + return this.transaction._execRequestAsync({ operation, source: this }); + } + + public toString() { + return "[object IDBObjectStore]"; + } +} + +/** @public */ +export class BridgeIDBRequest extends FakeEventTarget { + _result: any = null; + _error: Error | null | undefined = null; + source: BridgeIDBCursor | BridgeIDBIndex | BridgeIDBObjectStore | null = null; + transaction: BridgeIDBTransaction | null = null; + readyState: "done" | "pending" = "pending"; + onsuccess: EventListener | null = null; + onerror: EventListener | null = null; + + get error() { + if (this.readyState === "pending") { + throw new InvalidStateError(); + } + return this._error; + } + + set error(value: any) { + this._error = value; + } + + get result() { + if (this.readyState === "pending") { + throw new InvalidStateError(); + } + return this._result; + } + + set result(value: any) { + this._result = value; + } + + toString() { + return "[object IDBRequest]"; + } + + _finishWithError(err: Error) { + this.result = undefined; + this.readyState = "done"; + + this.error = new Error(err.message); + this.error.name = err.name; + + const event = new FakeEvent("error", { + bubbles: true, + cancelable: true, + }); + event.eventPath = []; + this.dispatchEvent(event); + } + + _finishWithResult(result: any) { + this.result = result; + this.readyState = "done"; + + const event = new FakeEvent("success"); + event.eventPath = []; + this.dispatchEvent(event); + } +} + +/** @public */ +export class BridgeIDBOpenDBRequest extends BridgeIDBRequest { + public onupgradeneeded: EventListener | null = null; + public onblocked: EventListener | null = null; + + constructor() { + super(); + // https://www.w3.org/TR/IndexedDB/#open-requests + this.source = null; + } + + public toString() { + return "[object IDBOpenDBRequest]"; + } +} + +// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#transaction +/** @public */ +export 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: IDBTransactionMode; + public db: BridgeIDBDatabase; + public error: Error | null = null; + public onabort: EventListener | null = null; + public oncomplete: EventListener | null = null; + public onerror: EventListener | 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: IDBTransactionMode, + 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 class BridgeIDBVersionChangeEvent extends FakeEvent { + public newVersion: number | null; + public oldVersion: number; + + constructor( + type: "blocked" | "success" | "upgradeneeded" | "versionchange", + parameters: { newVersion?: number | null; oldVersion?: number } = {}, + ) { + super(type); + + this.newVersion = + parameters.newVersion !== undefined ? parameters.newVersion : null; + this.oldVersion = + parameters.oldVersion !== undefined ? parameters.oldVersion : 0; + } + + public toString() { + return "[object IDBVersionChangeEvent]"; + } +} |