/* 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"; /** * Result of an inner join. */ export interface JoinResult { left: L; right: R; } /** * Result of a left outer join. */ export interface JoinLeftResult { left: L; right?: R; } /** * Definition of an object store. */ export class Store { 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 { 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 { 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( db: IDBDatabase, store: Store, key: any, ): Promise { 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( db: IDBDatabase, index: Index, key: any, ): Promise { 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( db: IDBDatabase, store: Store, value: T, key?: any, ): Promise { 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( req: 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: 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( db: IDBDatabase, store: Store, key: any, f: (x: T) => T | undefined, ): Promise { const tx = db.transaction([store.name], "readwrite"); const req = tx.objectStore(store.name).openCursor(key); await applyMutation(req, f); await transactionToPromise(tx); } type CursorResult = CursorEmptyResult | CursorValueResult; interface CursorEmptyResult { hasValue: false; } interface CursorValueResult { hasValue: true; value: T; } class ResultStream { private currentPromise: Promise; private gotCursorEnd: boolean = false; private awaitingResult: boolean = false; constructor(private req: 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 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 = 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( db: IDBDatabase, store: Store, ): ResultStream { const tx = db.transaction([store.name], "readonly"); const req = tx.objectStore(store.name).openCursor(); return new ResultStream(req); } export function oneShotIterIndex( db: IDBDatabase, index: Index, query?: any, ): ResultStream { const tx = db.transaction([index.storeName], "readonly"); const req = tx .objectStore(index.storeName) .index(index.indexName) .openCursor(query); return new ResultStream(req); } class TransactionHandle { constructor(private tx: 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); } iter(store: Store, key?: any): ResultStream { const req = this.tx.objectStore(store.name).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) { const req = this.tx.objectStore(store.name).openCursor(key); return applyMutation(req, f); } } export function runWithWriteTransaction( db: IDBDatabase, stores: Store[], f: (t: TransactionHandle) => Promise, ): 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, "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 { /** * 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; } /** * Exception that should be thrown by client code to abort a transaction. */ export const TransactionAbort = Symbol("transaction_abort");