/*
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 runWithReadTransaction(
db: IDBDatabase,
stores: Store[],
f: (t: TransactionHandle) => Promise,
): Promise {
return runWithTransaction(db, stores, f, "readonly");
}
export function runWithWriteTransaction(
db: IDBDatabase,
stores: Store[],
f: (t: TransactionHandle) => Promise,
): Promise {
return runWithTransaction(db, stores, f, "readwrite");
}
function runWithTransaction(
db: 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: 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");