aboutsummaryrefslogtreecommitdiff
path: root/src/query.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/query.ts')
-rw-r--r--src/query.ts1112
1 files changed, 259 insertions, 853 deletions
diff --git a/src/query.ts b/src/query.ts
index 7c9390467..7d03cfea8 100644
--- a/src/query.ts
+++ b/src/query.ts
@@ -1,3 +1,5 @@
+import { openPromise } from "./promiseUtils";
+
/*
This file is part of TALER
(C) 2016 GNUnet e.V.
@@ -20,9 +22,6 @@
* @author Florian Dold
*/
- import { openPromise } from "./promiseUtils";
-import { join } from "path";
-
/**
* Result of an inner join.
*/
@@ -63,928 +62,335 @@ export interface IndexOptions {
multiEntry?: boolean;
}
-/**
- * Definition of an index.
- */
-export class Index<S extends IDBValidKey, T> {
- /**
- * Name of the store that this index is associated with.
- */
- storeName: string;
-
- /**
- * Options to use for the index.
- */
- options: IndexOptions;
-
- constructor(
- s: Store<T>,
- public indexName: string,
- public keyPath: string | string[],
- options?: IndexOptions,
- ) {
- const defaultOptions = {
- multiEntry: false,
+function requestToPromise(req: IDBRequest): Promise<any> {
+ return new Promise((resolve, reject) => {
+ req.onsuccess = () => {
+ resolve(req.result);
};
- 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;
+ req.onerror = () => {
+ reject(req.error);
+ };
+ });
}
-/**
- * Stream that can be filtered, reduced or joined
- * with indices.
- */
-export interface QueryStream<T> {
- /**
- * Join the current query with values from an index.
- * The left side of the join is extracted via a function from the stream's
- * result, the right side of the join is the key of the index.
- */
- indexJoin<S, I extends IDBValidKey>(
- index: Index<I, S>,
- keyFn: (obj: T) => I,
- ): QueryStream<JoinResult<T, S>>;
- /**
- * Join the current query with values from an index, and keep values in the
- * current stream that don't have a match. The left side of the join is
- * extracted via a function from the stream's result, the right side of the
- * join is the key of the index.
- */
- indexJoinLeft<S, I extends IDBValidKey>(
- index: Index<I, S>,
- keyFn: (obj: T) => I,
- ): QueryStream<JoinLeftResult<T, S>>;
- /**
- * Join the current query with values from another object store.
- * The left side of the join is extracted via a function over the current query,
- * the right side of the join is the key of the object store.
- */
- keyJoin<S, I extends IDBValidKey>(
- store: Store<S>,
- keyFn: (obj: T) => I,
- ): QueryStream<JoinResult<T, S>>;
-
- /**
- * Only keep elements in the result stream for which the predicate returns
- * true.
- */
- filter(f: (x: T) => boolean): QueryStream<T>;
-
- /**
- * Fold the stream, resulting in a single value.
- */
- fold<S>(f: (v: T, acc: S) => S, start: S): Promise<S>;
-
- /**
- * Execute a function for every value of the stream, for the
- * side-effects of the function.
- */
- forEach(f: (v: T) => void): Promise<void>;
-
- /**
- * Map each element of the stream using a function, resulting in another
- * stream of a different type.
- */
- map<S>(f: (x: T) => S): QueryStream<S>;
-
- /**
- * Map each element of the stream to a potentially empty array, and collect
- * the result in a stream of the flattened arrays.
- */
- flatMap<S>(f: (x: T) => S[]): QueryStream<S>;
-
- /**
- * Collect the stream into an array and return a promise for it.
- */
- toArray(): Promise<T[]>;
-
- /**
- * Get the first value of the stream.
- */
- first(): QueryValue<T>;
-
- /**
- * Run the query without returning a result.
- * Useful for queries with side effects.
- */
- run(): Promise<void>;
+export function oneShotGet<T>(
+ db: IDBDatabase,
+ store: Store<T>,
+ key: any,
+): Promise<T | undefined> {
+ const tx = db.transaction([store.name], "readonly");
+ const req = tx.objectStore(store.name).get(key);
+ return requestToPromise(req);
}
-/**
- * Query result that consists of at most one value.
- */
-export interface QueryValue<T> {
- /**
- * Apply a function to a query value.
- */
- map<S>(f: (x: T) => S): QueryValue<S>;
- /**
- * Conditionally execute either of two queries based
- * on a property of this query value.
- *
- * Useful to properly implement complex queries within a transaction (as
- * opposed to just computing the conditional and then executing either
- * branch). This is necessary since IndexedDB does not allow long-lived
- * transactions.
- */
- cond<R>(
- f: (x: T) => boolean,
- onTrue: (r: QueryRoot) => R,
- onFalse: (r: QueryRoot) => R,
- ): Promise<void>;
+export function oneShotGetIndexed<S extends IDBValidKey, T>(
+ db: IDBDatabase,
+ index: Index<S, T>,
+ key: any,
+): Promise<T | undefined> {
+ const tx = db.transaction([index.storeName], "readonly");
+ const req = tx.objectStore(index.storeName).index(index.indexName).get(key);
+ return requestToPromise(req);
}
-abstract class BaseQueryValue<T> implements QueryValue<T> {
- constructor(public root: QueryRoot) {}
-
- map<S>(f: (x: T) => S): QueryValue<S> {
- return new MapQueryValue<T, S>(this, f);
- }
+export function oneShotPut<T>(
+ db: IDBDatabase,
+ store: Store<T>,
+ value: T,
+ key?: any,
+): Promise<any> {
+ const tx = db.transaction([store.name], "readwrite");
+ const req = tx.objectStore(store.name).put(value, key);
+ return requestToPromise(req);
+}
- cond<R>(
- f: (x: T) => boolean,
- onTrue: (r: QueryRoot) => R,
- onFalse: (r: QueryRoot) => R,
- ): Promise<void> {
- return new Promise<void>((resolve, reject) => {
- this.subscribeOne((v, tx) => {
- if (f(v)) {
- onTrue(new QueryRoot(this.root.db));
+function applyMutation<T>(req: IDBRequest, f: (x: T) => T | undefined): Promise<void> {
+ 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 {
- onFalse(new QueryRoot(this.root.db));
+ cursor.continue();
}
- });
- resolve();
- });
- }
-
- abstract subscribeOne(f: SubscribeOneFn): void;
-}
-
-class FirstQueryValue<T> extends BaseQueryValue<T> {
- private gotValue = false;
- private s: QueryStreamBase<T>;
-
- constructor(stream: QueryStreamBase<T>) {
- super(stream.root);
- this.s = stream;
- }
-
- subscribeOne(f: SubscribeOneFn): void {
- this.s.subscribe((isDone, value, tx) => {
- if (this.gotValue) {
- return;
- }
- if (isDone) {
- f(undefined, tx);
} else {
- f(value, tx);
+ resolve();
}
- this.gotValue = true;
- });
- }
+ };
+ req.onerror = () => {
+ reject(req.error);
+ };
+ });
}
-class MapQueryValue<T, S> extends BaseQueryValue<S> {
- constructor(private v: BaseQueryValue<T>, private mapFn: (x: T) => S) {
- super(v.root);
- }
-
- subscribeOne(f: SubscribeOneFn): void {
- this.v.subscribeOne((v, tx) => this.mapFn(v));
- }
+export function oneShotMutate<T>(
+ db: IDBDatabase,
+ store: Store<T>,
+ key: any,
+ f: (x: T) => T | undefined,
+): Promise<void> {
+ const tx = db.transaction([store.name], "readwrite");
+ const req = tx.objectStore(store.name).openCursor(key);
+ return applyMutation(req, f);
}
-/**
- * Exception that should be thrown by client code to abort a transaction.
- */
-export const AbortTransaction = Symbol("abort_transaction");
-
-abstract class QueryStreamBase<T> implements QueryStream<T> {
- abstract subscribe(
- f: (isDone: boolean, value: any, tx: IDBTransaction) => void,
- ): void;
- constructor(public root: QueryRoot) {}
-
- first(): QueryValue<T> {
- return new FirstQueryValue(this);
- }
-
- flatMap<S>(f: (x: T) => S[]): QueryStream<S> {
- return new QueryStreamFlatMap<T, S>(this, f);
- }
+type CursorResult<T> = CursorEmptyResult<T> | CursorValueResult<T>;
- map<S>(f: (x: T) => S): QueryStream<S> {
- return new QueryStreamMap(this, f);
- }
-
- indexJoin<S, I extends IDBValidKey>(
- index: Index<I, S>,
- keyFn: (obj: T) => I,
- ): QueryStream<JoinResult<T, S>> {
- this.root.addStoreAccess(index.storeName, false);
- return new QueryStreamIndexJoin<T, S>(
- this,
- index.storeName,
- index.indexName,
- keyFn,
- );
- }
-
- indexJoinLeft<S, I extends IDBValidKey>(
- index: Index<I, S>,
- keyFn: (obj: T) => I,
- ): QueryStream<JoinLeftResult<T, S>> {
- this.root.addStoreAccess(index.storeName, false);
- return new QueryStreamIndexJoinLeft<T, S>(
- this,
- index.storeName,
- index.indexName,
- keyFn,
- );
- }
-
- keyJoin<S, I extends IDBValidKey>(
- store: Store<S>,
- keyFn: (obj: T) => I,
- ): QueryStream<JoinResult<T, S>> {
- this.root.addStoreAccess(store.name, false);
- return new QueryStreamKeyJoin<T, S>(this, store.name, keyFn);
- }
-
- filter(f: (x: any) => boolean): QueryStream<T> {
- return new QueryStreamFilter(this, f);
- }
-
- toArray(): Promise<T[]> {
- const { resolve, promise } = openPromise<T[]>();
- const values: T[] = [];
-
- this.subscribe((isDone, value) => {
- if (isDone) {
- resolve(values);
- return;
- }
- values.push(value);
- });
-
- return Promise.resolve()
- .then(() => this.root.finish())
- .then(() => promise);
- }
-
- fold<A>(f: (x: T, acc: A) => A, init: A): Promise<A> {
- const { resolve, promise } = openPromise<A>();
- let acc = init;
-
- this.subscribe((isDone, value) => {
- if (isDone) {
- resolve(acc);
- return;
- }
- acc = f(value, acc);
- });
-
- return Promise.resolve()
- .then(() => this.root.finish())
- .then(() => promise);
- }
-
- forEach(f: (x: T) => void): Promise<void> {
- const { resolve, promise } = openPromise<void>();
-
- this.subscribe((isDone, value) => {
- if (isDone) {
- resolve();
- return;
- }
- f(value);
- });
-
- return Promise.resolve()
- .then(() => this.root.finish())
- .then(() => promise);
- }
-
- run(): Promise<void> {
- const { resolve, promise } = openPromise<void>();
-
- this.subscribe((isDone, value) => {
- if (isDone) {
- resolve();
- return;
- }
- });
-
- return Promise.resolve()
- .then(() => this.root.finish())
- .then(() => promise);
- }
+interface CursorEmptyResult<T> {
+ hasValue: false;
}
-type FilterFn = (e: any) => boolean;
-type SubscribeFn = (done: boolean, value: any, tx: IDBTransaction) => void;
-type SubscribeOneFn = (value: any, tx: IDBTransaction) => void;
-
-class QueryStreamFilter<T> extends QueryStreamBase<T> {
- constructor(public s: QueryStreamBase<T>, public filterFn: FilterFn) {
- super(s.root);
- }
-
- subscribe(f: SubscribeFn) {
- this.s.subscribe((isDone, value, tx) => {
- if (isDone) {
- f(true, undefined, tx);
- return;
- }
- if (this.filterFn(value)) {
- f(false, value, tx);
- }
- });
- }
+interface CursorValueResult<T> {
+ hasValue: true;
+ value: T;
}
-class QueryStreamFlatMap<T, S> extends QueryStreamBase<S> {
- constructor(public s: QueryStreamBase<T>, public flatMapFn: (v: T) => S[]) {
- super(s.root);
- }
-
- subscribe(f: SubscribeFn) {
- this.s.subscribe((isDone, value, tx) => {
- if (isDone) {
- f(true, undefined, tx);
- return;
+class ResultStream<T> {
+ private currentPromise: Promise<void>;
+ private gotCursorEnd: boolean = false;
+ private awaitingResult: boolean = 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 values = this.flatMapFn(value);
- for (const v in values) {
- f(false, v, tx);
+ const cursor = req.result;
+ if (cursor) {
+ this.awaitingResult = false;
+ p.resolve();
+ p = openPromise<void>();
+ this.currentPromise = p.promise;
+ } else {
+ this.gotCursorEnd = true;
+ p.resolve();
}
- });
- }
-}
-
-class QueryStreamMap<S, T> extends QueryStreamBase<T> {
- constructor(public s: QueryStreamBase<S>, public mapFn: (v: S) => T) {
- super(s.root);
+ };
+ req.onerror = () => {
+ p.reject(req.error);
+ };
}
- subscribe(f: SubscribeFn) {
- this.s.subscribe((isDone, value, tx) => {
- if (isDone) {
- f(true, undefined, tx);
- return;
+ async toArray(): Promise<T[]> {
+ const arr: T[] = [];
+ while (true) {
+ const x = await this.next();
+ if (x.hasValue) {
+ arr.push(x.value);
+ } else {
+ break;
}
- const mappedValue = this.mapFn(value);
- f(false, mappedValue, tx);
- });
- }
-}
-
-class QueryStreamIndexJoin<T, S> extends QueryStreamBase<JoinResult<T, S>> {
- constructor(
- public s: QueryStreamBase<T>,
- public storeName: string,
- public indexName: string,
- public key: any,
- ) {
- super(s.root);
+ }
+ return arr;
}
- subscribe(f: SubscribeFn) {
- this.s.subscribe((isDone, value, tx) => {
- if (isDone) {
- f(true, undefined, tx);
- return;
+ 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;
}
- const joinKey = this.key(value);
- const s = tx.objectStore(this.storeName).index(this.indexName);
- const req = s.openCursor(IDBKeyRange.only(joinKey));
- req.onsuccess = () => {
- const cursor = req.result;
- if (cursor) {
- f(false, { left: value, right: cursor.value }, tx);
- cursor.continue();
- }
- };
- });
- }
-}
-
-class QueryStreamIndexJoinLeft<T, S> extends QueryStreamBase<
- JoinLeftResult<T, S>
-> {
- constructor(
- public s: QueryStreamBase<T>,
- public storeName: string,
- public indexName: string,
- public key: any,
- ) {
- super(s.root);
+ }
+ return arr;
}
- subscribe(f: SubscribeFn) {
- this.s.subscribe((isDone, value, tx) => {
- if (isDone) {
- f(true, undefined, tx);
- return;
+ async forEach(f: (x: T) => void): Promise<void> {
+ while (true) {
+ const x = await this.next();
+ if (x.hasValue) {
+ f(x.value);
+ } else {
+ break;
}
- const s = tx.objectStore(this.storeName).index(this.indexName);
- const req = s.openCursor(IDBKeyRange.only(this.key(value)));
- let gotMatch = false;
- req.onsuccess = () => {
- const cursor = req.result;
- if (cursor) {
- gotMatch = true;
- f(false, { left: value, right: cursor.value }, tx);
- cursor.continue();
- } else {
- if (!gotMatch) {
- f(false, { left: value }, tx);
- }
- }
- };
- });
- }
-}
-
-class QueryStreamKeyJoin<T, S> extends QueryStreamBase<JoinResult<T, S>> {
- constructor(
- public s: QueryStreamBase<T>,
- public storeName: string,
- public key: any,
- ) {
- super(s.root);
+ }
}
- subscribe(f: SubscribeFn) {
- this.s.subscribe((isDone, value, tx) => {
- if (isDone) {
- f(true, undefined, tx);
- return;
- }
- const s = tx.objectStore(this.storeName);
- const req = s.openCursor(IDBKeyRange.only(this.key(value)));
- req.onsuccess = () => {
- const cursor = req.result;
- if (cursor) {
- f(false, { left: value, right: cursor.value }, tx);
- cursor.continue();
- } else {
- f(true, undefined, tx);
+ 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)) {
+ arr.push(x.value)
}
- };
- });
- }
-}
-
-class IterQueryStream<T> extends QueryStreamBase<T> {
- private storeName: string;
- private options: any;
- private subscribers: SubscribeFn[];
-
- constructor(qr: QueryRoot, storeName: string, options: any) {
- super(qr);
- this.options = options;
- this.storeName = storeName;
- this.subscribers = [];
-
- const doIt = (tx: IDBTransaction) => {
- const { indexName = void 0, only = void 0 } = this.options;
- let s: any;
- if (indexName !== void 0) {
- s = tx.objectStore(this.storeName).index(this.options.indexName);
} else {
- s = tx.objectStore(this.storeName);
- }
- let kr: IDBKeyRange | undefined;
- if (only !== undefined) {
- kr = IDBKeyRange.only(this.options.only);
+ break;
}
- const req = s.openCursor(kr);
- req.onsuccess = () => {
- const cursor: IDBCursorWithValue = req.result;
- if (cursor) {
- for (const f of this.subscribers) {
- f(false, cursor.value, tx);
- }
- cursor.continue();
- } else {
- for (const f of this.subscribers) {
- f(true, undefined, tx);
- }
- }
- };
- };
-
- this.root.addWork(doIt);
+ }
+ return arr;
}
- subscribe(f: SubscribeFn) {
- this.subscribers.push(f);
+ async next(): Promise<CursorResult<T>> {
+ 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 };
}
}
-/**
- * Root wrapper around an IndexedDB for queries with a fluent interface.
- */
-export class QueryRoot {
- private work: Array<(t: IDBTransaction) => void> = [];
- private stores: Set<string> = new Set();
- private kickoffPromise: Promise<void>;
-
- /**
- * Some operations is a write operation,
- * and we need to do a "readwrite" transaction/
- */
- private hasWrite: boolean;
-
- private finishScheduled: boolean;
-
- private finished: boolean = false;
+export function oneShotIter<T>(
+ db: IDBDatabase,
+ store: Store<T>
+): ResultStream<T> {
+ const tx = db.transaction([store.name], "readonly");
+ const req = tx.objectStore(store.name).openCursor();
+ return new ResultStream<T>(req);
+}
- private keys: { [keyName: string]: IDBValidKey } = {};
+export function oneShotIterIndex<S extends IDBValidKey, T>(
+ db: IDBDatabase,
+ index: Index<S, T>,
+ query?: any,
+): ResultStream<T> {
+ const tx = db.transaction([index.storeName], "readonly");
+ const req = tx.objectStore(index.storeName).index(index.indexName).openCursor(query);
+ return new ResultStream<T>(req);
+}
- constructor(public db: IDBDatabase) {}
+class TransactionHandle {
+ constructor(private tx: IDBTransaction) {}
- /**
- * Get a named key that was created during the query.
- */
- key(keyName: string): IDBValidKey | undefined {
- return this.keys[keyName];
+ put<T>(store: Store<T>, value: T, key?: any): Promise<any> {
+ const req = this.tx.objectStore(store.name).put(value, key);
+ return requestToPromise(req);
}
- private checkFinished() {
- if (this.finished) {
- throw Error("Can't add work to query after it was started");
- }
- }
-
- /**
- * Get a stream of all objects in the store.
- */
- iter<T>(store: Store<T>): QueryStream<T> {
- this.checkFinished();
- this.stores.add(store.name);
- this.scheduleFinish();
- return new IterQueryStream<T>(this, store.name, {});
+ add<T>(store: Store<T>, value: T, key?: any): Promise<any> {
+ const req = this.tx.objectStore(store.name).add(value, key);
+ return requestToPromise(req);
}
- /**
- * Count the number of objects in a store.
- */
- count<T>(store: Store<T>): Promise<number> {
- this.checkFinished();
- const { resolve, promise } = openPromise<number>();
-
- const doCount = (tx: IDBTransaction) => {
- const s = tx.objectStore(store.name);
- const req = s.count();
- req.onsuccess = () => {
- resolve(req.result);
- };
- };
-
- this.addWork(doCount, store.name, false);
- return Promise.resolve()
- .then(() => this.finish())
- .then(() => promise);
- }
-
- /**
- * Delete all objects in a store that match a predicate.
- */
- deleteIf<T>(
- store: Store<T>,
- predicate: (x: T, n: number) => boolean,
- ): QueryRoot {
- this.checkFinished();
- const doDeleteIf = (tx: IDBTransaction) => {
- const s = tx.objectStore(store.name);
- const req = s.openCursor();
- let n = 0;
- req.onsuccess = () => {
- const cursor: IDBCursorWithValue | null = req.result;
- if (cursor) {
- if (predicate(cursor.value, n++)) {
- cursor.delete();
- }
- cursor.continue();
- }
- };
- };
- this.addWork(doDeleteIf, store.name, true);
- return this;
+ get<T>(store: Store<T>, key: any): Promise<T | undefined> {
+ const req = this.tx.objectStore(store.name).get(key);
+ return requestToPromise(req);
}
- iterIndex<S extends IDBValidKey, T>(
- index: Index<S, T>,
- only?: S,
- ): QueryStream<T> {
- this.checkFinished();
- this.stores.add(index.storeName);
- this.scheduleFinish();
- return new IterQueryStream<T>(this, index.storeName, {
- indexName: index.indexName,
- only,
- });
+ iter<T>(store: Store<T>, key?: any): ResultStream<T> {
+ const req = this.tx.objectStore(store.name).openCursor(key);
+ return new ResultStream<T>(req);
}
- /**
- * Put an object into the given object store.
- * Overrides if an existing object with the same key exists
- * in the store.
- */
- put<T>(store: Store<T>, val: T, keyName?: string): QueryRoot {
- this.checkFinished();
- const doPut = (tx: IDBTransaction) => {
- const req = tx.objectStore(store.name).put(val);
- if (keyName) {
- req.onsuccess = () => {
- this.keys[keyName] = req.result;
- };
- }
- };
- this.scheduleFinish();
- this.addWork(doPut, store.name, true);
- return this;
+ delete<T>(store: Store<T>, key: any): Promise<void> {
+ const req = this.tx.objectStore(store.name).delete(key);
+ return requestToPromise(req);
}
- /**
- * Put an object into a store or return an existing record.
- */
- putOrGetExisting<T>(store: Store<T>, val: T, key: IDBValidKey): Promise<T> {
- this.checkFinished();
- const { resolve, promise } = openPromise<T>();
- const doPutOrGet = (tx: IDBTransaction) => {
- const objstore = tx.objectStore(store.name);
- const req = objstore.get(key);
- req.onsuccess = () => {
- if (req.result !== undefined) {
- resolve(req.result);
- } else {
- const req2 = objstore.add(val);
- req2.onsuccess = () => {
- resolve(val);
- };
- }
- };
- };
- this.scheduleFinish();
- this.addWork(doPutOrGet, store.name, true);
- return promise;
+ mutate<T>(store: Store<T>, key: any, f: (x: T) => T | undefined) {
+ const req = this.tx.objectStore(store.name).openCursor(key);
+ return applyMutation(req, f);
}
+}
- putWithResult<T>(store: Store<T>, val: T): Promise<IDBValidKey> {
- this.checkFinished();
- const { resolve, promise } = openPromise<IDBValidKey>();
- const doPutWithResult = (tx: IDBTransaction) => {
- const req = tx.objectStore(store.name).put(val);
- req.onsuccess = () => {
- resolve(req.result);
- };
- this.scheduleFinish();
+export function runWithWriteTransaction<T>(
+ db: IDBDatabase,
+ stores: Store<any>[],
+ f: (t: TransactionHandle) => Promise<T>,
+): Promise<T> {
+ 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.onerror = () => {
+ console.error("error in transaction:", tx.error);
+ reject(tx.error);
};
- this.addWork(doPutWithResult, store.name, true);
- return Promise.resolve()
- .then(() => this.finish())
- .then(() => promise);
- }
-
- /**
- * Update objects inside a transaction.
- *
- * If the mutation function throws AbortTransaction, the whole transaction will be aborted.
- * If the mutation function returns undefined or null, no modification will be made.
- */
- mutate<T>(store: Store<T>, key: any, f: (v: T) => T | undefined): QueryRoot {
- this.checkFinished();
- const doPut = (tx: IDBTransaction) => {
- const req = tx.objectStore(store.name).openCursor(IDBKeyRange.only(key));
- req.onsuccess = () => {
- const cursor = req.result;
- if (cursor) {
- const value = cursor.value;
- let modifiedValue: T | undefined;
- try {
- modifiedValue = f(value);
- } catch (e) {
- if (e === AbortTransaction) {
- tx.abort();
- return;
- }
- throw e;
- }
- if (modifiedValue !== undefined && modifiedValue !== null) {
- cursor.update(modifiedValue);
- }
- cursor.continue();
- }
- };
- };
- this.scheduleFinish();
- this.addWork(doPut, store.name, true);
- return this;
- }
-
- /**
- * Add all object from an iterable to the given object store.
- */
- putAll<T>(store: Store<T>, iterable: T[]): QueryRoot {
- this.checkFinished();
- const doPutAll = (tx: IDBTransaction) => {
- for (const obj of iterable) {
- tx.objectStore(store.name).put(obj);
+ 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);
};
- this.scheduleFinish();
- this.addWork(doPutAll, store.name, true);
- return this;
- }
-
- /**
- * Add an object to the given object store.
- * Fails if the object's key is already present
- * in the object store.
- */
- add<T>(store: Store<T>, val: T): QueryRoot {
- this.checkFinished();
- const doAdd = (tx: IDBTransaction) => {
- tx.objectStore(store.name).add(val);
+ tx.onabort = () => {
+ console.error("aborted transaction");
+ reject(AbortTransaction);
};
- this.scheduleFinish();
- this.addWork(doAdd, store.name, true);
- return this;
- }
-
- /**
- * Get one object from a store by its key.
- */
- get<T>(store: Store<T>, key: any): Promise<T | undefined> {
- this.checkFinished();
- if (key === void 0) {
- throw Error("key must not be undefined");
- }
-
- const { resolve, promise } = openPromise<T | undefined>();
-
- const doGet = (tx: IDBTransaction) => {
- const req = tx.objectStore(store.name).get(key);
- req.onsuccess = () => {
- resolve(req.result);
- };
- };
-
- this.addWork(doGet, store.name, false);
- return Promise.resolve()
- .then(() => this.finish())
- .then(() => promise);
- }
+ const th = new TransactionHandle(tx);
+ const resP = f(th);
+ resP.then(result => {
+ gotFunResult = true;
+ funResult = result;
+ });
+ });
+}
+/**
+ * Definition of an index.
+ */
+export class Index<S extends IDBValidKey, T> {
/**
- * Get get objects from a store by their keys.
- * If no object for a key exists, the resulting position in the array
- * contains 'undefined'.
+ * Name of the store that this index is associated with.
*/
- getMany<T>(store: Store<T>, keys: any[]): Promise<T[]> {
- this.checkFinished();
-
- const { resolve, promise } = openPromise<T[]>();
- const results: T[] = [];
-
- const doGetMany = (tx: IDBTransaction) => {
- for (const key of keys) {
- if (key === void 0) {
- throw Error("key must not be undefined");
- }
- const req = tx.objectStore(store.name).get(key);
- req.onsuccess = () => {
- results.push(req.result);
- if (results.length === keys.length) {
- resolve(results);
- }
- };
- }
- };
-
- this.addWork(doGetMany, store.name, false);
- return Promise.resolve()
- .then(() => this.finish())
- .then(() => promise);
- }
+ storeName: string;
/**
- * Get one object from a store by its key.
+ * Options to use for the index.
*/
- getIndexed<I extends IDBValidKey, T>(
- index: Index<I, T>,
- key: I,
- ): Promise<T | undefined> {
- this.checkFinished();
- if (key === void 0) {
- throw Error("key must not be undefined");
- }
-
- const { resolve, promise } = openPromise<T | undefined>();
+ options: IndexOptions;
- const doGetIndexed = (tx: IDBTransaction) => {
- const req = tx
- .objectStore(index.storeName)
- .index(index.indexName)
- .get(key);
- req.onsuccess = () => {
- resolve(req.result);
- };
+ constructor(
+ s: Store<T>,
+ public indexName: string,
+ public keyPath: string | string[],
+ options?: IndexOptions,
+ ) {
+ const defaultOptions = {
+ multiEntry: false,
};
-
- this.addWork(doGetIndexed, index.storeName, false);
- return Promise.resolve()
- .then(() => this.finish())
- .then(() => promise);
- }
-
- private scheduleFinish() {
- if (!this.finishScheduled) {
- Promise.resolve().then(() => this.finish());
- this.finishScheduled = true;
- }
- }
-
- /**
- * Finish the query, and start the query in the first place if necessary.
- */
- finish(): Promise<void> {
- if (this.kickoffPromise) {
- return this.kickoffPromise;
- }
- this.kickoffPromise = new Promise<void>((resolve, reject) => {
- // At this point, we can't add any more work
- this.finished = true;
- if (this.work.length === 0) {
- resolve();
- return;
- }
- const mode = this.hasWrite ? "readwrite" : "readonly";
- const tx = this.db.transaction(Array.from(this.stores), mode);
- tx.oncomplete = () => {
- resolve();
- };
- tx.onabort = () => {
- console.warn(
- `aborted ${mode} transaction on stores [${[...this.stores]}]`,
- );
- reject(Error("transaction aborted"));
- };
- tx.onerror = e => {
- console.warn(`error in transaction`, (e.target as any).error);
- };
- for (const w of this.work) {
- w(tx);
- }
- });
- return this.kickoffPromise;
+ this.options = { ...defaultOptions, ...(options || {}) };
+ this.storeName = s.name;
}
/**
- * Delete an object by from the given object store.
+ * We want to have the key type parameter in use somewhere,
+ * because otherwise the compiler complains. In iterIndex the
+ * key type is pretty useful.
*/
- delete<T>(store: Store<T>, key: any): QueryRoot {
- this.checkFinished();
- const doDelete = (tx: IDBTransaction) => {
- tx.objectStore(store.name).delete(key);
- };
- this.scheduleFinish();
- this.addWork(doDelete, store.name, true);
- return this;
- }
+ protected _dummyKey: S | undefined;
+}
- /**
- * Low-level function to add a task to the internal work queue.
- */
- addWork(
- workFn: (t: IDBTransaction) => void,
- storeName?: string,
- isWrite?: boolean,
- ) {
- this.work.push(workFn);
- if (storeName) {
- this.addStoreAccess(storeName, isWrite);
- }
- }
- addStoreAccess(storeName: string, isWrite?: boolean) {
- if (storeName) {
- this.stores.add(storeName);
- }
- if (isWrite) {
- this.hasWrite = true;
- }
- }
-}
+/**
+ * Exception that should be thrown by client code to abort a transaction.
+ */
+export const AbortTransaction = Symbol("abort_transaction");