wallet-core/packages/taler-wallet-core/src/util/query.ts

660 lines
17 KiB
TypeScript
Raw Normal View History

/*
This file is part of TALER
2016-01-05 14:20:13 +01:00
(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
2016-07-07 17:59:29 +02:00
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
2016-01-05 14:20:13 +01:00
/**
* Database query abstractions.
* @module Query
* @author Florian Dold
*/
2019-11-21 23:09:43 +01:00
/**
* Imports.
*/
import { openPromise } from "./promiseUtils";
import {
IDBRequest,
IDBTransaction,
IDBValidKey,
IDBDatabase,
IDBFactory,
IDBVersionChangeEvent,
Event,
IDBCursor,
} from "@gnu-taler/idb-bridge";
import { Logger } from "./logging";
const logger = new Logger("query.ts");
2019-11-21 23:09:43 +01:00
2020-04-06 20:02:01 +02:00
/**
* Exception that should be thrown by client code to abort a transaction.
*/
export const TransactionAbort = Symbol("transaction_abort");
2020-11-16 14:12:37 +01:00
export interface StoreParams<T> {
validator?: (v: T) => T;
autoIncrement?: boolean;
keyPath?: string | string[] | null;
/**
* Database version that this store was added in, or
* undefined if added in the first version.
*/
versionAdded?: number;
}
2017-05-28 01:10:54 +02:00
/**
* Definition of an object store.
*/
export class Store<N extends string, T> {
constructor(public name: N, public storeParams?: StoreParams<T>) {}
2016-10-18 01:16:31 +02:00
}
2017-11-30 04:07:36 +01:00
/**
* 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;
2020-11-16 14:12:37 +01:00
/**
* Database version that this store was added in, or
* undefined if added in the first version.
*/
versionAdded?: number;
2017-11-30 04:07:36 +01:00
}
function requestToPromise(req: IDBRequest): Promise<any> {
2019-12-03 00:52:15 +01:00
const stack = Error("Failed request was started here.");
return new Promise((resolve, reject) => {
req.onsuccess = () => {
resolve(req.result);
2017-11-30 04:07:36 +01:00
};
req.onerror = () => {
console.error("error in DB request", req.error);
reject(req.error);
console.error("Request failed:", stack);
};
});
2016-10-18 01:36:47 +02:00
}
function transactionToPromise(tx: IDBTransaction): Promise<void> {
2019-11-21 23:09:43 +01:00
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);
2019-11-21 23:09:43 +01:00
};
});
}
2019-11-20 20:02:48 +01:00
function applyMutation<T>(
req: IDBRequest,
2019-11-20 20:02:48 +01:00
f: (x: T) => T | undefined,
): Promise<void> {
return new Promise((resolve, reject) => {
req.onsuccess = () => {
const cursor = req.result;
if (cursor) {
2019-11-21 23:09:43 +01:00
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);
};
});
}
type CursorResult<T> = CursorEmptyResult<T> | CursorValueResult<T>;
interface CursorEmptyResult<T> {
hasValue: false;
2016-01-05 14:20:13 +01:00
}
interface CursorValueResult<T> {
hasValue: true;
value: T;
}
class ResultStream<T> {
private currentPromise: Promise<void>;
2020-04-06 17:45:41 +02:00
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();
2016-01-05 14:20:13 +01:00
}
};
req.onerror = () => {
p.reject(req.error);
};
2016-10-19 23:55:58 +02:00
}
async toArray(): Promise<T[]> {
const arr: T[] = [];
while (true) {
const x = await this.next();
if (x.hasValue) {
arr.push(x.value);
} else {
break;
2016-10-19 23:55:58 +02:00
}
}
return arr;
2016-01-05 14:20:13 +01:00
}
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;
2016-01-05 14:20:13 +01:00
}
}
return arr;
}
2019-12-16 12:53:22 +01:00
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;
}
}
2016-10-19 18:40:29 +02:00
}
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)) {
2019-11-20 20:02:48 +01:00
arr.push(x.value);
2016-10-19 18:40:29 +02:00
}
2016-01-05 17:11:01 +01:00
} else {
break;
2016-01-05 14:20:13 +01:00
}
}
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 };
}
}
export type AnyStoreMap = { [s: string]: Store<any, any> };
type StoreName<S> = S extends Store<infer N, any> ? N : never;
type StoreContent<S> = S extends Store<any, infer R> ? R : never;
type IndexRecord<Ind> = Ind extends Index<any, any, any, infer R> ? R : never;
2020-11-27 11:23:06 +01:00
type InferStore<S> = S extends Store<infer N, infer R> ? Store<N, R> : never;
type InferIndex<Ind> = Ind extends Index<
infer StN,
infer IndN,
infer KT,
infer RT
>
? Index<StN, IndN, KT, RT>
: never;
export class TransactionHandle<StoreTypes extends Store<string, any>> {
constructor(private tx: IDBTransaction) {}
put<S extends StoreTypes>(
store: S,
value: StoreContent<S>,
key?: any,
): Promise<any> {
const req = this.tx.objectStore(store.name).put(value, key);
return requestToPromise(req);
}
add<S extends StoreTypes>(
store: S,
value: StoreContent<S>,
key?: any,
): Promise<any> {
const req = this.tx.objectStore(store.name).add(value, key);
return requestToPromise(req);
2016-10-18 01:36:47 +02:00
}
get<S extends StoreTypes>(
store: S,
key: any,
): Promise<StoreContent<S> | undefined> {
const req = this.tx.objectStore(store.name).get(key);
return requestToPromise(req);
2016-11-18 04:09:04 +01:00
}
getIndexed<
2020-11-27 11:23:06 +01:00
St extends StoreTypes,
Ind extends Index<StoreName<St>, string, any, any>
>(index: InferIndex<Ind>, key: any): Promise<IndexRecord<Ind> | undefined> {
2019-12-03 00:52:15 +01:00
const req = this.tx
.objectStore(index.storeName)
.index(index.indexName)
.get(key);
return requestToPromise(req);
}
2020-11-27 11:23:06 +01:00
iter<St extends InferStore<StoreTypes>>(
store: St,
key?: any,
2020-11-27 11:23:06 +01:00
): ResultStream<StoreContent<St>> {
const req = this.tx.objectStore(store.name).openCursor(key);
2020-11-27 11:23:06 +01:00
return new ResultStream<StoreContent<St>>(req);
2016-01-05 14:20:13 +01:00
}
iterIndexed<
2020-11-27 11:23:06 +01:00
St extends InferStore<StoreTypes>,
Ind extends InferIndex<Index<StoreName<St>, string, any, any>>
>(index: Ind, key?: any): ResultStream<IndexRecord<Ind>> {
2020-03-30 12:39:32 +02:00
const req = this.tx
.objectStore(index.storeName)
.index(index.indexName)
.openCursor(key);
2020-11-27 11:23:06 +01:00
return new ResultStream<IndexRecord<Ind>>(req);
}
2020-11-27 11:23:06 +01:00
delete<St extends StoreTypes>(
store: InferStore<St>,
key: any,
): Promise<void> {
const req = this.tx.objectStore(store.name).delete(key);
return requestToPromise(req);
}
2020-11-27 11:23:06 +01:00
mutate<St extends StoreTypes>(
store: InferStore<St>,
2020-04-07 10:07:32 +02:00
key: any,
2020-11-27 11:23:06 +01:00
f: (x: StoreContent<St>) => StoreContent<St> | undefined,
2020-04-07 10:07:32 +02:00
): Promise<void> {
const req = this.tx.objectStore(store.name).openCursor(key);
return applyMutation(req, f);
}
}
function runWithTransaction<T, StoreTypes extends Store<string, {}>>(
db: IDBDatabase,
stores: StoreTypes[],
f: (t: TransactionHandle<StoreTypes>) => Promise<T>,
2019-12-02 17:35:47 +01:00
mode: "readonly" | "readwrite",
): Promise<T> {
2019-11-21 23:09:43 +01:00
const stack = Error("Failed transaction was started here.");
return new Promise((resolve, reject) => {
2020-03-30 12:39:32 +02:00
const storeName = stores.map((x) => x.name);
2019-12-02 17:35:47 +01:00
const tx = db.transaction(storeName, mode);
let funResult: any = undefined;
2020-04-06 17:45:41 +02:00
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);
2016-01-11 02:56:32 +01:00
};
2019-11-21 23:09:43 +01:00
tx.onerror = () => {
logger.error("error in transaction");
logger.error(`${stack}`);
2019-11-21 23:09:43 +01:00
};
tx.onabort = () => {
if (tx._error) {
logger.error("Transaction aborted with error:", tx._error);
2019-11-21 23:09:43 +01:00
} else {
logger.error("Trasaction aborted (no error)");
2019-11-21 23:09:43 +01:00
}
reject(TransactionAbort);
2016-01-11 02:56:32 +01:00
};
const th = new TransactionHandle(tx);
2020-03-12 08:52:46 +01:00
const resP = Promise.resolve().then(() => f(th));
2019-12-03 00:52:15 +01:00
resP
2020-03-30 12:39:32 +02:00
.then((result) => {
2019-12-03 00:52:15 +01:00
gotFunResult = true;
funResult = result;
})
2020-03-30 12:39:32 +02:00
.catch((e) => {
2019-12-03 00:52:15 +01:00
if (e == TransactionAbort) {
logger.trace("aborting transaction");
2019-12-03 00:52:15 +01:00
} else {
console.error("Transaction failed:", e);
console.error(stack);
2020-03-12 08:52:46 +01:00
tx.abort();
2019-12-03 00:52:15 +01:00
}
2020-03-30 12:39:32 +02:00
})
.catch((e) => {
2020-03-12 08:52:46 +01:00
console.error("fatal: aborting transaction failed", e);
2019-12-03 00:52:15 +01:00
});
});
}
/**
* Definition of an index.
*/
export class Index<
StoreName extends string,
IndexName extends string,
S extends IDBValidKey,
T
> {
/**
* Name of the store that this index is associated with.
*/
storeName: string;
2016-02-23 14:07:53 +01:00
/**
* Options to use for the index.
2016-02-23 14:07:53 +01:00
*/
options: IndexOptions;
2016-02-23 14:07:53 +01:00
constructor(
s: Store<StoreName, T>,
public indexName: IndexName,
public keyPath: string | string[],
options?: IndexOptions,
) {
const defaultOptions = {
multiEntry: false,
2016-02-23 14:07:53 +01:00
};
this.options = { ...defaultOptions, ...(options || {}) };
this.storeName = s.name;
}
2016-01-11 02:56:32 +01:00
/**
* We want to have the key type parameter in use somewhere,
* because otherwise the compiler complains. In iterIndex the
* key type is pretty useful.
2016-01-11 02:56:32 +01:00
*/
protected _dummyKey: S | undefined;
}
2016-01-11 02:56:32 +01:00
2019-12-12 22:39:45 +01:00
/**
* Return a promise that resolves to the opened IndexedDB database.
2019-12-12 22:39:45 +01:00
*/
export function openDatabase(
idbFactory: IDBFactory,
2019-12-12 22:39:45 +01:00
databaseName: string,
databaseVersion: number,
onVersionChange: () => void,
2020-03-30 12:39:32 +02:00
onUpgradeNeeded: (
db: IDBDatabase,
2020-03-30 12:39:32 +02:00
oldVersion: number,
newVersion: number,
2020-11-16 14:12:37 +01:00
upgradeTransaction: IDBTransaction,
2020-03-30 12:39:32 +02:00
) => void,
): Promise<IDBDatabase> {
return new Promise<IDBDatabase>((resolve, reject) => {
2019-12-12 22:39:45 +01:00
const req = idbFactory.open(databaseName, databaseVersion);
2020-03-30 12:39:32 +02:00
req.onerror = (e) => {
logger.error("database error", e);
2019-12-12 22:39:45 +01:00
reject(new Error("database error"));
};
2020-03-30 12:39:32 +02:00
req.onsuccess = (e) => {
req.result.onversionchange = (evt: IDBVersionChangeEvent) => {
logger.info(
2019-12-12 22:39:45 +01:00
`handling live db version change from ${evt.oldVersion} to ${evt.newVersion}`,
);
req.result.close();
onVersionChange();
};
resolve(req.result);
};
2020-03-30 12:39:32 +02:00
req.onupgradeneeded = (e) => {
2019-12-12 22:39:45 +01:00
const db = req.result;
2020-04-07 10:28:55 +02:00
const newVersion = e.newVersion;
if (!newVersion) {
throw Error("upgrade needed, but new version unknown");
}
2020-11-16 14:12:37 +01:00
const transaction = req.transaction;
if (!transaction) {
throw Error("no transaction handle available in upgrade handler");
}
onUpgradeNeeded(db, e.oldVersion, newVersion, transaction);
2019-12-12 22:39:45 +01:00
};
});
}
export class Database<StoreMap extends AnyStoreMap> {
constructor(private db: IDBDatabase, stores: StoreMap) {}
2019-12-12 22:39:45 +01:00
static deleteDatabase(idbFactory: IDBFactory, dbName: string): void {
2019-12-12 22:39:45 +01:00
idbFactory.deleteDatabase(dbName);
}
async exportDatabase(): Promise<any> {
const db = this.db;
const dump = {
name: db.name,
stores: {} as { [s: string]: any },
version: db.version,
};
2020-03-30 12:39:32 +02:00
2019-12-12 22:39:45 +01:00
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: Event) => {
2019-12-12 22:39:45 +01:00
const cursor = (e.target as any).result;
if (cursor) {
storeDump[cursor.key] = cursor.value;
cursor.continue();
}
});
}
});
}
importDatabase(dump: any): Promise<void> {
const db = this.db;
logger.info("importing db", dump);
2019-12-12 22:39:45 +01:00
return new Promise<void>((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]);
}
logger.info(`importing ${objects.length} records into ${storeName}`);
2019-12-12 22:39:45 +01:00
const store = tx.objectStore(storeName);
for (const obj of objects) {
store.put(obj);
}
}
}
tx.addEventListener("complete", () => {
resolve();
});
});
}
2020-03-30 12:39:32 +02:00
async get<N extends keyof StoreMap, S extends StoreMap[N]>(
store: S,
2020-12-14 16:45:15 +01:00
key: IDBValidKey,
): Promise<StoreContent<S> | undefined> {
2019-12-12 22:39:45 +01:00
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<Ind extends Index<string, string, any, any>>(
index: Ind,
2020-12-14 16:45:15 +01:00
key: IDBValidKey,
): Promise<IndexRecord<Ind> | undefined> {
2019-12-12 22:39:45 +01:00
const tx = this.db.transaction([index.storeName], "readonly");
2020-03-30 12:39:32 +02:00
const req = tx.objectStore(index.storeName).index(index.indexName).get(key);
2019-12-12 22:39:45 +01:00
const v = await requestToPromise(req);
await transactionToPromise(tx);
return v;
}
async put<St extends Store<string, any>>(
2020-12-02 14:55:04 +01:00
store: St,
value: StoreContent<St>,
2020-12-14 16:45:15 +01:00
key?: IDBValidKey,
): Promise<any> {
2019-12-12 22:39:45 +01:00
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<N extends string, T>(
store: Store<N, T>,
2020-12-14 16:45:15 +01:00
key: IDBValidKey,
2019-12-12 22:39:45 +01:00
f: (x: T) => T | undefined,
): Promise<void> {
const tx = this.db.transaction([store.name], "readwrite");
const req = tx.objectStore(store.name).openCursor(key);
await applyMutation(req, f);
await transactionToPromise(tx);
}
iter<N extends string, T>(store: Store<N, T>): ResultStream<T> {
2019-12-12 22:39:45 +01:00
const tx = this.db.transaction([store.name], "readonly");
const req = tx.objectStore(store.name).openCursor();
return new ResultStream<T>(req);
}
2020-11-27 11:23:06 +01:00
iterIndex<Ind extends Index<string, string, any, any>>(
index: InferIndex<Ind>,
2019-12-12 22:39:45 +01:00
query?: any,
2020-11-27 11:23:06 +01:00
): ResultStream<IndexRecord<Ind>> {
2019-12-12 22:39:45 +01:00
const tx = this.db.transaction([index.storeName], "readonly");
const req = tx
.objectStore(index.storeName)
.index(index.indexName)
.openCursor(query);
2020-11-27 11:23:06 +01:00
return new ResultStream<IndexRecord<Ind>>(req);
2019-12-12 22:39:45 +01:00
}
async runWithReadTransaction<
T,
N extends keyof StoreMap,
StoreTypes extends StoreMap[N]
>(
stores: StoreTypes[],
f: (t: TransactionHandle<StoreTypes>) => Promise<T>,
2019-12-12 22:39:45 +01:00
): Promise<T> {
return runWithTransaction<T, StoreTypes>(this.db, stores, f, "readonly");
2019-12-12 22:39:45 +01:00
}
async runWithWriteTransaction<
T,
N extends keyof StoreMap,
StoreTypes extends StoreMap[N]
>(
stores: StoreTypes[],
f: (t: TransactionHandle<StoreTypes>) => Promise<T>,
2019-12-12 22:39:45 +01:00
): Promise<T> {
return runWithTransaction<T, StoreTypes>(this.db, stores, f, "readwrite");
2019-12-12 22:39:45 +01:00
}
}