/*
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, If not, see
*/
///
/**
* Database query abstractions.
* @module Query
* @author Florian Dold
*/
"use strict";
function Query(db) {
return new QueryRoot(db);
}
abstract class QueryStreamBase {
abstract subscribe(f: (isDone: boolean, value: any) => void);
root: QueryRoot;
constructor(root: QueryRoot) {
this.root = root;
}
indexJoin(storeName: string, indexName: string, key: any): QueryStreamBase {
// join on the source relation's key, which may be
// a path or a transformer function
return new QueryStreamIndexJoin(this, storeName, indexName, key);
}
filter(f: (any) => boolean): QueryStreamBase {
return new QueryStreamFilter(this, f);
}
reduce(f, acc?): Promise {
let leakedResolve;
let p = new Promise((resolve, reject) => {
leakedResolve = resolve;
});
this.subscribe((isDone, value) => {
if (isDone) {
leakedResolve(acc);
return;
}
acc = f(value, acc);
});
return Promise.resolve().then(() => this.root.finish().then(() => p));
}
}
class QueryStreamFilter extends QueryStreamBase {
s: QueryStreamBase;
filterFn;
constructor(s: QueryStreamBase, filterFn) {
super(s.root);
this.s = s;
this.filterFn = filterFn;
}
subscribe(f) {
this.s.subscribe((isDone, value) => {
if (isDone) {
f(true, undefined);
return;
}
if (this.filterFn(value)) {
f(false, value)
}
});
}
}
class QueryStreamIndexJoin extends QueryStreamBase {
s: QueryStreamBase;
storeName;
key;
constructor(s, storeName: string, indexName: string, key: any) {
super(s.root);
this.s = s;
this.storeName = storeName;
this.key = key;
}
subscribe(f) {
this.s.subscribe((isDone, value) => {
if (isDone) {
f(true, undefined);
return;
}
let s = this.root.tx.objectStore(this.storeName);
let req = s.openCursor(IDBKeyRange.only(value));
req.onsuccess = () => {
let cursor = req.result;
if (cursor) {
f(false, [value, cursor.value]);
cursor.continue();
} else {
f(true, undefined);
}
}
});
}
}
class IterQueryStream extends QueryStreamBase {
private qr: QueryRoot;
private storeName;
private options;
constructor(qr, storeName, options?) {
super(qr);
this.qr = qr;
this.options = options;
this.storeName = storeName;
}
subscribe(f) {
function doIt() {
let s = this.qr.tx.objectStore(this.storeName);
let kr = undefined;
if (this.options && ("only" in this.options)) {
kr = IDBKeyRange.only(this.options.only);
}
let req = s.openCursor(kr);
req.onsuccess = (e) => {
let cursor: IDBCursorWithValue = req.result;
if (cursor) {
f(false, cursor.value);
cursor.continue();
} else {
f(true, undefined);
}
}
}
this.qr.work.push(doIt.bind(this));
}
}
class QueryRoot {
work = [];
db: IDBDatabase;
tx: IDBTransaction;
stores = new Set();
kickoffPromise;
constructor(db) {
this.db = db;
}
iter(storeName): QueryStreamBase {
this.stores.add(storeName);
return new IterQueryStream(this, storeName);
}
iterOnly(storeName, key): QueryStreamBase {
this.stores.add(storeName);
return new IterQueryStream(this, storeName, {only: key});
}
put(storeName, val): QueryRoot {
this.stores.add(storeName);
function doPut() {
this.tx.objectStore(storeName).put(val);
}
this.work.push(doPut.bind(this));
return this;
}
putAll(storeName, iterable): QueryRoot {
this.stores.add(storeName);
function doPutAll() {
for (let obj of iterable) {
this.tx.objectStore(storeName).put(obj);
}
}
this.work.push(doPutAll.bind(this));
return this;
}
add(storeName, val): QueryRoot {
this.stores.add(storeName);
function doAdd() {
this.tx.objectStore(storeName).add(val);
}
this.work.push(doAdd.bind(this));
return this;
}
get(storeName, key): Promise {
this.stores.add(storeName);
let leakedResolve;
let p = new Promise((resolve, reject) => {
leakedResolve = resolve;
});
if (!leakedResolve) {
// According to ES6 spec (paragraph 25.4.3.1), this can't happen.
throw Error("assertion failed");
}
function doGet() {
let req = this.tx.objectStore(storeName).get(key);
req.onsuccess = (r) => {
leakedResolve(req.result);
};
}
this.work.push(doGet.bind(this));
return Promise.resolve().then(() => {
return this.finish().then(() => p);
});
}
finish(): Promise {
if (this.kickoffPromise) {
return this.kickoffPromise;
}
this.kickoffPromise = new Promise((resolve, reject) => {
this.tx = this.db.transaction(Array.from(this.stores), "readwrite");
this.tx.oncomplete = () => {
resolve();
};
for (let w of this.work) {
w();
}
});
return this.kickoffPromise;
}
delete(storeName: string, key): QueryRoot {
this.stores.add(storeName);
function doDelete() {
this.tx.objectStore(storeName).delete(key);
}
this.work.push(doDelete.bind(this));
return this;
}
}