wallet-core/lib/wallet/query.ts

456 lines
11 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
*/
"use strict";
2016-01-05 17:11:01 +01:00
2016-10-18 01:16:31 +02:00
export class Store<T> {
name: string;
validator?: (v: T) => T;
constructor(name: string, validator?: (v: T) => T) {
this.name = name;
this.validator = validator;
}
}
2016-10-18 01:36:47 +02:00
export class Index<S,T> {
indexName: string;
storeName: string;
constructor(s: Store<T>, indexName: string) {
this.storeName = s.name;
this.indexName = indexName;
}
}
2016-01-11 02:56:32 +01:00
/**
* Stream that can be filtered, reduced or joined
* with indices.
*/
export interface QueryStream<T> {
2016-10-18 01:36:47 +02:00
indexJoin<S,I>(index: Index<I,S>,
keyFn: (obj: T) => I): QueryStream<[T, S]>;
2016-09-12 17:41:12 +02:00
filter(f: (x: any) => boolean): QueryStream<T>;
reduce<S>(f: (v: T, acc: S) => S, start?: S): Promise<S>;
2016-09-12 17:41:12 +02:00
flatMap(f: (x: T) => T[]): QueryStream<T>;
2016-10-12 02:55:53 +02:00
toArray(): Promise<T[]>;
2016-01-11 02:56:32 +01:00
}
2016-01-05 14:20:13 +01:00
2016-01-11 02:56:32 +01:00
/**
* Get an unresolved promise together with its extracted resolve / reject
* function.
*/
function openPromise<T>() {
2016-09-12 17:41:12 +02:00
let resolve: ((value?: T | PromiseLike<T>) => void) | null = null;
let reject: ((reason?: any) => void) | null = null;
2016-01-11 02:56:32 +01:00
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
2016-09-12 17:41:12 +02:00
if (!(resolve && reject)) {
// Never happens, unless JS implementation is broken
throw Error();
}
2016-10-18 01:36:47 +02:00
return {resolve, reject, promise};
2016-01-11 02:56:32 +01:00
}
abstract class QueryStreamBase<T> implements QueryStream<T> {
abstract subscribe(f: (isDone: boolean,
2016-10-18 01:36:47 +02:00
value: any,
tx: IDBTransaction) => void): void;
2016-01-05 17:11:01 +01:00
2016-01-05 14:20:13 +01:00
root: QueryRoot;
constructor(root: QueryRoot) {
this.root = root;
}
2016-01-05 14:20:13 +01:00
2016-09-12 17:41:12 +02:00
flatMap(f: (x: T) => T[]): QueryStream<T> {
return new QueryStreamFlatMap(this, f);
}
2016-10-18 01:36:47 +02:00
indexJoin<S,I>(index: Index<I,S>,
keyFn: (obj: T) => I): QueryStream<[T, S]> {
this.root.addStoreAccess(index.storeName, false);
return new QueryStreamIndexJoin(this, index.storeName, index.indexName, keyFn);
2016-01-05 14:20:13 +01:00
}
2016-09-12 17:41:12 +02:00
filter(f: (x: any) => boolean): QueryStream<T> {
2016-01-05 14:20:13 +01:00
return new QueryStreamFilter(this, f);
}
2016-01-05 14:20:13 +01:00
2016-10-12 02:55:53 +02:00
toArray(): Promise<T[]> {
let {resolve, promise} = openPromise();
let values: T[] = [];
this.subscribe((isDone, value) => {
if (isDone) {
resolve(values);
return;
}
values.push(value);
});
return Promise.resolve()
2016-10-18 01:36:47 +02:00
.then(() => this.root.finish())
.then(() => promise);
2016-10-12 02:55:53 +02:00
}
2016-09-12 17:41:12 +02:00
reduce<A>(f: (x: any, acc?: A) => A, init?: A): Promise<any> {
let {resolve, promise} = openPromise();
let acc = init;
2016-01-05 14:20:13 +01:00
this.subscribe((isDone, value) => {
if (isDone) {
2016-09-12 17:41:12 +02:00
resolve(acc);
2016-01-05 14:20:13 +01:00
return;
}
acc = f(value, acc);
});
2016-01-11 02:56:32 +01:00
return Promise.resolve()
2016-10-18 01:36:47 +02:00
.then(() => this.root.finish())
.then(() => promise);
2016-01-05 14:20:13 +01:00
}
}
2016-09-12 17:41:12 +02:00
type FilterFn = (e: any) => boolean;
type SubscribeFn = (done: boolean, value: any, tx: IDBTransaction) => void;
interface FlatMapFn<T> {
(v: T): T[];
}
2016-01-05 14:20:13 +01:00
2016-01-11 02:56:32 +01:00
class QueryStreamFilter<T> extends QueryStreamBase<T> {
s: QueryStreamBase<T>;
2016-09-12 17:41:12 +02:00
filterFn: FilterFn;
2016-01-05 14:20:13 +01:00
2016-09-12 17:41:12 +02:00
constructor(s: QueryStreamBase<T>, filterFn: FilterFn) {
2016-01-05 14:20:13 +01:00
super(s.root);
this.s = s;
this.filterFn = filterFn;
}
2016-09-12 17:41:12 +02:00
subscribe(f: SubscribeFn) {
2016-01-11 02:56:32 +01:00
this.s.subscribe((isDone, value, tx) => {
2016-01-05 14:20:13 +01:00
if (isDone) {
2016-01-11 02:56:32 +01:00
f(true, undefined, tx);
2016-01-05 14:20:13 +01:00
return;
}
if (this.filterFn(value)) {
f(false, value, tx);
}
});
}
}
class QueryStreamFlatMap<T> extends QueryStreamBase<T> {
s: QueryStreamBase<T>;
2016-09-12 17:41:12 +02:00
flatMapFn: (v: T) => T[];
2016-09-12 17:41:12 +02:00
constructor(s: QueryStreamBase<T>, flatMapFn: (v: T) => T[]) {
super(s.root);
this.s = s;
2016-09-12 17:41:12 +02:00
this.flatMapFn = flatMapFn;
}
2016-09-12 17:41:12 +02:00
subscribe(f: SubscribeFn) {
this.s.subscribe((isDone, value, tx) => {
if (isDone) {
f(true, undefined, tx);
return;
}
let values = this.flatMapFn(value);
for (let v in values) {
2016-01-11 02:56:32 +01:00
f(false, value, tx)
2016-01-05 14:20:13 +01:00
}
});
}
}
2016-10-12 02:55:53 +02:00
class QueryStreamIndexJoin<T, S> extends QueryStreamBase<[T, S]> {
2016-01-11 02:56:32 +01:00
s: QueryStreamBase<T>;
2016-09-12 17:41:12 +02:00
storeName: string;
key: any;
indexName: string;
2016-01-05 17:11:01 +01:00
2016-10-18 01:36:47 +02:00
constructor(s: QueryStreamBase<T>, storeName: string, indexName: string,
key: any) {
2016-01-05 14:20:13 +01:00
super(s.root);
this.s = s;
this.storeName = storeName;
this.key = key;
2016-01-05 17:11:01 +01:00
this.indexName = indexName;
2016-01-05 14:20:13 +01:00
}
2016-09-12 17:41:12 +02:00
subscribe(f: SubscribeFn) {
2016-01-11 02:56:32 +01:00
this.s.subscribe((isDone, value, tx) => {
2016-01-05 14:20:13 +01:00
if (isDone) {
2016-01-11 02:56:32 +01:00
f(true, undefined, tx);
2016-01-05 14:20:13 +01:00
return;
}
console.log("joining on", this.key(value));
2016-01-11 02:56:32 +01:00
let s = tx.objectStore(this.storeName).index(this.indexName);
2016-01-05 17:11:01 +01:00
let req = s.openCursor(IDBKeyRange.only(this.key(value)));
2016-01-05 14:20:13 +01:00
req.onsuccess = () => {
let cursor = req.result;
if (cursor) {
2016-01-11 02:56:32 +01:00
f(false, [value, cursor.value], tx);
2016-01-05 14:20:13 +01:00
cursor.continue();
} else {
2016-01-11 02:56:32 +01:00
f(true, undefined, tx);
2016-01-05 14:20:13 +01:00
}
}
});
}
}
2016-01-11 02:56:32 +01:00
class IterQueryStream<T> extends QueryStreamBase<T> {
2016-09-12 17:41:12 +02:00
private storeName: string;
private options: any;
private subscribers: SubscribeFn[];
2016-01-05 14:20:13 +01:00
2016-09-12 17:41:12 +02:00
constructor(qr: QueryRoot, storeName: string, options: any) {
2016-01-05 14:20:13 +01:00
super(qr);
this.options = options;
this.storeName = storeName;
this.subscribers = [];
2016-01-05 14:20:13 +01:00
2016-09-12 17:41:12 +02:00
let doIt = (tx: IDBTransaction) => {
const {indexName = void 0, only = void 0} = this.options;
2016-09-12 17:41:12 +02:00
let s: any;
if (indexName !== void 0) {
2016-01-11 02:56:32 +01:00
s = tx.objectStore(this.storeName)
2016-10-18 01:36:47 +02:00
.index(this.options.indexName);
2016-01-05 17:11:01 +01:00
} else {
2016-01-11 02:56:32 +01:00
s = tx.objectStore(this.storeName);
2016-01-05 17:11:01 +01:00
}
2016-10-12 02:55:53 +02:00
let kr: IDBKeyRange | undefined = undefined;
2016-09-12 17:41:12 +02:00
if (only !== undefined) {
2016-01-05 14:20:13 +01:00
kr = IDBKeyRange.only(this.options.only);
}
let req = s.openCursor(kr);
2016-09-12 17:41:12 +02:00
req.onsuccess = () => {
let cursor: IDBCursorWithValue = req.result;
if (cursor) {
for (let f of this.subscribers) {
f(false, cursor.value, tx);
}
cursor.continue();
} else {
for (let f of this.subscribers) {
f(true, undefined, tx);
}
}
}
2016-01-11 02:56:32 +01:00
};
2016-01-05 17:11:01 +01:00
2016-09-12 17:41:12 +02:00
this.root.addWork(doIt);
}
2016-09-12 17:41:12 +02:00
subscribe(f: SubscribeFn) {
this.subscribers.push(f);
}
}
2016-10-13 02:23:24 +02:00
export class QueryRoot {
2016-09-12 17:41:12 +02:00
private work: ((t: IDBTransaction) => void)[] = [];
2016-01-11 02:56:32 +01:00
private db: IDBDatabase;
private stores = new Set();
2016-09-12 17:41:12 +02:00
private kickoffPromise: Promise<void>;
2016-01-11 02:56:32 +01:00
/**
* Some operations is a write operation,
* and we need to do a "readwrite" transaction/
*/
2016-09-12 17:41:12 +02:00
private hasWrite: boolean;
2016-09-12 17:41:12 +02:00
constructor(db: IDBDatabase) {
this.db = db;
}
2016-10-18 01:36:47 +02:00
iter<T>(store: Store<T>): QueryStream<T> {
2016-10-18 01:16:31 +02:00
this.stores.add(store.name);
2016-10-18 01:36:47 +02:00
return new IterQueryStream(this, store.name, {});
}
iterIndex<S,T>(index: Index<S,T>, only?: S): QueryStream<T> {
this.stores.add(index.storeName);
return new IterQueryStream(this, index.storeName, {
only,
indexName: index.indexName
});
2016-01-05 14:20:13 +01:00
}
2016-01-11 02:56:32 +01:00
/**
* Put an object into the given object store.
* Overrides if an existing object with the same key exists
* in the store.
*/
2016-10-18 01:16:31 +02:00
put<T>(store: Store<T>, val: T): QueryRoot {
2016-01-11 02:56:32 +01:00
let doPut = (tx: IDBTransaction) => {
2016-10-18 01:16:31 +02:00
tx.objectStore(store.name).put(val);
2016-01-11 02:56:32 +01:00
};
2016-10-18 01:16:31 +02:00
this.addWork(doPut, store.name, true);
return this;
}
2016-01-11 02:56:32 +01:00
/**
* Add all object from an iterable to the given object store.
* Fails if the object's key is already present
* in the object store.
*/
2016-10-18 01:16:31 +02:00
putAll<T>(store: Store<T>, iterable: T[]): QueryRoot {
2016-01-11 02:56:32 +01:00
const doPutAll = (tx: IDBTransaction) => {
for (let obj of iterable) {
2016-10-18 01:16:31 +02:00
tx.objectStore(store.name).put(obj);
}
2016-01-11 02:56:32 +01:00
};
2016-10-18 01:16:31 +02:00
this.addWork(doPutAll, store.name, true);
return this;
}
2016-01-11 02:56:32 +01:00
/**
* Add an object to the given object store.
* Fails if the object's key is already present
* in the object store.
*/
2016-10-18 01:16:31 +02:00
add<T>(store: Store<T>, val: T): QueryRoot {
2016-01-11 02:56:32 +01:00
const doAdd = (tx: IDBTransaction) => {
2016-10-18 01:16:31 +02:00
tx.objectStore(store.name).add(val);
2016-01-11 02:56:32 +01:00
};
2016-10-18 01:16:31 +02:00
this.addWork(doAdd, store.name, true);
return this;
}
2016-01-11 02:56:32 +01:00
/**
* Get one object from a store by its key.
*/
2016-10-18 01:16:31 +02:00
get<T>(store: Store<T>, key: any): Promise<T|undefined> {
2016-02-09 21:56:06 +01:00
if (key === void 0) {
throw Error("key must not be undefined");
}
2016-01-11 02:56:32 +01:00
const {resolve, promise} = openPromise();
2016-09-12 17:41:12 +02:00
const doGet = (tx: IDBTransaction) => {
2016-10-18 01:16:31 +02:00
const req = tx.objectStore(store.name).get(key);
2016-09-12 17:41:12 +02:00
req.onsuccess = () => {
2016-01-11 02:56:32 +01:00
resolve(req.result);
};
2016-01-11 02:56:32 +01:00
};
2016-01-05 17:11:01 +01:00
2016-10-18 01:16:31 +02:00
this.addWork(doGet, store.name, false);
2016-01-11 02:56:32 +01:00
return Promise.resolve()
2016-10-18 01:36:47 +02:00
.then(() => this.finish())
.then(() => promise);
}
2016-02-23 14:07:53 +01:00
/**
* Get one object from a store by its key.
*/
2016-09-12 17:41:12 +02:00
getIndexed(storeName: string, indexName: string, key: any): Promise<any> {
2016-02-23 14:07:53 +01:00
if (key === void 0) {
throw Error("key must not be undefined");
}
const {resolve, promise} = openPromise();
2016-09-12 17:41:12 +02:00
const doGetIndexed = (tx: IDBTransaction) => {
2016-02-23 14:07:53 +01:00
const req = tx.objectStore(storeName).index(indexName).get(key);
2016-09-12 17:41:12 +02:00
req.onsuccess = () => {
2016-02-23 14:07:53 +01:00
resolve(req.result);
};
};
this.addWork(doGetIndexed, storeName, false);
return Promise.resolve()
2016-10-18 01:36:47 +02:00
.then(() => this.finish())
.then(() => promise);
2016-02-23 14:07:53 +01:00
}
2016-01-11 02:56:32 +01:00
/**
* Finish the query, and start the query in the first place if necessary.
*/
finish(): Promise<void> {
if (this.kickoffPromise) {
return this.kickoffPromise;
}
2016-09-12 17:41:12 +02:00
this.kickoffPromise = new Promise<void>((resolve, reject) => {
if (this.work.length == 0) {
resolve();
return;
}
2016-01-11 02:56:32 +01:00
const mode = this.hasWrite ? "readwrite" : "readonly";
const tx = this.db.transaction(Array.from(this.stores), mode);
tx.oncomplete = () => {
resolve();
};
for (let w of this.work) {
2016-01-11 02:56:32 +01:00
w(tx);
}
});
return this.kickoffPromise;
}
2016-01-11 02:56:32 +01:00
/**
* Delete an object by from the given object store.
*/
2016-09-12 17:41:12 +02:00
delete(storeName: string, key: any): QueryRoot {
const doDelete = (tx: IDBTransaction) => {
2016-01-11 02:56:32 +01:00
tx.objectStore(storeName).delete(key);
};
this.addWork(doDelete, storeName, true);
return this;
}
2016-01-11 02:56:32 +01:00
/**
* Low-level function to add a task to the internal work queue.
*/
2016-09-12 17:41:12 +02:00
addWork(workFn: (t: IDBTransaction) => void,
2016-10-18 01:36:47 +02:00
storeName?: string,
isWrite?: boolean) {
2016-09-12 17:41:12 +02:00
this.work.push(workFn);
if (storeName) {
this.addStoreAccess(storeName, isWrite);
}
}
addStoreAccess(storeName: string, isWrite?: boolean) {
2016-01-11 02:56:32 +01:00
if (storeName) {
this.stores.add(storeName);
}
if (isWrite) {
this.hasWrite = true;
}
}
}