aboutsummaryrefslogtreecommitdiff
path: root/src/util/query.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/util/query.ts')
-rw-r--r--src/util/query.ts446
1 files changed, 446 insertions, 0 deletions
diff --git a/src/util/query.ts b/src/util/query.ts
new file mode 100644
index 000000000..5726bcaa6
--- /dev/null
+++ b/src/util/query.ts
@@ -0,0 +1,446 @@
+/*
+ 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 { openPromise } from "./promiseUtils";
+
+
+/**
+ * Result of an inner join.
+ */
+export interface JoinResult<L, R> {
+ left: L;
+ right: R;
+}
+
+/**
+ * Result of a left outer join.
+ */
+export interface JoinLeftResult<L, R> {
+ left: L;
+ right?: R;
+}
+
+/**
+ * Definition of an object store.
+ */
+export class Store<T> {
+ constructor(
+ public name: string,
+ public storeParams?: 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: IDBRequest): Promise<any> {
+ 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: IDBTransaction): Promise<void> {
+ 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);
+ };
+ });
+}
+
+export async function oneShotGet<T>(
+ db: IDBDatabase,
+ store: Store<T>,
+ key: any,
+): Promise<T | undefined> {
+ const tx = db.transaction([store.name], "readonly");
+ const req = tx.objectStore(store.name).get(key);
+ const v = await requestToPromise(req)
+ await transactionToPromise(tx);
+ return v;
+}
+
+export async function oneShotGetIndexed<S extends IDBValidKey, T>(
+ db: IDBDatabase,
+ index: Index<S, T>,
+ key: any,
+): Promise<T | undefined> {
+ const tx = 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;
+}
+
+export async function oneShotPut<T>(
+ db: IDBDatabase,
+ store: Store<T>,
+ value: T,
+ key?: any,
+): Promise<any> {
+ const tx = db.transaction([store.name], "readwrite");
+ const req = tx.objectStore(store.name).put(value, key);
+ const v = await requestToPromise(req);
+ await transactionToPromise(tx);
+ return v;
+}
+
+function applyMutation<T>(
+ req: IDBRequest,
+ f: (x: T) => T | undefined,
+): Promise<void> {
+ 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: IDBRequest = cursor.update(modVal);
+ req2.onerror = () => {
+ reject(req2.error);
+ };
+ req2.onsuccess = () => {
+ cursor.continue();
+ };
+ } else {
+ cursor.continue();
+ }
+ } else {
+ resolve();
+ }
+ };
+ req.onerror = () => {
+ reject(req.error);
+ };
+ });
+}
+
+export async function oneShotMutate<T>(
+ db: IDBDatabase,
+ store: Store<T>,
+ key: any,
+ f: (x: T) => T | undefined,
+): Promise<void> {
+ const tx = db.transaction([store.name], "readwrite");
+ const req = tx.objectStore(store.name).openCursor(key);
+ await applyMutation(req, f);
+ await transactionToPromise(tx);
+}
+
+type CursorResult<T> = CursorEmptyResult<T> | CursorValueResult<T>;
+
+interface CursorEmptyResult<T> {
+ hasValue: false;
+}
+
+interface CursorValueResult<T> {
+ hasValue: true;
+ value: T;
+}
+
+class ResultStream<T> {
+ private currentPromise: Promise<void>;
+ private gotCursorEnd: boolean = false;
+ private awaitingResult: boolean = 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 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 = 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 function oneShotIter<T>(
+ db: IDBDatabase,
+ store: Store<T>,
+): ResultStream<T> {
+ const tx = db.transaction([store.name], "readonly");
+ const req = tx.objectStore(store.name).openCursor();
+ return new ResultStream<T>(req);
+}
+
+export function oneShotIterIndex<S extends IDBValidKey, T>(
+ db: IDBDatabase,
+ index: Index<S, T>,
+ query?: any,
+): ResultStream<T> {
+ const tx = db.transaction([index.storeName], "readonly");
+ const req = tx
+ .objectStore(index.storeName)
+ .index(index.indexName)
+ .openCursor(query);
+ return new ResultStream<T>(req);
+}
+
+class TransactionHandle {
+ constructor(private tx: IDBTransaction) {}
+
+ put<T>(store: Store<T>, value: T, key?: any): Promise<any> {
+ const req = this.tx.objectStore(store.name).put(value, key);
+ return requestToPromise(req);
+ }
+
+ add<T>(store: Store<T>, value: T, key?: any): Promise<any> {
+ const req = this.tx.objectStore(store.name).add(value, key);
+ return requestToPromise(req);
+ }
+
+ get<T>(store: Store<T>, key: any): Promise<T | undefined> {
+ const req = this.tx.objectStore(store.name).get(key);
+ return requestToPromise(req);
+ }
+
+ iter<T>(store: Store<T>, key?: any): ResultStream<T> {
+ const req = this.tx.objectStore(store.name).openCursor(key);
+ return new ResultStream<T>(req);
+ }
+
+ delete<T>(store: Store<T>, key: any): Promise<void> {
+ const req = this.tx.objectStore(store.name).delete(key);
+ return requestToPromise(req);
+ }
+
+ mutate<T>(store: Store<T>, key: any, f: (x: T) => T | undefined) {
+ const req = this.tx.objectStore(store.name).openCursor(key);
+ return applyMutation(req, f);
+ }
+}
+
+export function runWithWriteTransaction<T>(
+ db: IDBDatabase,
+ stores: Store<any>[],
+ f: (t: TransactionHandle) => Promise<T>,
+): Promise<T> {
+ 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, "readwrite");
+ let funResult: any = undefined;
+ let gotFunResult: boolean = 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");
+ };
+ 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 = f(th);
+ resP.then(result => {
+ gotFunResult = true;
+ funResult = result;
+ }).catch((e) => {
+ if (e == TransactionAbort) {
+ console.info("aborting transaction");
+ } else {
+ tx.abort();
+ console.error("Transaction failed:", e);
+ console.error(stack);
+ }
+ });
+ });
+}
+
+/**
+ * Definition of an index.
+ */
+export class Index<S extends IDBValidKey, T> {
+ /**
+ * Name of the store that this index is associated with.
+ */
+ storeName: string;
+
+ /**
+ * Options to use for the index.
+ */
+ options: IndexOptions;
+
+ constructor(
+ s: Store<T>,
+ 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;
+}
+
+/**
+ * Exception that should be thrown by client code to abort a transaction.
+ */
+export const TransactionAbort = Symbol("transaction_abort");