diff --git a/src/matrix/storage/idb/Store.js b/src/matrix/storage/idb/Store.js deleted file mode 100644 index e1f63eec..00000000 --- a/src/matrix/storage/idb/Store.js +++ /dev/null @@ -1,164 +0,0 @@ -/* -Copyright 2020 Bruno Windels - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import {QueryTarget} from "./QueryTarget"; -import {IDBRequestAttemptError} from "./error"; - -const LOG_REQUESTS = false; - -function logRequest(method, params, source) { - const storeName = source?.name; - const databaseName = source?.transaction?.db?.name; - console.info(`${databaseName}.${storeName}.${method}(${params.map(p => JSON.stringify(p)).join(", ")})`); -} - -class QueryTargetWrapper { - constructor(qt) { - this._qt = qt; - } - - get keyPath() { - if (this._qt.objectStore) { - return this._qt.objectStore.keyPath; - } else { - return this._qt.keyPath; - } - } - - supports(methodName) { - return !!this._qt[methodName]; - } - - openKeyCursor(...params) { - try { - // not supported on Edge 15 - if (!this._qt.openKeyCursor) { - LOG_REQUESTS && logRequest("openCursor", params, this._qt); - return this.openCursor(...params); - } - LOG_REQUESTS && logRequest("openKeyCursor", params, this._qt); - return this._qt.openKeyCursor(...params); - } catch(err) { - throw new IDBRequestAttemptError("openKeyCursor", this._qt, err, params); - } - } - - openCursor(...params) { - try { - LOG_REQUESTS && logRequest("openCursor", params, this._qt); - return this._qt.openCursor(...params); - } catch(err) { - throw new IDBRequestAttemptError("openCursor", this._qt, err, params); - } - } - - put(...params) { - try { - LOG_REQUESTS && logRequest("put", params, this._qt); - return this._qt.put(...params); - } catch(err) { - throw new IDBRequestAttemptError("put", this._qt, err, params); - } - } - - add(...params) { - try { - LOG_REQUESTS && logRequest("add", params, this._qt); - return this._qt.add(...params); - } catch(err) { - throw new IDBRequestAttemptError("add", this._qt, err, params); - } - } - - get(...params) { - try { - LOG_REQUESTS && logRequest("get", params, this._qt); - return this._qt.get(...params); - } catch(err) { - throw new IDBRequestAttemptError("get", this._qt, err, params); - } - } - - getKey(...params) { - try { - LOG_REQUESTS && logRequest("getKey", params, this._qt); - return this._qt.getKey(...params); - } catch(err) { - throw new IDBRequestAttemptError("getKey", this._qt, err, params); - } - } - - delete(...params) { - try { - LOG_REQUESTS && logRequest("delete", params, this._qt); - return this._qt.delete(...params); - } catch(err) { - throw new IDBRequestAttemptError("delete", this._qt, err, params); - } - } - - index(...params) { - try { - return this._qt.index(...params); - } catch(err) { - // TODO: map to different error? this is not a request - throw new IDBRequestAttemptError("index", this._qt, err, params); - } - } -} - -export class Store extends QueryTarget { - constructor(idbStore, transaction) { - super(new QueryTargetWrapper(idbStore)); - this._transaction = transaction; - } - - get IDBKeyRange() { - return this._transaction.IDBKeyRange; - } - - get _idbStore() { - return this._target; - } - - index(indexName) { - return new QueryTarget(new QueryTargetWrapper(this._idbStore.index(indexName))); - } - - put(value) { - // If this request fails, the error will bubble up to the transaction and abort it, - // which is the behaviour we want. Therefore, it is ok to not create a promise for this - // request and await it. - // - // Perhaps at some later point, we will want to handle an error (like ConstraintError) for - // individual write requests. In that case, we should add a method that returns a promise (e.g. putAndObserve) - // and call preventDefault on the event to prevent it from aborting the transaction - // - // Note that this can still throw synchronously, like it does for TransactionInactiveError, - // see https://www.w3.org/TR/IndexedDB-2/#transaction-lifetime-concept - this._idbStore.put(value); - } - - add(value) { - // ok to not monitor result of request, see comment in `put`. - this._idbStore.add(value); - } - - delete(keyOrKeyRange) { - // ok to not monitor result of request, see comment in `put`. - this._idbStore.delete(keyOrKeyRange); - } -} diff --git a/src/matrix/storage/idb/Store.ts b/src/matrix/storage/idb/Store.ts new file mode 100644 index 00000000..890785bd --- /dev/null +++ b/src/matrix/storage/idb/Store.ts @@ -0,0 +1,175 @@ +/* +Copyright 2020 Bruno Windels + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {QueryTarget, IDBQuery} from "./QueryTarget"; +import {IDBRequestAttemptError} from "./error"; +import {reqAsPromise} from "./utils"; +import {Transaction} from "./Transaction"; + +const LOG_REQUESTS = false; + +function logRequest(method: string, params: any[], source: any): void { + const storeName = source?.name; + const databaseName = source?.transaction?.db?.name; + console.info(`${databaseName}.${storeName}.${method}(${params.map(p => JSON.stringify(p)).join(", ")})`); +} + +class QueryTargetWrapper { + private _qt: IDBIndex | IDBObjectStore; + + constructor(qt: IDBIndex | IDBObjectStore) { + this._qt = qt; + } + + get keyPath(): string | string[] { + if (this._qt["objectStore"]) { + return (this._qt as IDBIndex).objectStore.keyPath; + } else { + return this._qt.keyPath; + } + } + + get _qtStore(): IDBObjectStore { + return this._qt as IDBObjectStore; + } + + supports(methodName: string): boolean { + return !!this._qt[methodName]; + } + + openKeyCursor(range?: IDBQuery, direction?: IDBCursorDirection | undefined): IDBRequest { + try { + // not supported on Edge 15 + if (!this._qt.openKeyCursor) { + LOG_REQUESTS && logRequest("openCursor", [range, direction], this._qt); + return this.openCursor(range, direction); + } + LOG_REQUESTS && logRequest("openKeyCursor", [range, direction], this._qt); + return this._qt.openKeyCursor(range, direction) + } catch(err) { + throw new IDBRequestAttemptError("openKeyCursor", this._qt, err, [range, direction]); + } + } + + openCursor(range?: IDBQuery, direction?: IDBCursorDirection | undefined): IDBRequest { + try { + LOG_REQUESTS && logRequest("openCursor", [], this._qt); + return this._qt.openCursor(range, direction) + } catch(err) { + throw new IDBRequestAttemptError("openCursor", this._qt, err, [range, direction]); + } + } + + put(item: T, key?: IDBValidKey | undefined): IDBRequest { + try { + LOG_REQUESTS && logRequest("put", [item, key], this._qt); + return this._qtStore.put(item, key); + } catch(err) { + throw new IDBRequestAttemptError("put", this._qt, err, [item, key]); + } + } + + add(item: T, key?: IDBValidKey | undefined): IDBRequest { + try { + LOG_REQUESTS && logRequest("add", [item, key], this._qt); + return this._qtStore.add(item, key); + } catch(err) { + throw new IDBRequestAttemptError("add", this._qt, err, [item, key]); + } + } + + get(key: IDBValidKey | IDBKeyRange): IDBRequest { + try { + LOG_REQUESTS && logRequest("get", [key], this._qt); + return this._qt.get(key); + } catch(err) { + throw new IDBRequestAttemptError("get", this._qt, err, [key]); + } + } + + getKey(key: IDBValidKey | IDBKeyRange): IDBRequest { + try { + LOG_REQUESTS && logRequest("getKey", [key], this._qt); + return this._qt.getKey(key) + } catch(err) { + throw new IDBRequestAttemptError("getKey", this._qt, err, [key]); + } + } + + delete(key: IDBValidKey | IDBKeyRange): IDBRequest { + try { + LOG_REQUESTS && logRequest("delete", [key], this._qt); + return this._qtStore.delete(key); + } catch(err) { + throw new IDBRequestAttemptError("delete", this._qt, err, [key]); + } + } + + index(name: string): IDBIndex { + try { + return this._qtStore.index(name); + } catch(err) { + // TODO: map to different error? this is not a request + throw new IDBRequestAttemptError("index", this._qt, err, [name]); + } + } +} + +export class Store extends QueryTarget { + private _transaction: Transaction; + + constructor(idbStore: IDBObjectStore, transaction: Transaction) { + super(new QueryTargetWrapper(idbStore)); + this._transaction = transaction; + } + + get IDBKeyRange() { + // @ts-ignore + return this._transaction.IDBKeyRange; + } + + get _idbStore(): QueryTargetWrapper { + return (this._target as QueryTargetWrapper); + } + + index(indexName: string): QueryTarget { + return new QueryTarget(new QueryTargetWrapper(this._idbStore.index(indexName))); + } + + put(value: T): Promise { + // If this request fails, the error will bubble up to the transaction and abort it, + // which is the behaviour we want. Therefore, it is ok to not create a promise for this + // request and await it. + // + // Perhaps at some later point, we will want to handle an error (like ConstraintError) for + // individual write requests. In that case, we should add a method that returns a promise (e.g. putAndObserve) + // and call preventDefault on the event to prevent it from aborting the transaction + // + // Note that this can still throw synchronously, like it does for TransactionInactiveError, + // see https://www.w3.org/TR/IndexedDB-2/#transaction-lifetime-concept + return reqAsPromise(this._idbStore.put(value)); + } + + add(value: T): Promise { + // ok to not monitor result of request, see comment in `put`. + return reqAsPromise(this._idbStore.add(value)); + } + + delete(keyOrKeyRange: IDBValidKey | IDBKeyRange): Promise { + // ok to not monitor result of request, see comment in `put`. + return reqAsPromise(this._idbStore.delete(keyOrKeyRange)); + } +} diff --git a/src/matrix/storage/idb/Transaction.js b/src/matrix/storage/idb/Transaction.js index d1c91d69..4c543f4c 100644 --- a/src/matrix/storage/idb/Transaction.js +++ b/src/matrix/storage/idb/Transaction.js @@ -16,7 +16,7 @@ limitations under the License. import {txnAsPromise} from "./utils"; import {StorageError} from "../common"; -import {Store} from "./Store.js"; +import {Store} from "./Store"; import {SessionStore} from "./stores/SessionStore.js"; import {RoomSummaryStore} from "./stores/RoomSummaryStore.js"; import {InviteStore} from "./stores/InviteStore.js";