summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/query.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/query.ts')
-rw-r--r--packages/taler-wallet-core/src/query.ts1004
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;
+ }
+}