From ffd2a62c3f7df94365980302fef3bc3376b48182 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 3 Aug 2020 13:00:48 +0530 Subject: modularize repo, use pnpm, improve typechecking --- packages/taler-wallet-core/src/util/query.ts | 576 +++++++++++++++++++++++++++ 1 file changed, 576 insertions(+) create mode 100644 packages/taler-wallet-core/src/util/query.ts (limited to 'packages/taler-wallet-core/src/util/query.ts') diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts new file mode 100644 index 000000000..53359752e --- /dev/null +++ b/packages/taler-wallet-core/src/util/query.ts @@ -0,0 +1,576 @@ +/* + 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 + */ + +/** + * Database query abstractions. + * @module Query + * @author Florian Dold + */ + +/** + * Imports. + */ +import { openPromise } from "./promiseUtils"; +import type { idbtypes } from "idb-bridge"; + +/** + * Exception that should be thrown by client code to abort a transaction. + */ +export const TransactionAbort = Symbol("transaction_abort"); + +/** + * Definition of an object store. + */ +export class Store { + constructor( + public name: string, + public storeParams?: idbtypes.IDBObjectStoreParameters, + public validator?: (v: T) => T, + ) {} +} + +/** + * 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; +} + +function requestToPromise(req: idbtypes.IDBRequest): Promise { + const stack = Error("Failed request was started here."); + return new Promise((resolve, reject) => { + req.onsuccess = () => { + resolve(req.result); + }; + req.onerror = () => { + console.log("error in DB request", req.error); + reject(req.error); + console.log("Request failed:", stack); + }; + }); +} + +function transactionToPromise(tx: idbtypes.IDBTransaction): Promise { + const stack = Error("Failed transaction was started here."); + return new Promise((resolve, reject) => { + tx.onabort = () => { + reject(TransactionAbort); + }; + tx.oncomplete = () => { + resolve(); + }; + tx.onerror = () => { + console.error("Transaction failed:", stack); + reject(tx.error); + }; + }); +} + +function applyMutation( + req: idbtypes.IDBRequest, + f: (x: T) => T | undefined, +): Promise { + return new Promise((resolve, reject) => { + req.onsuccess = () => { + const cursor = req.result; + if (cursor) { + const val = cursor.value; + const modVal = f(val); + if (modVal !== undefined && modVal !== null) { + const req2: idbtypes.IDBRequest = cursor.update(modVal); + req2.onerror = () => { + reject(req2.error); + }; + req2.onsuccess = () => { + cursor.continue(); + }; + } else { + cursor.continue(); + } + } else { + resolve(); + } + }; + req.onerror = () => { + reject(req.error); + }; + }); +} + +type CursorResult = CursorEmptyResult | CursorValueResult; + +interface CursorEmptyResult { + hasValue: false; +} + +interface CursorValueResult { + hasValue: true; + value: T; +} + +class ResultStream { + private currentPromise: Promise; + private gotCursorEnd = false; + private awaitingResult = false; + + constructor(private req: idbtypes.IDBRequest) { + this.awaitingResult = true; + let p = openPromise(); + 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(); + this.currentPromise = p.promise; + } else { + this.gotCursorEnd = true; + p.resolve(); + } + }; + req.onerror = () => { + p.reject(req.error); + }; + } + + async toArray(): Promise { + const arr: T[] = []; + while (true) { + const x = await this.next(); + if (x.hasValue) { + arr.push(x.value); + } else { + break; + } + } + return arr; + } + + async map(f: (x: T) => R): Promise { + const arr: R[] = []; + while (true) { + const x = await this.next(); + if (x.hasValue) { + arr.push(f(x.value)); + } else { + break; + } + } + return arr; + } + + async forEachAsync(f: (x: T) => Promise): Promise { + while (true) { + const x = await this.next(); + if (x.hasValue) { + await f(x.value); + } else { + break; + } + } + } + + async forEach(f: (x: T) => void): Promise { + while (true) { + const x = await this.next(); + if (x.hasValue) { + f(x.value); + } else { + break; + } + } + } + + async filter(f: (x: T) => boolean): Promise { + 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> { + if (this.gotCursorEnd) { + return { hasValue: false }; + } + if (!this.awaitingResult) { + const cursor: idbtypes.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 }; + } +} + +export class TransactionHandle { + constructor(private tx: idbtypes.IDBTransaction) {} + + put(store: Store, value: T, key?: any): Promise { + const req = this.tx.objectStore(store.name).put(value, key); + return requestToPromise(req); + } + + add(store: Store, value: T, key?: any): Promise { + const req = this.tx.objectStore(store.name).add(value, key); + return requestToPromise(req); + } + + get(store: Store, key: any): Promise { + const req = this.tx.objectStore(store.name).get(key); + return requestToPromise(req); + } + + getIndexed( + index: Index, + key: any, + ): Promise { + const req = this.tx + .objectStore(index.storeName) + .index(index.indexName) + .get(key); + return requestToPromise(req); + } + + iter(store: Store, key?: any): ResultStream { + const req = this.tx.objectStore(store.name).openCursor(key); + return new ResultStream(req); + } + + iterIndexed( + index: Index, + key?: any, + ): ResultStream { + const req = this.tx + .objectStore(index.storeName) + .index(index.indexName) + .openCursor(key); + return new ResultStream(req); + } + + delete(store: Store, key: any): Promise { + const req = this.tx.objectStore(store.name).delete(key); + return requestToPromise(req); + } + + mutate( + store: Store, + key: any, + f: (x: T) => T | undefined, + ): Promise { + const req = this.tx.objectStore(store.name).openCursor(key); + return applyMutation(req, f); + } +} + +function runWithTransaction( + db: idbtypes.IDBDatabase, + stores: Store[], + f: (t: TransactionHandle) => Promise, + mode: "readonly" | "readwrite", +): Promise { + const stack = Error("Failed transaction was started here."); + return new Promise((resolve, reject) => { + const storeName = stores.map((x) => x.name); + const tx = db.transaction(storeName, mode); + let funResult: any = undefined; + let gotFunResult = false; + 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"; + console.error(msg); + reject(Error(msg)); + } + resolve(funResult); + }; + tx.onerror = () => { + console.error("error in transaction"); + console.error(stack); + }; + tx.onabort = () => { + if (tx.error) { + console.error("Transaction aborted with error:", tx.error); + } else { + console.log("Trasaction aborted (no error)"); + } + reject(TransactionAbort); + }; + const th = new TransactionHandle(tx); + const resP = Promise.resolve().then(() => f(th)); + resP + .then((result) => { + gotFunResult = true; + funResult = result; + }) + .catch((e) => { + if (e == TransactionAbort) { + console.info("aborting transaction"); + } else { + console.error("Transaction failed:", e); + console.error(stack); + tx.abort(); + } + }) + .catch((e) => { + console.error("fatal: aborting transaction failed", e); + }); + }); +} + +/** + * Definition of an index. + */ +export class Index { + /** + * Name of the store that this index is associated with. + */ + storeName: string; + + /** + * Options to use for the index. + */ + options: IndexOptions; + + constructor( + s: Store, + public indexName: string, + public keyPath: string | string[], + options?: IndexOptions, + ) { + const defaultOptions = { + multiEntry: false, + }; + this.options = { ...defaultOptions, ...(options || {}) }; + this.storeName = s.name; + } + + /** + * We want to have the key type parameter in use somewhere, + * because otherwise the compiler complains. In iterIndex the + * key type is pretty useful. + */ + protected _dummyKey: S | undefined; +} + +/** + * Return a promise that resolves + * to the taler wallet db. + */ +export function openDatabase( + idbFactory: idbtypes.IDBFactory, + databaseName: string, + databaseVersion: number, + onVersionChange: () => void, + onUpgradeNeeded: ( + db: idbtypes.IDBDatabase, + oldVersion: number, + newVersion: number, + ) => void, +): Promise { + return new Promise((resolve, reject) => { + const req = idbFactory.open(databaseName, databaseVersion); + req.onerror = (e) => { + console.log("taler database error", e); + reject(new Error("database error")); + }; + req.onsuccess = (e) => { + req.result.onversionchange = (evt: idbtypes.IDBVersionChangeEvent) => { + console.log( + `handling live db version change 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) { + throw Error("upgrade needed, but new version unknown"); + } + onUpgradeNeeded(db, e.oldVersion, newVersion); + }; + }); +} + +export class Database { + constructor(private db: idbtypes.IDBDatabase) {} + + static deleteDatabase(idbFactory: idbtypes.IDBFactory, dbName: string): void { + idbFactory.deleteDatabase(dbName); + } + + async exportDatabase(): Promise { + const db = this.db; + const dump = { + name: db.name, + stores: {} as { [s: string]: any }, + version: db.version, + }; + + return new Promise((resolve, reject) => { + const tx = db.transaction(Array.from(db.objectStoreNames)); + tx.addEventListener("complete", () => { + resolve(dump); + }); + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < db.objectStoreNames.length; i++) { + const name = db.objectStoreNames[i]; + const storeDump = {} as { [s: string]: any }; + dump.stores[name] = storeDump; + tx.objectStore(name) + .openCursor() + .addEventListener("success", (e: idbtypes.Event) => { + const cursor = (e.target as any).result; + if (cursor) { + storeDump[cursor.key] = cursor.value; + cursor.continue(); + } + }); + } + }); + } + + importDatabase(dump: any): Promise { + const db = this.db; + console.log("importing db", dump); + return new Promise((resolve, reject) => { + const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); + if (dump.stores) { + for (const storeName in dump.stores) { + const objects = []; + const dumpStore = dump.stores[storeName]; + for (const key in dumpStore) { + objects.push(dumpStore[key]); + } + console.log(`importing ${objects.length} records into ${storeName}`); + const store = tx.objectStore(storeName); + for (const obj of objects) { + store.put(obj); + } + } + } + tx.addEventListener("complete", () => { + resolve(); + }); + }); + } + + async get(store: Store, key: any): Promise { + const tx = this.db.transaction([store.name], "readonly"); + const req = tx.objectStore(store.name).get(key); + const v = await requestToPromise(req); + await transactionToPromise(tx); + return v; + } + + async getIndexed( + index: Index, + key: any, + ): Promise { + const tx = this.db.transaction([index.storeName], "readonly"); + const req = tx.objectStore(index.storeName).index(index.indexName).get(key); + const v = await requestToPromise(req); + await transactionToPromise(tx); + return v; + } + + async put(store: Store, value: T, key?: any): Promise { + const tx = this.db.transaction([store.name], "readwrite"); + const req = tx.objectStore(store.name).put(value, key); + const v = await requestToPromise(req); + await transactionToPromise(tx); + return v; + } + + async mutate( + store: Store, + key: any, + f: (x: T) => T | undefined, + ): Promise { + const tx = this.db.transaction([store.name], "readwrite"); + const req = tx.objectStore(store.name).openCursor(key); + await applyMutation(req, f); + await transactionToPromise(tx); + } + + iter(store: Store): ResultStream { + const tx = this.db.transaction([store.name], "readonly"); + const req = tx.objectStore(store.name).openCursor(); + return new ResultStream(req); + } + + iterIndex( + index: Index, + query?: any, + ): ResultStream { + const tx = this.db.transaction([index.storeName], "readonly"); + const req = tx + .objectStore(index.storeName) + .index(index.indexName) + .openCursor(query); + return new ResultStream(req); + } + + async runWithReadTransaction( + stores: Store[], + f: (t: TransactionHandle) => Promise, + ): Promise { + return runWithTransaction(this.db, stores, f, "readonly"); + } + + async runWithWriteTransaction( + stores: Store[], + f: (t: TransactionHandle) => Promise, + ): Promise { + return runWithTransaction(this.db, stores, f, "readwrite"); + } +} -- cgit v1.2.3