diff --git a/src/matrix/room/timeline/EventKey.js b/src/matrix/room/timeline/EventKey.js index b4dce376..af01a7e5 100644 --- a/src/matrix/room/timeline/EventKey.js +++ b/src/matrix/room/timeline/EventKey.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {KeyLimits} from "../../storage/common.js"; +import {KeyLimits} from "../../storage/common"; // key for events in the timelineEvents store export class EventKey { diff --git a/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js b/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js index 3d4bd67b..0697fbdd 100644 --- a/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js +++ b/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js @@ -17,7 +17,7 @@ limitations under the License. import {BaseEntry} from "./BaseEntry"; import {Direction} from "../Direction.js"; import {isValidFragmentId} from "../common.js"; -import {KeyLimits} from "../../../storage/common.js"; +import {KeyLimits} from "../../../storage/common"; export class FragmentBoundaryEntry extends BaseEntry { constructor(fragment, isFragmentStart, fragmentIdComparer) { diff --git a/src/matrix/storage/common.js b/src/matrix/storage/common.ts similarity index 51% rename from src/matrix/storage/common.js rename to src/matrix/storage/common.ts index 4d10ef65..23bb0d31 100644 --- a/src/matrix/storage/common.js +++ b/src/matrix/storage/common.ts @@ -14,34 +14,34 @@ See the License for the specific language governing permissions and limitations under the License. */ -export const STORE_NAMES = Object.freeze([ - "session", - "roomState", - "roomSummary", - "archivedRoomSummary", - "invites", - "roomMembers", - "timelineEvents", - "timelineRelations", - "timelineFragments", - "pendingEvents", - "userIdentities", - "deviceIdentities", - "olmSessions", - "inboundGroupSessions", - "outboundGroupSessions", - "groupSessionDecryptions", - "operations", - "accountData", -]); +export enum StoreNames { + session = "session", + roomState = "roomState", + roomSummary = "roomSummary", + archivedRoomSummary = "archivedRoomSummary", + invites = "invites", + roomMembers = "roomMembers", + timelineEvents = "timelineEvents", + timelineRelations = "timelineRelations", + timelineFragments = "timelineFragments", + pendingEvents = "pendingEvents", + userIdentities = "userIdentities", + deviceIdentities = "deviceIdentities", + olmSessions = "olmSessions", + inboundGroupSessions = "inboundGroupSessions", + outboundGroupSessions = "outboundGroupSessions", + groupSessionDecryptions = "groupSessionDecryptions", + operations = "operations", + accountData = "accountData", +} -export const STORE_MAP = Object.freeze(STORE_NAMES.reduce((nameMap, name) => { - nameMap[name] = name; - return nameMap; -}, {})); +export const STORE_NAMES: Readonly = Object.values(StoreNames); export class StorageError extends Error { - constructor(message, cause) { + errcode?: string; + cause: Error | null; + + constructor(message: string, cause: Error | null = null) { super(message); if (cause) { this.errcode = cause.name; @@ -49,23 +49,23 @@ export class StorageError extends Error { this.cause = cause; } - get name() { + get name(): string { return "StorageError"; } } export const KeyLimits = { - get minStorageKey() { + get minStorageKey(): number { // for indexeddb, we use unsigned 32 bit integers as keys return 0; }, - get middleStorageKey() { + get middleStorageKey(): number { // for indexeddb, we use unsigned 32 bit integers as keys return 0x7FFFFFFF; }, - get maxStorageKey() { + get maxStorageKey(): number { // for indexeddb, we use unsigned 32 bit integers as keys return 0xFFFFFFFF; } diff --git a/src/matrix/storage/idb/QueryTarget.js b/src/matrix/storage/idb/QueryTarget.ts similarity index 58% rename from src/matrix/storage/idb/QueryTarget.js rename to src/matrix/storage/idb/QueryTarget.ts index 348e67f9..e3b77810 100644 --- a/src/matrix/storage/idb/QueryTarget.js +++ b/src/matrix/storage/idb/QueryTarget.ts @@ -14,14 +14,29 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {iterateCursor, reqAsPromise} from "./utils.js"; +import {iterateCursor, DONE, NOT_DONE, reqAsPromise} from "./utils"; -export class QueryTarget { - constructor(target) { +type Reducer = (acc: B, val: A) => B + +export type IDBQuery = IDBValidKey | IDBKeyRange | undefined | null + +interface QueryTargetInterface { + openCursor(range?: IDBQuery, direction?: IDBCursorDirection | undefined): IDBRequest; + openKeyCursor(range?: IDBQuery, direction?: IDBCursorDirection | undefined): IDBRequest; + supports(method: string): boolean; + keyPath: string | string[]; + get(key: IDBValidKey | IDBKeyRange): IDBRequest; + getKey(key: IDBValidKey | IDBKeyRange): IDBRequest; +} + +export class QueryTarget { + protected _target: QueryTargetInterface; + + constructor(target: QueryTargetInterface) { this._target = target; } - _openCursor(range, direction) { + _openCursor(range?: IDBQuery, direction?: IDBCursorDirection): IDBRequest { if (range && direction) { return this._target.openCursor(range, direction); } else if (range) { @@ -33,95 +48,99 @@ export class QueryTarget { } } - supports(methodName) { + supports(methodName: string): boolean { return this._target.supports(methodName); } - get(key) { + get(key: IDBValidKey | IDBKeyRange): Promise { return reqAsPromise(this._target.get(key)); } - getKey(key) { + getKey(key: IDBValidKey | IDBKeyRange): Promise { if (this._target.supports("getKey")) { return reqAsPromise(this._target.getKey(key)); } else { return reqAsPromise(this._target.get(key)).then(value => { if (value) { - return value[this._target.keyPath]; + let keyPath = this._target.keyPath; + if (typeof keyPath === "string") { + keyPath = [keyPath]; + } + return keyPath.reduce((obj, key) => obj[key], value); } }); } } - reduce(range, reducer, initialValue) { + reduce(range: IDBQuery, reducer: Reducer, initialValue: B): Promise { return this._reduce(range, reducer, initialValue, "next"); } - reduceReverse(range, reducer, initialValue) { + reduceReverse(range: IDBQuery, reducer: Reducer, initialValue: B): Promise { return this._reduce(range, reducer, initialValue, "prev"); } - selectLimit(range, amount) { + selectLimit(range: IDBQuery, amount: number): Promise { return this._selectLimit(range, amount, "next"); } - selectLimitReverse(range, amount) { + selectLimitReverse(range: IDBQuery, amount: number): Promise { return this._selectLimit(range, amount, "prev"); } - selectWhile(range, predicate) { + selectWhile(range: IDBQuery, predicate: (v: T) => boolean): Promise { return this._selectWhile(range, predicate, "next"); } - selectWhileReverse(range, predicate) { + selectWhileReverse(range: IDBQuery, predicate: (v: T) => boolean): Promise { return this._selectWhile(range, predicate, "prev"); } - async selectAll(range, direction) { + async selectAll(range?: IDBQuery, direction?: IDBCursorDirection): Promise { const cursor = this._openCursor(range, direction); - const results = []; - await iterateCursor(cursor, (value) => { + const results: T[] = []; + await iterateCursor(cursor, (value) => { results.push(value); - return {done: false}; + return NOT_DONE; }); return results; } - selectFirst(range) { + selectFirst(range: IDBQuery): Promise { return this._find(range, () => true, "next"); } - selectLast(range) { + selectLast(range: IDBQuery): Promise { return this._find(range, () => true, "prev"); } - find(range, predicate) { + find(range: IDBQuery, predicate: (v: T) => boolean): Promise { return this._find(range, predicate, "next"); } - findReverse(range, predicate) { + findReverse(range: IDBQuery, predicate: (v : T) => boolean): Promise { return this._find(range, predicate, "prev"); } - async findMaxKey(range) { + async findMaxKey(range: IDBQuery): Promise { const cursor = this._target.openKeyCursor(range, "prev"); let maxKey; await iterateCursor(cursor, (_, key) => { maxKey = key; - return {done: true}; + return DONE; }); return maxKey; } - async iterateValues(range, callback) { + async iterateValues(range: IDBQuery, callback: (val: T, key: IDBValidKey, cur: IDBCursorWithValue) => boolean): Promise { const cursor = this._target.openCursor(range, "next"); - await iterateCursor(cursor, (value, key, cur) => { + await iterateCursor(cursor, (value, key, cur) => { return {done: callback(value, key, cur)}; }); } - async iterateKeys(range, callback) { + async iterateKeys(range: IDBQuery, callback: (key: IDBValidKey, cur: IDBCursor) => boolean): Promise { const cursor = this._target.openKeyCursor(range, "next"); await iterateCursor(cursor, (_, key, cur) => { return {done: callback(key, cur)}; @@ -134,7 +153,7 @@ export class QueryTarget { * If the callback returns true, the search is halted and callback won't be called again. * `callback` is called with the same instances of the key as given in `keys`, so direct comparison can be used. */ - async findExistingKeys(keys, backwards, callback) { + async findExistingKeys(keys: IDBValidKey[], backwards: boolean, callback: (key: IDBValidKey, found: boolean) => boolean): Promise { const direction = backwards ? "prev" : "next"; const compareKeys = (a, b) => backwards ? -indexedDB.cmp(a, b) : indexedDB.cmp(a, b); const sortedKeys = keys.slice().sort(compareKeys); @@ -154,7 +173,10 @@ export class QueryTarget { ++i; } const done = consumerDone || i >= sortedKeys.length; - const jumpTo = !done && sortedKeys[i]; + let jumpTo; + if (!done) { + jumpTo = sortedKeys[i]; + } return {done, jumpTo}; }); // report null for keys we didn't to at the end @@ -164,25 +186,25 @@ export class QueryTarget { } } - _reduce(range, reducer, initialValue, direction) { + _reduce(range: IDBQuery, reducer: (reduced: B, value: T) => B, initialValue: B, direction: IDBCursorDirection): Promise { let reducedValue = initialValue; const cursor = this._openCursor(range, direction); - return iterateCursor(cursor, (value) => { + return iterateCursor(cursor, (value) => { reducedValue = reducer(reducedValue, value); - return {done: false}; + return NOT_DONE; }); } - _selectLimit(range, amount, direction) { + _selectLimit(range: IDBQuery, amount: number, direction: IDBCursorDirection): Promise { return this._selectUntil(range, (results) => { return results.length === amount; }, direction); } - async _selectUntil(range, predicate, direction) { + async _selectUntil(range: IDBQuery, predicate: (vs: T[], v: T) => boolean, direction: IDBCursorDirection): Promise { const cursor = this._openCursor(range, direction); - const results = []; - await iterateCursor(cursor, (value) => { + const results: T[] = []; + await iterateCursor(cursor, (value) => { results.push(value); return {done: predicate(results, value)}; }); @@ -190,10 +212,10 @@ export class QueryTarget { } // allows you to fetch one too much that won't get added when the predicate fails - async _selectWhile(range, predicate, direction) { + async _selectWhile(range: IDBQuery, predicate: (v: T) => boolean, direction: IDBCursorDirection): Promise { const cursor = this._openCursor(range, direction); - const results = []; - await iterateCursor(cursor, (value) => { + const results: T[] = []; + await iterateCursor(cursor, (value) => { const passesPredicate = predicate(value); if (passesPredicate) { results.push(value); @@ -203,18 +225,18 @@ export class QueryTarget { return results; } - async iterateWhile(range, predicate) { + async iterateWhile(range: IDBQuery, predicate: (v: T) => boolean): Promise { const cursor = this._openCursor(range, "next"); - await iterateCursor(cursor, (value) => { + await iterateCursor(cursor, (value) => { const passesPredicate = predicate(value); return {done: !passesPredicate}; }); } - async _find(range, predicate, direction) { + async _find(range: IDBQuery, predicate: (v: T) => boolean, direction: IDBCursorDirection): Promise { const cursor = this._openCursor(range, direction); let result; - const found = await iterateCursor(cursor, (value) => { + const found = await iterateCursor(cursor, (value) => { const found = predicate(value); if (found) { result = value; diff --git a/src/matrix/storage/idb/Storage.js b/src/matrix/storage/idb/Storage.js index 1f96e347..a0814a53 100644 --- a/src/matrix/storage/idb/Storage.js +++ b/src/matrix/storage/idb/Storage.js @@ -15,8 +15,8 @@ limitations under the License. */ import {Transaction} from "./Transaction.js"; -import { STORE_NAMES, StorageError } from "../common.js"; -import { reqAsPromise } from "./utils.js"; +import { STORE_NAMES, StoreNames, StorageError } from "../common"; +import { reqAsPromise } from "./utils"; const WEBKITEARLYCLOSETXNBUG_BOGUS_KEY = "782rh281re38-boguskey"; @@ -25,11 +25,7 @@ export class Storage { this._db = idbDatabase; this._IDBKeyRange = IDBKeyRange; this._hasWebkitEarlyCloseTxnBug = hasWebkitEarlyCloseTxnBug; - const nameMap = STORE_NAMES.reduce((nameMap, name) => { - nameMap[name] = name; - return nameMap; - }, {}); - this.storeNames = Object.freeze(nameMap); + this.storeNames = StoreNames; } _validateStoreNames(storeNames) { diff --git a/src/matrix/storage/idb/StorageFactory.js b/src/matrix/storage/idb/StorageFactory.js index a9b86273..df68823f 100644 --- a/src/matrix/storage/idb/StorageFactory.js +++ b/src/matrix/storage/idb/StorageFactory.js @@ -15,7 +15,7 @@ limitations under the License. */ import {Storage} from "./Storage.js"; -import { openDatabase, reqAsPromise } from "./utils.js"; +import { openDatabase, reqAsPromise } from "./utils"; import { exportSession, importSession } from "./export.js"; import { schema } from "./schema.js"; import { detectWebkitEarlyCloseTxnBug } from "./quirks.js"; diff --git a/src/matrix/storage/idb/Store.js b/src/matrix/storage/idb/Store.js deleted file mode 100644 index 2cafe500..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.js"; -import {IDBRequestAttemptError} from "./error.js"; - -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..8063a4c8 --- /dev/null +++ b/src/matrix/storage/idb/Store.ts @@ -0,0 +1,174 @@ +/* +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[] { + return this._qtStore.keyPath; + } + + get _qtStore(): IDBObjectStore { + if ("objectStore" in this._qt) { + return this._qt.objectStore; + } + return this._qt; + } + + 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 bdcc45e3..4c543f4c 100644 --- a/src/matrix/storage/idb/Transaction.js +++ b/src/matrix/storage/idb/Transaction.js @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {txnAsPromise} from "./utils.js"; -import {StorageError} from "../common.js"; -import {Store} from "./Store.js"; +import {txnAsPromise} from "./utils"; +import {StorageError} from "../common"; +import {Store} from "./Store"; import {SessionStore} from "./stores/SessionStore.js"; import {RoomSummaryStore} from "./stores/RoomSummaryStore.js"; import {InviteStore} from "./stores/InviteStore.js"; diff --git a/src/matrix/storage/idb/error.js b/src/matrix/storage/idb/error.ts similarity index 61% rename from src/matrix/storage/idb/error.js rename to src/matrix/storage/idb/error.ts index 2ba6289f..388ad4c0 100644 --- a/src/matrix/storage/idb/error.js +++ b/src/matrix/storage/idb/error.ts @@ -15,12 +15,28 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { StorageError } from "../common.js"; +import { StorageError } from "../common"; + +function _sourceName(source: IDBIndex | IDBObjectStore): string { + return "objectStore" in source ? + `${source.objectStore.name}.${source.name}` : + source.name; +} + +function _sourceDatabase(source: IDBIndex | IDBObjectStore): string { + return "objectStore" in source ? + source.objectStore?.transaction?.db?.name : + source.transaction?.db?.name; +} export class IDBError extends StorageError { - constructor(message, source, cause) { - const storeName = source?.name || ""; - const databaseName = source?.transaction?.db?.name || ""; + storeName: string; + databaseName: string; + + constructor(message: string, sourceOrCursor: IDBIndex | IDBCursor | IDBObjectStore, cause: DOMException | null = null) { + const source = "source" in sourceOrCursor ? sourceOrCursor.source : sourceOrCursor; + const storeName = _sourceName(source); + const databaseName = _sourceDatabase(source); let fullMessage = `${message} on ${databaseName}.${storeName}`; if (cause) { fullMessage += ": "; @@ -41,7 +57,7 @@ export class IDBError extends StorageError { } export class IDBRequestError extends IDBError { - constructor(request, message = "IDBRequest failed") { + constructor(request: IDBRequest, message: string = "IDBRequest failed") { const source = request.source; const cause = request.error; super(message, source, cause); @@ -49,7 +65,7 @@ export class IDBRequestError extends IDBError { } export class IDBRequestAttemptError extends IDBError { - constructor(method, source, cause, params) { + constructor(method: string, source: IDBIndex | IDBObjectStore, cause: DOMException, params: any[]) { super(`${method}(${params.map(p => JSON.stringify(p)).join(", ")}) failed`, source, cause); } } diff --git a/src/matrix/storage/idb/export.js b/src/matrix/storage/idb/export.js index 8a7724a3..27979ce0 100644 --- a/src/matrix/storage/idb/export.js +++ b/src/matrix/storage/idb/export.js @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { iterateCursor, txnAsPromise } from "./utils.js"; -import { STORE_NAMES } from "../common.js"; +import { iterateCursor, txnAsPromise } from "./utils"; +import { STORE_NAMES } from "../common"; export async function exportSession(db) { const NOT_DONE = {done: false}; diff --git a/src/matrix/storage/idb/quirks.js b/src/matrix/storage/idb/quirks.js index 9739b7da..5eaa6836 100644 --- a/src/matrix/storage/idb/quirks.js +++ b/src/matrix/storage/idb/quirks.js @@ -15,7 +15,7 @@ limitations under the License. */ -import {openDatabase, txnAsPromise, reqAsPromise} from "./utils.js"; +import {openDatabase, txnAsPromise, reqAsPromise} from "./utils"; // filed as https://bugs.webkit.org/show_bug.cgi?id=222746 export async function detectWebkitEarlyCloseTxnBug(idbFactory) { diff --git a/src/matrix/storage/idb/schema.js b/src/matrix/storage/idb/schema.js index 6f02b828..4915f3a0 100644 --- a/src/matrix/storage/idb/schema.js +++ b/src/matrix/storage/idb/schema.js @@ -1,4 +1,4 @@ -import {iterateCursor, reqAsPromise} from "./utils.js"; +import {iterateCursor, reqAsPromise} from "./utils"; import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../room/members/RoomMember.js"; import {addRoomToIdentity} from "../../e2ee/DeviceTracker.js"; import {RoomMemberStore} from "./stores/RoomMemberStore.js"; diff --git a/src/matrix/storage/idb/stores/PendingEventStore.js b/src/matrix/storage/idb/stores/PendingEventStore.js index 455cad2c..61071f11 100644 --- a/src/matrix/storage/idb/stores/PendingEventStore.js +++ b/src/matrix/storage/idb/stores/PendingEventStore.js @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { encodeUint32, decodeUint32 } from "../utils.js"; -import {KeyLimits} from "../../common.js"; +import { encodeUint32, decodeUint32 } from "../utils"; +import {KeyLimits} from "../../common"; function encodeKey(roomId, queueIndex) { return `${roomId}|${encodeUint32(queueIndex)}`; diff --git a/src/matrix/storage/idb/stores/TimelineEventStore.js b/src/matrix/storage/idb/stores/TimelineEventStore.js index b4b204a8..8ff445f0 100644 --- a/src/matrix/storage/idb/stores/TimelineEventStore.js +++ b/src/matrix/storage/idb/stores/TimelineEventStore.js @@ -15,9 +15,9 @@ limitations under the License. */ import {EventKey} from "../../../room/timeline/EventKey.js"; -import { StorageError } from "../../common.js"; -import { encodeUint32 } from "../utils.js"; -import {KeyLimits} from "../../common.js"; +import { StorageError } from "../../common"; +import { encodeUint32 } from "../utils"; +import {KeyLimits} from "../../common"; function encodeKey(roomId, fragmentId, eventIndex) { return `${roomId}|${encodeUint32(fragmentId)}|${encodeUint32(eventIndex)}`; diff --git a/src/matrix/storage/idb/stores/TimelineFragmentStore.js b/src/matrix/storage/idb/stores/TimelineFragmentStore.js index 96ff441e..07a8ff42 100644 --- a/src/matrix/storage/idb/stores/TimelineFragmentStore.js +++ b/src/matrix/storage/idb/stores/TimelineFragmentStore.js @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { StorageError } from "../../common.js"; -import {KeyLimits} from "../../common.js"; -import { encodeUint32 } from "../utils.js"; +import { StorageError } from "../../common"; +import {KeyLimits} from "../../common"; +import { encodeUint32 } from "../utils"; function encodeKey(roomId, fragmentId) { return `${roomId}|${encodeUint32(fragmentId)}`; diff --git a/src/matrix/storage/idb/utils.js b/src/matrix/storage/idb/utils.ts similarity index 61% rename from src/matrix/storage/idb/utils.js rename to src/matrix/storage/idb/utils.ts index 3b9ca8bc..bd1683ea 100644 --- a/src/matrix/storage/idb/utils.js +++ b/src/matrix/storage/idb/utils.ts @@ -15,17 +15,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IDBRequestError } from "./error.js"; -import { StorageError } from "../common.js"; +import { IDBRequestError } from "./error"; +import { StorageError } from "../common"; let needsSyncPromise = false; +export const DONE = { done: true } +export const NOT_DONE = { done: false } + /* should be called on legacy platforms to see if transactions close before draining the microtask queue (IE11 on Windows 7). If this is the case, promises need to be resolved synchronously from the idb request handler to prevent the transaction from closing prematurely. */ -export async function checkNeedsSyncPromise() { +export async function checkNeedsSyncPromise(): Promise { // important to have it turned off while doing the test, // otherwise reqAsPromise would not fail needsSyncPromise = false; @@ -49,26 +52,29 @@ export async function checkNeedsSyncPromise() { } // storage keys are defined to be unsigned 32bit numbers in KeyLimits, which is assumed by idb -export function encodeUint32(n) { +export function encodeUint32(n: number): string { const hex = n.toString(16); return "0".repeat(8 - hex.length) + hex; } // used for logs where timestamp is part of key, which is larger than 32 bit -export function encodeUint64(n) { +export function encodeUint64(n: number): string { const hex = n.toString(16); return "0".repeat(16 - hex.length) + hex; } -export function decodeUint32(str) { +export function decodeUint32(str: string): number { return parseInt(str, 16); } -export function openDatabase(name, createObjectStore, version, idbFactory = window.indexedDB) { +type CreateObjectStore = (db : IDBDatabase, txn: IDBTransaction | null, oldVersion: number, version: number) => any + +export function openDatabase(name: string, createObjectStore: CreateObjectStore, version: number, idbFactory: IDBFactory = window.indexedDB): Promise { const req = idbFactory.open(name, version); - req.onupgradeneeded = async (ev) => { - const db = ev.target.result; - const txn = ev.target.transaction; + req.onupgradeneeded = async (ev : IDBVersionChangeEvent) => { + const req = ev.target as IDBRequest; + const db = req.result; + const txn = req.transaction!; const oldVersion = ev.oldVersion; try { await createObjectStore(db, txn, oldVersion, version); @@ -82,25 +88,28 @@ export function openDatabase(name, createObjectStore, version, idbFactory = wind return reqAsPromise(req); } -export function reqAsPromise(req) { +export function reqAsPromise(req: IDBRequest): Promise { return new Promise((resolve, reject) => { req.addEventListener("success", event => { - resolve(event.target.result); + resolve((event.target as IDBRequest).result); + // @ts-ignore needsSyncPromise && Promise._flush && Promise._flush(); }); req.addEventListener("error", event => { - const error = new IDBRequestError(event.target); + const error = new IDBRequestError(event.target as IDBRequest); reject(error); + // @ts-ignore needsSyncPromise && Promise._flush && Promise._flush(); }); }); } -export function txnAsPromise(txn) { +export function txnAsPromise(txn): Promise { let error; return new Promise((resolve, reject) => { txn.addEventListener("complete", () => { resolve(); + // @ts-ignore needsSyncPromise && Promise._flush && Promise._flush(); }); txn.addEventListener("error", event => { @@ -119,33 +128,56 @@ export function txnAsPromise(txn) { error = new StorageError(`Transaction on ${dbName} with stores ${storeNames} was aborted.`); } reject(error); + // @ts-ignore needsSyncPromise && Promise._flush && Promise._flush(); }); }); } -export function iterateCursor(cursorRequest, processValue) { +/** + * This type is rather complicated, but I hope that this is for a good reason. There + * are currently two uses for `iterateCursor`: iterating a regular cursor, and iterating + * a key-only cursor, which does not have values. These two uses are distinct, and iteration + * never stops or starts having a value halfway through. + * + * Each of the argument functions currently either assumes the value will be there, or that it won't. We thus can't + * just accept a function argument `(T | undefined) => { done: boolean }`, since this messes with + * the type safety in both cases: the former case will have to check for `undefined`, and + * the latter would have an argument that can be `T`, even though it never will. + * + * So the approach here is to let TypeScript infer and accept (via generics) the type of + * the cursor, which is either `IDBCursorWithValue` or `IDBCursor`. Since the type is accepted + * via generics, we can actually vary the types of the actual function arguments depending on it. + * Thus, when a value is available (an `IDBCursorWithValue` is given), we require a function `(T) => ...`, and when it is not, we require + * a function `(undefined) => ...`. + */ +type CursorIterator = (value: I extends IDBCursorWithValue ? T : undefined, key: IDBValidKey, cursor: I) => { done: boolean, jumpTo?: IDBValidKey } + +export function iterateCursor(cursorRequest: IDBRequest, processValue: CursorIterator): Promise { // TODO: does cursor already have a value here?? - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { cursorRequest.onerror = () => { reject(new IDBRequestError(cursorRequest)); + // @ts-ignore needsSyncPromise && Promise._flush && Promise._flush(); }; // collect results cursorRequest.onsuccess = (event) => { - const cursor = event.target.result; + const cursor = (event.target as IDBRequest).result; if (!cursor) { resolve(false); + // @ts-ignore needsSyncPromise && Promise._flush && Promise._flush(); return; // end of results } - const result = processValue(cursor.value, cursor.key, cursor); + const result = processValue(cursor["value"], cursor.key, cursor); // TODO: don't use object for result and assume it's jumpTo when not === true/false or undefined const done = result?.done; const jumpTo = result?.jumpTo; if (done) { resolve(true); + // @ts-ignore needsSyncPromise && Promise._flush && Promise._flush(); } else if(jumpTo) { cursor.continue(jumpTo); @@ -158,16 +190,20 @@ export function iterateCursor(cursorRequest, processValue) { }); } -export async function fetchResults(cursor, isDone) { - const results = []; - await iterateCursor(cursor, (value) => { +type Pred = (value: T) => boolean + +export async function fetchResults(cursor: IDBRequest, isDone: Pred): Promise { + const results: T[] = []; + await iterateCursor(cursor, (value) => { results.push(value); return {done: isDone(results)}; }); return results; } -export async function select(db, storeName, toCursor, isDone) { +type ToCursor = (store: IDBObjectStore) => IDBRequest + +export async function select(db: IDBDatabase, storeName: string, toCursor: ToCursor, isDone: Pred): Promise { if (!isDone) { isDone = () => false; } @@ -180,7 +216,7 @@ export async function select(db, storeName, toCursor, isDone) { return await fetchResults(cursor, isDone); } -export async function findStoreValue(db, storeName, toCursor, matchesValue) { +export async function findStoreValue(db: IDBDatabase, storeName: string, toCursor: ToCursor, matchesValue: Pred): Promise { if (!matchesValue) { matchesValue = () => true; } @@ -192,11 +228,12 @@ export async function findStoreValue(db, storeName, toCursor, matchesValue) { const store = tx.objectStore(storeName); const cursor = await reqAsPromise(toCursor(store)); let match; - const matched = await iterateCursor(cursor, (value) => { + const matched = await iterateCursor(cursor, (value) => { if (matchesValue(value)) { match = value; - return true; + return DONE; } + return NOT_DONE; }); if (!matched) { throw new StorageError("Value not found"); diff --git a/src/platform/web/legacy-polyfill.js b/src/platform/web/legacy-polyfill.js index ea2461f7..45628f2c 100644 --- a/src/platform/web/legacy-polyfill.js +++ b/src/platform/web/legacy-polyfill.js @@ -16,7 +16,7 @@ limitations under the License. // polyfills needed for IE11 import Promise from "../../../lib/es6-promise/index.js"; -import {checkNeedsSyncPromise} from "../../matrix/storage/idb/utils.js"; +import {checkNeedsSyncPromise} from "../../matrix/storage/idb/utils"; if (typeof window.Promise === "undefined") { window.Promise = Promise;