diff options
Diffstat (limited to 'packages/taler-wallet-core/src/query.ts')
-rw-r--r-- | packages/taler-wallet-core/src/query.ts | 1004 |
1 files changed, 1004 insertions, 0 deletions
diff --git a/packages/taler-wallet-core/src/query.ts b/packages/taler-wallet-core/src/query.ts new file mode 100644 index 000000000..dc15bbdd1 --- /dev/null +++ b/packages/taler-wallet-core/src/query.ts @@ -0,0 +1,1004 @@ +/* + This file is part of TALER + (C) 2016 GNUnet e.V. + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * @fileoverview + * Query helpers for IndexedDB databases. + * + * @author Florian Dold + */ + +/** + * Imports. + */ +import { + IDBCursor, + IDBDatabase, + IDBFactory, + IDBKeyPath, + IDBKeyRange, + IDBRequest, + IDBTransaction, + IDBTransactionMode, + IDBValidKey, + IDBVersionChangeEvent, +} from "@gnu-taler/idb-bridge"; +import { + CancellationToken, + Codec, + Logger, + openPromise, +} from "@gnu-taler/taler-util"; + +const logger = new Logger("query.ts"); + +/** + * Exception that should be thrown by client code to abort a transaction. + */ +export const TransactionAbort = Symbol("transaction_abort"); + +/** + * Options for an index. + */ +export interface IndexOptions { + /** + * If true and the path resolves to an array, create an index entry for + * each member of the array (instead of one index entry containing the full array). + * + * Defaults to false. + */ + multiEntry?: boolean; + + /** + * Database version that this store was added in, or + * undefined if added in the first version. + */ + versionAdded?: number; + + /** + * Does this index enforce unique keys? + * + * Defaults to false. + */ + unique?: boolean; +} + +function requestToPromise(req: IDBRequest): Promise<any> { + const stack = Error("Failed request was started here."); + return new Promise((resolve, reject) => { + req.onsuccess = () => { + resolve(req.result); + }; + req.onerror = () => { + console.error("error in DB request", req.error); + reject(req.error); + console.error("Request failed:", stack); + }; + }); +} + +type CursorResult<T> = CursorEmptyResult<T> | CursorValueResult<T>; + +interface CursorEmptyResult<T> { + hasValue: false; +} + +interface CursorValueResult<T> { + hasValue: true; + value: T; +} + +class TransactionAbortedError extends Error { + constructor(m: string) { + super(m); + + // Set the prototype explicitly. + Object.setPrototypeOf(this, TransactionAbortedError.prototype); + } +} + +class ResultStream<T> { + private currentPromise: Promise<void>; + private gotCursorEnd = false; + private awaitingResult = false; + + constructor(private req: IDBRequest) { + this.awaitingResult = true; + let p = openPromise<void>(); + this.currentPromise = p.promise; + req.onsuccess = () => { + if (!this.awaitingResult) { + throw Error("BUG: invariant violated"); + } + const cursor = req.result; + if (cursor) { + this.awaitingResult = false; + p.resolve(); + p = openPromise<void>(); + this.currentPromise = p.promise; + } else { + this.gotCursorEnd = true; + p.resolve(); + } + }; + req.onerror = () => { + p.reject(req.error); + }; + } + + async toArray(): Promise<T[]> { + const arr: T[] = []; + while (true) { + const x = await this.next(); + if (x.hasValue) { + arr.push(x.value); + } else { + break; + } + } + return arr; + } + + async map<R>(f: (x: T) => R): Promise<R[]> { + const arr: R[] = []; + while (true) { + const x = await this.next(); + if (x.hasValue) { + arr.push(f(x.value)); + } else { + break; + } + } + return arr; + } + + async mapAsync<R>(f: (x: T) => Promise<R>): Promise<R[]> { + const arr: R[] = []; + while (true) { + const x = await this.next(); + if (x.hasValue) { + arr.push(await f(x.value)); + } else { + break; + } + } + return arr; + } + + async forEachAsync(f: (x: T) => Promise<void>): Promise<void> { + while (true) { + const x = await this.next(); + if (x.hasValue) { + await f(x.value); + } else { + break; + } + } + } + + async forEach(f: (x: T) => void): Promise<void> { + while (true) { + const x = await this.next(); + if (x.hasValue) { + f(x.value); + } else { + break; + } + } + } + + async filter(f: (x: T) => boolean): Promise<T[]> { + const arr: T[] = []; + while (true) { + const x = await this.next(); + if (x.hasValue) { + if (f(x.value)) { + arr.push(x.value); + } + } else { + break; + } + } + return arr; + } + + async next(): Promise<CursorResult<T>> { + if (this.gotCursorEnd) { + return { hasValue: false }; + } + if (!this.awaitingResult) { + const cursor: IDBCursor | undefined = this.req.result; + if (!cursor) { + throw Error("assertion failed"); + } + this.awaitingResult = true; + cursor.continue(); + } + await this.currentPromise; + if (this.gotCursorEnd) { + return { hasValue: false }; + } + const cursor = this.req.result; + if (!cursor) { + throw Error("assertion failed"); + } + return { hasValue: true, value: cursor.value }; + } +} + +/** + * Return a promise that resolves to the opened IndexedDB database. + */ +export function openDatabase( + idbFactory: IDBFactory, + databaseName: string, + databaseVersion: number | undefined, + onVersionChange: () => void, + onUpgradeNeeded: ( + db: IDBDatabase, + oldVersion: number, + newVersion: number, + upgradeTransaction: IDBTransaction, + ) => void, +): Promise<IDBDatabase> { + return new Promise<IDBDatabase>((resolve, reject) => { + const req = idbFactory.open(databaseName, databaseVersion); + req.onerror = (event) => { + // @ts-expect-error + reject(new Error(`database opening error`, { cause: req.error })); + }; + req.onsuccess = (e) => { + req.result.onversionchange = (evt: IDBVersionChangeEvent) => { + logger.info( + `handling versionchange on ${databaseName} from ${evt.oldVersion} to ${evt.newVersion}`, + ); + req.result.close(); + onVersionChange(); + }; + resolve(req.result); + }; + req.onupgradeneeded = (e) => { + const db = req.result; + const newVersion = e.newVersion; + if (!newVersion) { + // @ts-expect-error + throw Error("upgrade needed, but new version unknown", { + cause: req.error, + }); + } + const transaction = req.transaction; + if (!transaction) { + // @ts-expect-error + throw Error("no transaction handle available in upgrade handler", { + cause: req.error, + }); + } + logger.info( + `handling upgradeneeded event on ${databaseName} from ${e.oldVersion} to ${e.newVersion}`, + ); + onUpgradeNeeded(db, e.oldVersion, newVersion, transaction); + }; + }); +} + +export interface IndexDescriptor { + name: string; + keyPath: IDBKeyPath | IDBKeyPath[]; + multiEntry?: boolean; + unique?: boolean; + versionAdded?: number; +} + +export interface StoreDescriptor<RecordType> { + _dummy: undefined & RecordType; + keyPath?: IDBKeyPath | IDBKeyPath[]; + autoIncrement?: boolean; + /** + * Database version that this store was added in, or + * undefined if added in the first version. + */ + versionAdded?: number; +} + +export interface StoreOptions { + keyPath?: IDBKeyPath | IDBKeyPath[]; + autoIncrement?: boolean; + + /** + * First minor database version that this store was added in, or + * undefined if added in the first version. + */ + versionAdded?: number; +} + +export function describeContents<RecordType = never>( + options: StoreOptions, +): StoreDescriptor<RecordType> { + return { + keyPath: options.keyPath, + _dummy: undefined as any, + autoIncrement: options.autoIncrement, + versionAdded: options.versionAdded, + }; +} + +export function describeIndex( + name: string, + keyPath: IDBKeyPath | IDBKeyPath[], + options: IndexOptions = {}, +): IndexDescriptor { + return { + keyPath, + name, + multiEntry: options.multiEntry, + unique: options.unique, + versionAdded: options.versionAdded, + }; +} + +interface IndexReadOnlyAccessor<RecordType> { + iter(query?: IDBKeyRange | IDBValidKey): ResultStream<RecordType>; + get(query: IDBValidKey): Promise<RecordType | undefined>; + getAll( + query?: IDBKeyRange | IDBValidKey, + count?: number, + ): Promise<RecordType[]>; + getAllKeys( + query?: IDBKeyRange | IDBValidKey, + count?: number, + ): Promise<IDBValidKey[]>; + count(query?: IDBValidKey): Promise<number>; +} + +type GetIndexReadOnlyAccess<RecordType, IndexMap> = { + [P in keyof IndexMap]: IndexReadOnlyAccessor<RecordType>; +}; + +interface IndexReadWriteAccessor<RecordType> { + iter(query: IDBKeyRange | IDBValidKey): ResultStream<RecordType>; + get(query: IDBValidKey): Promise<RecordType | undefined>; + getAll( + query?: IDBKeyRange | IDBValidKey, + count?: number, + ): Promise<RecordType[]>; + getAllKeys( + query?: IDBKeyRange | IDBValidKey, + count?: number, + ): Promise<IDBValidKey[]>; + count(query?: IDBValidKey): Promise<number>; +} + +type GetIndexReadWriteAccess<RecordType, IndexMap> = { + [P in keyof IndexMap]: IndexReadWriteAccessor<RecordType>; +}; + +export interface StoreReadOnlyAccessor<RecordType, IndexMap> { + get(key: IDBValidKey): Promise<RecordType | undefined>; + getAll( + query?: IDBKeyRange | IDBValidKey, + count?: number, + ): Promise<RecordType[]>; + iter(query?: IDBValidKey): ResultStream<RecordType>; + indexes: GetIndexReadOnlyAccess<RecordType, IndexMap>; +} + +export interface InsertResponse { + /** + * Key of the newly inserted (via put/add) record. + */ + key: IDBValidKey; +} + +export interface StoreReadWriteAccessor<RecordType, IndexMap> { + get(key: IDBValidKey): Promise<RecordType | undefined>; + getAll( + query?: IDBKeyRange | IDBValidKey, + count?: number, + ): Promise<RecordType[]>; + iter(query?: IDBValidKey): ResultStream<RecordType>; + put(r: RecordType, key?: IDBValidKey): Promise<InsertResponse>; + add(r: RecordType, key?: IDBValidKey): Promise<InsertResponse>; + delete(key: IDBValidKey): Promise<void>; + indexes: GetIndexReadWriteAccess<RecordType, IndexMap>; +} + +export interface StoreWithIndexes< + StoreName extends string, + RecordType, + IndexMap, +> { + storeName: StoreName; + store: StoreDescriptor<RecordType>; + indexMap: IndexMap; + + /** + * Type marker symbol, to check that the descriptor + * has been created through the right function. + */ + mark: Symbol; +} + +const storeWithIndexesSymbol = Symbol("StoreWithIndexesMark"); + +export function describeStore<StoreName extends string, RecordType, IndexMap>( + name: StoreName, + s: StoreDescriptor<RecordType>, + m: IndexMap, +): StoreWithIndexes<StoreName, RecordType, IndexMap> { + return { + storeName: name, + store: s, + indexMap: m, + mark: storeWithIndexesSymbol, + }; +} + +export function describeStoreV2< + StoreName extends string, + RecordType, + IndexMap extends { [x: string]: IndexDescriptor } = {}, +>(args: { + storeName: StoreName; + recordCodec: Codec<RecordType>; + keyPath?: IDBKeyPath | IDBKeyPath[]; + autoIncrement?: boolean; + /** + * Database version that this store was added in, or + * undefined if added in the first version. + */ + versionAdded?: number; + indexes?: IndexMap; +}): StoreWithIndexes<StoreName, RecordType, IndexMap> { + return { + storeName: args.storeName, + store: { + _dummy: undefined as any, + autoIncrement: args.autoIncrement, + keyPath: args.keyPath, + versionAdded: args.versionAdded, + }, + indexMap: args.indexes ?? ({} as IndexMap), + mark: storeWithIndexesSymbol, + }; +} + +type KeyPathComponents = string | number; + +/** + * Follow a key path (dot-separated) in an object. + */ +type DerefKeyPath<T, P> = P extends `${infer PX extends keyof T & + KeyPathComponents}` + ? T[PX] + : P extends `${infer P0 extends keyof T & KeyPathComponents}.${infer Rest}` + ? DerefKeyPath<T[P0], Rest> + : unknown; + +/** + * Return a path if it is a valid dot-separate path to an object. + * Otherwise, return "never". + */ +type ValidateKeyPath<T, P> = P extends `${infer PX extends keyof T & + KeyPathComponents}` + ? PX + : P extends `${infer P0 extends keyof T & KeyPathComponents}.${infer Rest}` + ? `${P0}.${ValidateKeyPath<T[P0], Rest>}` + : never; + +// function foo<T, P>( +// x: T, +// p: P extends ValidateKeyPath<T, P> ? P : never, +// ): void {} + +// foo({x: [0,1,2]}, "x.0"); + +export type StoreNames<StoreMap> = StoreMap extends { + [P in keyof StoreMap]: StoreWithIndexes<infer SN1, infer SD1, infer IM1>; +} + ? keyof StoreMap + : unknown; + +export type DbReadWriteTransaction< + StoreMap, + StoresArr extends Array<StoreNames<StoreMap>>, +> = StoreMap extends { + [P in string]: StoreWithIndexes<infer _SN1, infer _SD1, infer _IM1>; +} + ? { + [X in StoresArr[number] & + keyof StoreMap]: StoreMap[X] extends StoreWithIndexes< + infer _StoreName, + infer RecordType, + infer IndexMap + > + ? StoreReadWriteAccessor<RecordType, IndexMap> + : unknown; + } + : never; + +export type DbReadOnlyTransaction< + StoreMap, + StoresArr extends Array<StoreNames<StoreMap>>, +> = StoreMap extends { + [P in string]: StoreWithIndexes<infer _SN1, infer _SD1, infer _IM1>; +} + ? { + [X in StoresArr[number] & + keyof StoreMap]: StoreMap[X] extends StoreWithIndexes< + infer _StoreName, + infer RecordType, + infer IndexMap + > + ? StoreReadOnlyAccessor<RecordType, IndexMap> + : unknown; + } + : never; + +/** + * Convert the type of an array to a union of the contents. + * + * Example: + * Input ["foo", "bar"] + * Output "foo" | "bar" + */ +export type UnionFromArray<Arr> = Arr extends { + [X in keyof Arr]: Arr[X] & string; +} + ? Arr[keyof Arr & number] + : unknown; + +function runTx<Arg, Res>( + tx: IDBTransaction, + arg: Arg, + f: (t: Arg, t2: IDBTransaction) => Promise<Res>, + triggerContext: InternalTriggerContext, +): Promise<Res> { + const stack = Error("Failed transaction was started here."); + return new Promise((resolve, reject) => { + let funResult: any = undefined; + let gotFunResult = false; + let transactionException: any = undefined; + tx.oncomplete = () => { + // This is a fatal error: The transaction completed *before* + // the transaction function returned. Likely, the transaction + // function waited on a promise that is *not* resolved in the + // microtask queue, thus triggering the auto-commit behavior. + // Unfortunately, the auto-commit behavior of IDB can't be switched + // of. There are some proposals to add this functionality in the future. + if (!gotFunResult) { + const msg = + "BUG: transaction closed before transaction function returned"; + logger.error(msg); + logger.error(`${stack.stack}`); + reject(Error(msg)); + } + triggerContext.handleAfterCommit(); + resolve(funResult); + }; + tx.onerror = () => { + logger.error("error in transaction"); + logger.error(`${stack.stack}`); + }; + tx.onabort = () => { + let msg: string; + if (tx.error) { + msg = `Transaction aborted (transaction error): ${tx.error}`; + } else if (transactionException !== undefined) { + msg = `Transaction aborted (exception thrown): ${transactionException}`; + } else { + msg = "Transaction aborted (no DB error)"; + } + logger.error(msg); + logger.error(`${stack.stack}`); + reject(new TransactionAbortedError(msg)); + }; + const resP = Promise.resolve().then(() => f(arg, tx)); + resP + .then((result) => { + gotFunResult = true; + funResult = result; + }) + .catch((e) => { + if (e == TransactionAbort) { + logger.trace("aborting transaction"); + } else { + transactionException = e; + console.error("Transaction failed:", e); + console.error(stack); + tx.abort(); + } + }) + .catch((e) => { + console.error("fatal: aborting transaction failed", e); + }); + }); +} + +function makeReadContext( + tx: IDBTransaction, + storePick: { [n: string]: StoreWithIndexes<any, any, any> }, + triggerContext: InternalTriggerContext, +): any { + const ctx: { [s: string]: StoreReadOnlyAccessor<any, any> } = {}; + for (const storeAlias in storePick) { + const indexes: { [s: string]: IndexReadOnlyAccessor<any> } = {}; + const swi = storePick[storeAlias]; + const storeName = swi.storeName; + for (const indexAlias in storePick[storeAlias].indexMap) { + const indexDescriptor: IndexDescriptor = + storePick[storeAlias].indexMap[indexAlias]; + const indexName = indexDescriptor.name; + indexes[indexAlias] = { + get(key) { + triggerContext.storesAccessed.add(storeName); + const req = tx.objectStore(storeName).index(indexName).get(key); + return requestToPromise(req); + }, + iter(query) { + triggerContext.storesAccessed.add(storeName); + const req = tx + .objectStore(storeName) + .index(indexName) + .openCursor(query); + return new ResultStream<any>(req); + }, + getAll(query, count) { + triggerContext.storesAccessed.add(storeName); + const req = tx + .objectStore(storeName) + .index(indexName) + .getAll(query, count); + return requestToPromise(req); + }, + getAllKeys(query, count) { + triggerContext.storesAccessed.add(storeName); + const req = tx + .objectStore(storeName) + .index(indexName) + .getAllKeys(query, count); + return requestToPromise(req); + }, + count(query) { + triggerContext.storesAccessed.add(storeName); + const req = tx.objectStore(storeName).index(indexName).count(query); + return requestToPromise(req); + }, + }; + } + ctx[storeAlias] = { + indexes, + get(key) { + triggerContext.storesAccessed.add(storeName); + const req = tx.objectStore(storeName).get(key); + return requestToPromise(req); + }, + getAll(query, count) { + triggerContext.storesAccessed.add(storeName); + const req = tx.objectStore(storeName).getAll(query, count); + return requestToPromise(req); + }, + iter(query) { + triggerContext.storesAccessed.add(storeName); + const req = tx.objectStore(storeName).openCursor(query); + return new ResultStream<any>(req); + }, + }; + } + return ctx; +} + +function makeWriteContext( + tx: IDBTransaction, + storePick: { [n: string]: StoreWithIndexes<any, any, any> }, + triggerContext: InternalTriggerContext, +): any { + const ctx: { [s: string]: StoreReadWriteAccessor<any, any> } = {}; + for (const storeAlias in storePick) { + const indexes: { [s: string]: IndexReadWriteAccessor<any> } = {}; + const swi = storePick[storeAlias]; + const storeName = swi.storeName; + for (const indexAlias in storePick[storeAlias].indexMap) { + const indexDescriptor: IndexDescriptor = + storePick[storeAlias].indexMap[indexAlias]; + const indexName = indexDescriptor.name; + indexes[indexAlias] = { + get(key) { + triggerContext.storesAccessed.add(storeName); + const req = tx.objectStore(storeName).index(indexName).get(key); + return requestToPromise(req); + }, + iter(query) { + triggerContext.storesAccessed.add(storeName); + const req = tx + .objectStore(storeName) + .index(indexName) + .openCursor(query); + return new ResultStream<any>(req); + }, + getAll(query, count) { + triggerContext.storesAccessed.add(storeName); + const req = tx + .objectStore(storeName) + .index(indexName) + .getAll(query, count); + return requestToPromise(req); + }, + getAllKeys(query, count) { + triggerContext.storesAccessed.add(storeName); + const req = tx + .objectStore(storeName) + .index(indexName) + .getAllKeys(query, count); + return requestToPromise(req); + }, + count(query) { + triggerContext.storesAccessed.add(storeName); + const req = tx.objectStore(storeName).index(indexName).count(query); + return requestToPromise(req); + }, + }; + } + ctx[storeAlias] = { + indexes, + get(key) { + triggerContext.storesAccessed.add(storeName); + const req = tx.objectStore(storeName).get(key); + return requestToPromise(req); + }, + getAll(query, count) { + triggerContext.storesAccessed.add(storeName); + const req = tx.objectStore(storeName).getAll(query, count); + return requestToPromise(req); + }, + iter(query) { + triggerContext.storesAccessed.add(storeName); + const req = tx.objectStore(storeName).openCursor(query); + return new ResultStream<any>(req); + }, + async add(r, k) { + triggerContext.storesAccessed.add(storeName); + triggerContext.storesModified.add(storeName); + const req = tx.objectStore(storeName).add(r, k); + const key = await requestToPromise(req); + return { + key: key, + }; + }, + async put(r, k) { + triggerContext.storesAccessed.add(storeName); + triggerContext.storesModified.add(storeName); + const req = tx.objectStore(storeName).put(r, k); + const key = await requestToPromise(req); + return { + key: key, + }; + }, + delete(k) { + triggerContext.storesAccessed.add(storeName); + triggerContext.storesModified.add(storeName); + const req = tx.objectStore(storeName).delete(k); + return requestToPromise(req); + }, + }; + } + return ctx; +} + +export interface DbAccess<StoreMap> { + idbHandle(): IDBDatabase; + + runAllStoresReadWriteTx<T>( + options: { + label?: string; + }, + txf: ( + tx: DbReadWriteTransaction<StoreMap, Array<StoreNames<StoreMap>>>, + ) => Promise<T>, + ): Promise<T>; + + runAllStoresReadOnlyTx<T>( + options: { + label?: string; + }, + txf: ( + tx: DbReadOnlyTransaction<StoreMap, Array<StoreNames<StoreMap>>>, + ) => Promise<T>, + ): Promise<T>; + + runReadWriteTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>( + opts: { + storeNames: StoreNameArray; + label?: string; + }, + txf: (tx: DbReadWriteTransaction<StoreMap, StoreNameArray>) => Promise<T>, + ): Promise<T>; + + runReadOnlyTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>( + opts: { + storeNames: StoreNameArray; + label?: string; + }, + txf: (tx: DbReadOnlyTransaction<StoreMap, StoreNameArray>) => Promise<T>, + ): Promise<T>; +} + +export interface AfterCommitInfo { + mode: IDBTransactionMode; + scope: Set<string>; + accessedStores: Set<string>; + modifiedStores: Set<string>; +} + +export interface TriggerSpec { + /** + * Trigger run after every successful commit, run outside of the transaction. + */ + afterCommit?: (info: AfterCommitInfo) => void; + + // onRead(store, value) + // initState<State> () => State + // beforeCommit<State>? (tx: Transaction, s: State | undefined) => Promise<void>; +} + +class InternalTriggerContext { + storesScope: Set<string>; + storesAccessed: Set<string> = new Set(); + storesModified: Set<string> = new Set(); + + constructor( + private triggerSpec: TriggerSpec, + private mode: IDBTransactionMode, + scope: string[], + ) { + this.storesScope = new Set(scope); + } + + handleAfterCommit() { + if (this.triggerSpec.afterCommit) { + this.triggerSpec.afterCommit({ + mode: this.mode, + accessedStores: this.storesAccessed, + modifiedStores: this.storesModified, + scope: this.storesScope, + }); + } + } +} + +/** + * Type-safe access to a database with a particular store map. + * + * A store map is the metadata that describes the store. + */ +export class DbAccessImpl<StoreMap> implements DbAccess<StoreMap> { + constructor( + private db: IDBDatabase, + private stores: StoreMap, + private triggers: TriggerSpec = {}, + private cancellationToken: CancellationToken, + ) {} + + idbHandle(): IDBDatabase { + return this.db; + } + + runAllStoresReadWriteTx<T>( + options: { + label?: string; + }, + txf: ( + tx: DbReadWriteTransaction<StoreMap, Array<StoreNames<StoreMap>>>, + ) => Promise<T>, + ): Promise<T> { + const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } = + {}; + const strStoreNames: string[] = []; + for (const sn of Object.keys(this.stores as any)) { + const swi = (this.stores as any)[sn] as StoreWithIndexes<any, any, any>; + strStoreNames.push(swi.storeName); + accessibleStores[swi.storeName] = swi; + } + const mode = "readwrite"; + const triggerContext = new InternalTriggerContext( + this.triggers, + mode, + strStoreNames, + ); + const tx = this.db.transaction(strStoreNames, mode); + const writeContext = makeWriteContext(tx, accessibleStores, triggerContext); + return runTx(tx, writeContext, txf, triggerContext); + } + + async runAllStoresReadOnlyTx<T>( + options: { + label?: string; + }, + txf: ( + tx: DbReadOnlyTransaction<StoreMap, Array<StoreNames<StoreMap>>>, + ) => Promise<T>, + ): Promise<T> { + const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } = + {}; + const strStoreNames: string[] = []; + for (const sn of Object.keys(this.stores as any)) { + const swi = (this.stores as any)[sn] as StoreWithIndexes<any, any, any>; + strStoreNames.push(swi.storeName); + accessibleStores[swi.storeName] = swi; + } + const mode = "readonly"; + const triggerContext = new InternalTriggerContext( + this.triggers, + mode, + strStoreNames, + ); + const tx = this.db.transaction(strStoreNames, mode); + const writeContext = makeReadContext(tx, accessibleStores, triggerContext); + const res = await runTx(tx, writeContext, txf, triggerContext); + return res; + } + + async runReadWriteTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>( + opts: { + storeNames: StoreNameArray; + }, + txf: (tx: DbReadWriteTransaction<StoreMap, StoreNameArray>) => Promise<T>, + ): Promise<T> { + const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } = + {}; + const strStoreNames: string[] = []; + for (const sn of opts.storeNames) { + const swi = (this.stores as any)[sn] as StoreWithIndexes<any, any, any>; + strStoreNames.push(swi.storeName); + accessibleStores[swi.storeName] = swi; + } + const mode = "readwrite"; + const triggerContext = new InternalTriggerContext( + this.triggers, + mode, + strStoreNames, + ); + const tx = this.db.transaction(strStoreNames, mode); + const writeContext = makeWriteContext(tx, accessibleStores, triggerContext); + const res = await runTx(tx, writeContext, txf, triggerContext); + return res; + } + + runReadOnlyTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>( + opts: { + storeNames: StoreNameArray; + }, + txf: (tx: DbReadOnlyTransaction<StoreMap, StoreNameArray>) => Promise<T>, + ): Promise<T> { + const accessibleStores: { [x: string]: StoreWithIndexes<any, any, any> } = + {}; + const strStoreNames: string[] = []; + for (const sn of opts.storeNames) { + const swi = (this.stores as any)[sn] as StoreWithIndexes<any, any, any>; + strStoreNames.push(swi.storeName); + accessibleStores[swi.storeName] = swi; + } + const mode = "readonly"; + const triggerContext = new InternalTriggerContext( + this.triggers, + mode, + strStoreNames, + ); + const tx = this.db.transaction(strStoreNames, mode); + const readContext = makeReadContext(tx, accessibleStores, triggerContext); + const res = runTx(tx, readContext, txf, triggerContext); + return res; + } +} |