diff options
author | Florian Dold <florian@dold.me> | 2024-02-19 18:05:48 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2024-02-19 18:05:48 +0100 |
commit | e951075d2ef52fa8e9e7489c62031777c3a7e66b (patch) | |
tree | 64208c09a9162f3a99adccf30edc36de1ef884ef /packages/taler-wallet-core/src/util | |
parent | e975740ac4e9ba4bc531226784d640a018c00833 (diff) | |
download | wallet-core-e951075d2ef52fa8e9e7489c62031777c3a7e66b.tar.gz wallet-core-e951075d2ef52fa8e9e7489c62031777c3a7e66b.tar.bz2 wallet-core-e951075d2ef52fa8e9e7489c62031777c3a7e66b.zip |
wallet-core: flatten directory structure
Diffstat (limited to 'packages/taler-wallet-core/src/util')
-rw-r--r-- | packages/taler-wallet-core/src/util/coinSelection.ts | 4 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/query.ts | 831 |
2 files changed, 2 insertions, 833 deletions
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts index 0f6316bce..f33891c88 100644 --- a/packages/taler-wallet-core/src/util/coinSelection.ts +++ b/packages/taler-wallet-core/src/util/coinSelection.ts @@ -64,8 +64,8 @@ import { InternalWalletState } from "../internal-wallet-state.js"; import { getMerchantPaymentBalanceDetails, getPeerPaymentBalanceDetailsInTx, -} from "../operations/balance.js"; -import { getAutoRefreshExecuteThreshold } from "../operations/common.js"; +} from "../balance.js"; +import { getAutoRefreshExecuteThreshold } from "../common.js"; import { checkDbInvariant, checkLogicInvariant } from "./invariants.js"; const logger = new Logger("coinSelection.ts"); diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts deleted file mode 100644 index 90a3cac70..000000000 --- a/packages/taler-wallet-core/src/util/query.ts +++ /dev/null @@ -1,831 +0,0 @@ -/* - 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/> - */ - -/** - * Database query abstractions. - * @module Query - * @author Florian Dold - */ - -/** - * Imports. - */ -import { - IDBCursor, - IDBDatabase, - IDBFactory, - IDBKeyPath, - IDBKeyRange, - IDBRequest, - IDBTransaction, - IDBValidKey, - IDBVersionChangeEvent, -} from "@gnu-taler/idb-bridge"; -import { 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[]>; - 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[]>; - 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>, -): 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)); - } - 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> }, -): 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) { - const req = tx.objectStore(storeName).index(indexName).get(key); - return requestToPromise(req); - }, - iter(query) { - const req = tx - .objectStore(storeName) - .index(indexName) - .openCursor(query); - return new ResultStream<any>(req); - }, - getAll(query, count) { - const req = tx - .objectStore(storeName) - .index(indexName) - .getAll(query, count); - return requestToPromise(req); - }, - count(query) { - const req = tx.objectStore(storeName).index(indexName).count(query); - return requestToPromise(req); - }, - }; - } - ctx[storeAlias] = { - indexes, - get(key) { - const req = tx.objectStore(storeName).get(key); - return requestToPromise(req); - }, - getAll(query, count) { - const req = tx.objectStore(storeName).getAll(query, count); - return requestToPromise(req); - }, - iter(query) { - 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> }, -): 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) { - const req = tx.objectStore(storeName).index(indexName).get(key); - return requestToPromise(req); - }, - iter(query) { - const req = tx - .objectStore(storeName) - .index(indexName) - .openCursor(query); - return new ResultStream<any>(req); - }, - getAll(query, count) { - const req = tx - .objectStore(storeName) - .index(indexName) - .getAll(query, count); - return requestToPromise(req); - }, - count(query) { - const req = tx.objectStore(storeName).index(indexName).count(query); - return requestToPromise(req); - }, - }; - } - ctx[storeAlias] = { - indexes, - get(key) { - const req = tx.objectStore(storeName).get(key); - return requestToPromise(req); - }, - getAll(query, count) { - const req = tx.objectStore(storeName).getAll(query, count); - return requestToPromise(req); - }, - iter(query) { - const req = tx.objectStore(storeName).openCursor(query); - return new ResultStream<any>(req); - }, - async add(r, k) { - const req = tx.objectStore(storeName).add(r, k); - const key = await requestToPromise(req); - return { - key: key, - }; - }, - async put(r, k) { - const req = tx.objectStore(storeName).put(r, k); - const key = await requestToPromise(req); - return { - key: key, - }; - }, - delete(k) { - const req = tx.objectStore(storeName).delete(k); - return requestToPromise(req); - }, - }; - } - return ctx; -} - -/** - * Type-safe access to a database with a particular store map. - * - * A store map is the metadata that describes the store. - */ -export class DbAccess<StoreMap> { - constructor( - private db: IDBDatabase, - private stores: StoreMap, - ) {} - - idbHandle(): IDBDatabase { - return this.db; - } - - runAllStoresReadWriteTx<T>( - 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 tx = this.db.transaction(strStoreNames, "readwrite"); - const writeContext = makeWriteContext(tx, accessibleStores); - return runTx(tx, writeContext, txf); - } - - runAllStoresReadOnlyTx<T>( - 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 tx = this.db.transaction(strStoreNames, "readonly"); - const writeContext = makeReadContext(tx, accessibleStores); - return runTx(tx, writeContext, txf); - } - - runReadWriteTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>( - 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 storeNames) { - const swi = (this.stores as any)[sn] as StoreWithIndexes<any, any, any>; - strStoreNames.push(swi.storeName); - accessibleStores[swi.storeName] = swi; - } - const tx = this.db.transaction(strStoreNames, "readwrite"); - const writeContext = makeWriteContext(tx, accessibleStores); - return runTx(tx, writeContext, txf); - } - - runReadOnlyTx<T, StoreNameArray extends Array<StoreNames<StoreMap>>>( - 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 storeNames) { - const swi = (this.stores as any)[sn] as StoreWithIndexes<any, any, any>; - strStoreNames.push(swi.storeName); - accessibleStores[swi.storeName] = swi; - } - const tx = this.db.transaction(strStoreNames, "readonly"); - const readContext = makeReadContext(tx, accessibleStores); - return runTx(tx, readContext, txf); - } -} |