diff options
author | Florian Dold <florian.dold@gmail.com> | 2019-11-20 19:48:43 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2019-11-20 19:48:43 +0100 |
commit | 553da649902f71d5ca34c9a6289ab6b1ef0ba7cb (patch) | |
tree | 857c4eb2c39e4a92e71c8a623d3188e6dbbbd1e9 /src/query.ts | |
parent | faedf697626dd37f3ac74ad4cac1ec378598bbf3 (diff) |
WIP: simplify DB queries and error handling
Diffstat (limited to 'src/query.ts')
-rw-r--r-- | src/query.ts | 1112 |
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"); |