From 5579c018d19cd1057e694b1995b420e3d309303e Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Mon, 9 Aug 2021 13:44:07 -0700 Subject: [PATCH 01/18] Migrate common.js to TypeScript Add initial stab at annotating common Add missing return types and semicolons --- src/matrix/room/timeline/EventKey.js | 2 +- .../timeline/entries/FragmentBoundaryEntry.js | 2 +- src/matrix/storage/{common.js => common.ts} | 17 ++++++++++------- src/matrix/storage/idb/Storage.js | 2 +- src/matrix/storage/idb/Transaction.js | 2 +- src/matrix/storage/idb/error.js | 2 +- src/matrix/storage/idb/export.js | 2 +- .../storage/idb/stores/PendingEventStore.js | 2 +- .../storage/idb/stores/TimelineEventStore.js | 4 ++-- .../storage/idb/stores/TimelineFragmentStore.js | 4 ++-- src/matrix/storage/idb/utils.js | 2 +- src/matrix/storage/memory/Storage.js | 2 +- 12 files changed, 23 insertions(+), 20 deletions(-) rename src/matrix/storage/{common.js => common.ts} (79%) 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 79% rename from src/matrix/storage/common.js rename to src/matrix/storage/common.ts index 4d10ef65..926ccbbf 100644 --- a/src/matrix/storage/common.js +++ b/src/matrix/storage/common.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -export const STORE_NAMES = Object.freeze([ +export const STORE_NAMES: Readonly = Object.freeze([ "session", "roomState", "roomSummary", @@ -35,13 +35,16 @@ export const STORE_NAMES = Object.freeze([ "accountData", ]); -export const STORE_MAP = Object.freeze(STORE_NAMES.reduce((nameMap, name) => { +export const STORE_MAP: Readonly<{ [name : string]: string }> = Object.freeze(STORE_NAMES.reduce((nameMap, name) => { nameMap[name] = name; return nameMap; }, {})); export class StorageError extends Error { - constructor(message, cause) { + errcode?: string; + cause?: Error; + + constructor(message: string, cause?: Error) { super(message); if (cause) { this.errcode = cause.name; @@ -49,23 +52,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/Storage.js b/src/matrix/storage/idb/Storage.js index 1f96e347..0a357dda 100644 --- a/src/matrix/storage/idb/Storage.js +++ b/src/matrix/storage/idb/Storage.js @@ -15,7 +15,7 @@ limitations under the License. */ import {Transaction} from "./Transaction.js"; -import { STORE_NAMES, StorageError } from "../common.js"; +import { STORE_NAMES, StorageError } from "../common"; import { reqAsPromise } from "./utils.js"; const WEBKITEARLYCLOSETXNBUG_BOGUS_KEY = "782rh281re38-boguskey"; diff --git a/src/matrix/storage/idb/Transaction.js b/src/matrix/storage/idb/Transaction.js index bdcc45e3..ae0f75be 100644 --- a/src/matrix/storage/idb/Transaction.js +++ b/src/matrix/storage/idb/Transaction.js @@ -15,7 +15,7 @@ limitations under the License. */ import {txnAsPromise} from "./utils.js"; -import {StorageError} from "../common.js"; +import {StorageError} from "../common"; import {Store} from "./Store.js"; import {SessionStore} from "./stores/SessionStore.js"; import {RoomSummaryStore} from "./stores/RoomSummaryStore.js"; diff --git a/src/matrix/storage/idb/error.js b/src/matrix/storage/idb/error.js index 2ba6289f..0b9de95c 100644 --- a/src/matrix/storage/idb/error.js +++ b/src/matrix/storage/idb/error.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { StorageError } from "../common.js"; +import { StorageError } from "../common"; export class IDBError extends StorageError { constructor(message, source, cause) { diff --git a/src/matrix/storage/idb/export.js b/src/matrix/storage/idb/export.js index 8a7724a3..34dd8706 100644 --- a/src/matrix/storage/idb/export.js +++ b/src/matrix/storage/idb/export.js @@ -15,7 +15,7 @@ limitations under the License. */ import { iterateCursor, txnAsPromise } from "./utils.js"; -import { STORE_NAMES } from "../common.js"; +import { STORE_NAMES } from "../common"; export async function exportSession(db) { const NOT_DONE = {done: false}; diff --git a/src/matrix/storage/idb/stores/PendingEventStore.js b/src/matrix/storage/idb/stores/PendingEventStore.js index 455cad2c..385f3c9a 100644 --- a/src/matrix/storage/idb/stores/PendingEventStore.js +++ b/src/matrix/storage/idb/stores/PendingEventStore.js @@ -15,7 +15,7 @@ limitations under the License. */ import { encodeUint32, decodeUint32 } from "../utils.js"; -import {KeyLimits} from "../../common.js"; +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..664958e6 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 { StorageError } from "../../common"; import { encodeUint32 } from "../utils.js"; -import {KeyLimits} from "../../common.js"; +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..208b7300 100644 --- a/src/matrix/storage/idb/stores/TimelineFragmentStore.js +++ b/src/matrix/storage/idb/stores/TimelineFragmentStore.js @@ -14,8 +14,8 @@ 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 { StorageError } from "../../common"; +import {KeyLimits} from "../../common"; import { encodeUint32 } from "../utils.js"; function encodeKey(roomId, fragmentId) { diff --git a/src/matrix/storage/idb/utils.js b/src/matrix/storage/idb/utils.js index fbb82fc5..f90324cd 100644 --- a/src/matrix/storage/idb/utils.js +++ b/src/matrix/storage/idb/utils.js @@ -16,7 +16,7 @@ limitations under the License. */ import { IDBRequestError } from "./error.js"; -import { StorageError } from "../common.js"; +import { StorageError } from "../common"; let needsSyncPromise = false; diff --git a/src/matrix/storage/memory/Storage.js b/src/matrix/storage/memory/Storage.js index c1c0fe3c..a76c16f3 100644 --- a/src/matrix/storage/memory/Storage.js +++ b/src/matrix/storage/memory/Storage.js @@ -15,7 +15,7 @@ limitations under the License. */ import {Transaction} from "./Transaction.js"; -import { STORE_MAP, STORE_NAMES } from "../common.js"; +import { STORE_MAP, STORE_NAMES } from "../common"; export class Storage { constructor(initialStoreValues = {}) { From cd9fe360a45bb475c1dc933bdbed36a0a20e63c8 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Mon, 9 Aug 2021 13:56:20 -0700 Subject: [PATCH 02/18] Start migrating utils.js to TypeScript --- src/matrix/storage/idb/QueryTarget.js | 2 +- src/matrix/storage/idb/Storage.js | 2 +- src/matrix/storage/idb/StorageFactory.js | 2 +- src/matrix/storage/idb/Transaction.js | 2 +- src/matrix/storage/idb/export.js | 2 +- src/matrix/storage/idb/quirks.js | 2 +- src/matrix/storage/idb/schema.js | 2 +- .../storage/idb/stores/PendingEventStore.js | 2 +- .../storage/idb/stores/TimelineEventStore.js | 2 +- .../idb/stores/TimelineFragmentStore.js | 2 +- src/matrix/storage/idb/{utils.js => utils.ts} | 66 ++++++++++++------- src/platform/web/legacy-polyfill.js | 2 +- 12 files changed, 55 insertions(+), 33 deletions(-) rename src/matrix/storage/idb/{utils.js => utils.ts} (70%) diff --git a/src/matrix/storage/idb/QueryTarget.js b/src/matrix/storage/idb/QueryTarget.js index 348e67f9..c2657c52 100644 --- a/src/matrix/storage/idb/QueryTarget.js +++ b/src/matrix/storage/idb/QueryTarget.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {iterateCursor, reqAsPromise} from "./utils.js"; +import {iterateCursor, reqAsPromise} from "./utils"; export class QueryTarget { constructor(target) { diff --git a/src/matrix/storage/idb/Storage.js b/src/matrix/storage/idb/Storage.js index 0a357dda..cef636fb 100644 --- a/src/matrix/storage/idb/Storage.js +++ b/src/matrix/storage/idb/Storage.js @@ -16,7 +16,7 @@ limitations under the License. import {Transaction} from "./Transaction.js"; import { STORE_NAMES, StorageError } from "../common"; -import { reqAsPromise } from "./utils.js"; +import { reqAsPromise } from "./utils"; const WEBKITEARLYCLOSETXNBUG_BOGUS_KEY = "782rh281re38-boguskey"; diff --git a/src/matrix/storage/idb/StorageFactory.js b/src/matrix/storage/idb/StorageFactory.js index 719d2672..a9dc8eed 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/Transaction.js b/src/matrix/storage/idb/Transaction.js index ae0f75be..d1c91d69 100644 --- a/src/matrix/storage/idb/Transaction.js +++ b/src/matrix/storage/idb/Transaction.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {txnAsPromise} from "./utils.js"; +import {txnAsPromise} from "./utils"; import {StorageError} from "../common"; import {Store} from "./Store.js"; import {SessionStore} from "./stores/SessionStore.js"; diff --git a/src/matrix/storage/idb/export.js b/src/matrix/storage/idb/export.js index 34dd8706..27979ce0 100644 --- a/src/matrix/storage/idb/export.js +++ b/src/matrix/storage/idb/export.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { iterateCursor, txnAsPromise } from "./utils.js"; +import { iterateCursor, txnAsPromise } from "./utils"; import { STORE_NAMES } from "../common"; export async function exportSession(db) { 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 352c810c..d593f6b9 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 {RoomMemberStore} from "./stores/RoomMemberStore.js"; import {SessionStore} from "./stores/SessionStore.js"; diff --git a/src/matrix/storage/idb/stores/PendingEventStore.js b/src/matrix/storage/idb/stores/PendingEventStore.js index 385f3c9a..61071f11 100644 --- a/src/matrix/storage/idb/stores/PendingEventStore.js +++ b/src/matrix/storage/idb/stores/PendingEventStore.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { encodeUint32, decodeUint32 } from "../utils.js"; +import { encodeUint32, decodeUint32 } from "../utils"; import {KeyLimits} from "../../common"; function encodeKey(roomId, queueIndex) { diff --git a/src/matrix/storage/idb/stores/TimelineEventStore.js b/src/matrix/storage/idb/stores/TimelineEventStore.js index 664958e6..8ff445f0 100644 --- a/src/matrix/storage/idb/stores/TimelineEventStore.js +++ b/src/matrix/storage/idb/stores/TimelineEventStore.js @@ -16,7 +16,7 @@ limitations under the License. import {EventKey} from "../../../room/timeline/EventKey.js"; import { StorageError } from "../../common"; -import { encodeUint32 } from "../utils.js"; +import { encodeUint32 } from "../utils"; import {KeyLimits} from "../../common"; function encodeKey(roomId, fragmentId, eventIndex) { diff --git a/src/matrix/storage/idb/stores/TimelineFragmentStore.js b/src/matrix/storage/idb/stores/TimelineFragmentStore.js index 208b7300..07a8ff42 100644 --- a/src/matrix/storage/idb/stores/TimelineFragmentStore.js +++ b/src/matrix/storage/idb/stores/TimelineFragmentStore.js @@ -16,7 +16,7 @@ limitations under the License. import { StorageError } from "../../common"; import {KeyLimits} from "../../common"; -import { encodeUint32 } from "../utils.js"; +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 70% rename from src/matrix/storage/idb/utils.js rename to src/matrix/storage/idb/utils.ts index f90324cd..9954e747 100644 --- a/src/matrix/storage/idb/utils.js +++ b/src/matrix/storage/idb/utils.ts @@ -20,12 +20,15 @@ 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,51 +52,57 @@ 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 = (ev) => { - const db = ev.target.result; - const txn = ev.target.transaction; + req.onupgradeneeded = (ev : IDBVersionChangeEvent) => { + const req = ev.target as IDBRequest; + const db = req.result; + const txn = req.transaction; const oldVersion = ev.oldVersion; createObjectStore(db, txn, oldVersion, version); }; 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 => { @@ -112,33 +121,41 @@ 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) { +type CursorIterator = I extends IDBCursorWithValue ? + (value: T, key: IDBValidKey, cursor: IDBCursorWithValue) => { done: boolean, jumpTo?: IDBValidKey } : + (value: undefined, key: IDBValidKey, cursor: IDBCursor) => { 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); @@ -151,16 +168,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; } @@ -173,7 +194,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; } @@ -185,7 +206,8 @@ 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) => { + // @ts-ignore + const matched = await iterateCursor(cursor, (value) => { if (matchesValue(value)) { match = value; return true; 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; From aa9839ee408f929828669358d238857c1864862d Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Mon, 9 Aug 2021 19:13:32 -0700 Subject: [PATCH 03/18] Seemingly fix a bug in utils.ts --- src/matrix/storage/idb/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/storage/idb/utils.ts b/src/matrix/storage/idb/utils.ts index 9954e747..2905eedb 100644 --- a/src/matrix/storage/idb/utils.ts +++ b/src/matrix/storage/idb/utils.ts @@ -206,12 +206,12 @@ export async function findStoreValue(db: IDBDatabase, storeName: string, toCu const store = tx.objectStore(storeName); const cursor = await reqAsPromise(toCursor(store)); let match; - // @ts-ignore 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"); From 28ee87cd2f238cc646963a1b2021dd9157d93c0b Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Mon, 9 Aug 2021 19:24:09 -0700 Subject: [PATCH 04/18] Migrate error.js to TypeScript --- src/matrix/storage/idb/Store.js | 2 +- src/matrix/storage/idb/{error.js => error.ts} | 11 +++++++---- src/matrix/storage/idb/utils.ts | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) rename src/matrix/storage/idb/{error.js => error.ts} (84%) diff --git a/src/matrix/storage/idb/Store.js b/src/matrix/storage/idb/Store.js index 2cafe500..91617b5f 100644 --- a/src/matrix/storage/idb/Store.js +++ b/src/matrix/storage/idb/Store.js @@ -15,7 +15,7 @@ limitations under the License. */ import {QueryTarget} from "./QueryTarget.js"; -import {IDBRequestAttemptError} from "./error.js"; +import {IDBRequestAttemptError} from "./error"; const LOG_REQUESTS = false; diff --git a/src/matrix/storage/idb/error.js b/src/matrix/storage/idb/error.ts similarity index 84% rename from src/matrix/storage/idb/error.js rename to src/matrix/storage/idb/error.ts index 0b9de95c..02953886 100644 --- a/src/matrix/storage/idb/error.js +++ b/src/matrix/storage/idb/error.ts @@ -18,7 +18,10 @@ limitations under the License. import { StorageError } from "../common"; export class IDBError extends StorageError { - constructor(message, source, cause) { + storeName: string; + databaseName: string; + + constructor(message: string, source, cause: DOMException | null) { const storeName = source?.name || ""; const databaseName = source?.transaction?.db?.name || ""; let fullMessage = `${message} on ${databaseName}.${storeName}`; @@ -34,14 +37,14 @@ export class IDBError extends StorageError { if (cause) { fullMessage += cause.message; } - super(fullMessage, cause); + super(fullMessage, cause || undefined); this.storeName = storeName; this.databaseName = databaseName; } } 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 +52,7 @@ export class IDBRequestError extends IDBError { } export class IDBRequestAttemptError extends IDBError { - constructor(method, source, cause, params) { + constructor(method: string, source, cause: DOMException, params: any[]) { super(`${method}(${params.map(p => JSON.stringify(p)).join(", ")}) failed`, source, cause); } } diff --git a/src/matrix/storage/idb/utils.ts b/src/matrix/storage/idb/utils.ts index 2905eedb..83686864 100644 --- a/src/matrix/storage/idb/utils.ts +++ b/src/matrix/storage/idb/utils.ts @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IDBRequestError } from "./error.js"; +import { IDBRequestError } from "./error"; import { StorageError } from "../common"; let needsSyncPromise = false; From c4e8ed8851d980630749651549c8e871103f290d Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Tue, 10 Aug 2021 09:58:33 -0700 Subject: [PATCH 05/18] Migrate QueryTarget.js to TypeScript --- .../idb/{QueryTarget.js => QueryTarget.ts} | 108 +++++++++++------- src/matrix/storage/idb/Store.js | 2 +- 2 files changed, 66 insertions(+), 44 deletions(-) rename src/matrix/storage/idb/{QueryTarget.js => QueryTarget.ts} (59%) diff --git a/src/matrix/storage/idb/QueryTarget.js b/src/matrix/storage/idb/QueryTarget.ts similarity index 59% rename from src/matrix/storage/idb/QueryTarget.js rename to src/matrix/storage/idb/QueryTarget.ts index c2657c52..3af303b9 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"; +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) { 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) { 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) { 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) { 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) { 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/Store.js b/src/matrix/storage/idb/Store.js index 91617b5f..e1f63eec 100644 --- a/src/matrix/storage/idb/Store.js +++ b/src/matrix/storage/idb/Store.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {QueryTarget} from "./QueryTarget.js"; +import {QueryTarget} from "./QueryTarget"; import {IDBRequestAttemptError} from "./error"; const LOG_REQUESTS = false; From db66570d7ac9766f0b70dcca49667d132056f788 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Tue, 10 Aug 2021 12:31:03 -0700 Subject: [PATCH 06/18] Migrate Store.js to TypeScript --- src/matrix/storage/idb/Store.js | 164 ------------------------ src/matrix/storage/idb/Store.ts | 175 ++++++++++++++++++++++++++ src/matrix/storage/idb/Transaction.js | 2 +- 3 files changed, 176 insertions(+), 165 deletions(-) delete mode 100644 src/matrix/storage/idb/Store.js create mode 100644 src/matrix/storage/idb/Store.ts 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"; From 704a8d99c72d88b283de2bafb5cc3eaa4a2e5612 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Wed, 18 Aug 2021 10:06:03 -0700 Subject: [PATCH 07/18] Add missing return types to QueryTarget --- src/matrix/storage/idb/QueryTarget.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/matrix/storage/idb/QueryTarget.ts b/src/matrix/storage/idb/QueryTarget.ts index 3af303b9..91756129 100644 --- a/src/matrix/storage/idb/QueryTarget.ts +++ b/src/matrix/storage/idb/QueryTarget.ts @@ -36,7 +36,7 @@ export class QueryTarget { this._target = target; } - _openCursor(range?: IDBQuery, direction?: IDBCursorDirection) { + _openCursor(range?: IDBQuery, direction?: IDBCursorDirection): IDBRequest { if (range && direction) { return this._target.openCursor(range, direction); } else if (range) { @@ -133,14 +133,14 @@ export class QueryTarget { } - async iterateValues(range: IDBQuery, callback: (val: T, key: IDBValidKey, cur: IDBCursorWithValue) => boolean) { + 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) => { return {done: callback(value, key, cur)}; }); } - async iterateKeys(range: IDBQuery, callback: (key: IDBValidKey, cur: IDBCursor) => boolean) { + 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)}; @@ -153,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: IDBValidKey[], backwards: boolean, callback: (key: IDBValidKey, found: boolean) => boolean) { + 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); @@ -225,7 +225,7 @@ export class QueryTarget { return results; } - async iterateWhile(range: IDBQuery, predicate: (v: T) => boolean) { + async iterateWhile(range: IDBQuery, predicate: (v: T) => boolean): Promise { const cursor = this._openCursor(range, "next"); await iterateCursor(cursor, (value) => { const passesPredicate = predicate(value); From 19bababa680ebdb0bcccea3e8710a5ca4b877ebe Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Thu, 19 Aug 2021 16:44:50 -0700 Subject: [PATCH 08/18] Use method syntax in QueryTarget. --- src/matrix/storage/idb/QueryTarget.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/matrix/storage/idb/QueryTarget.ts b/src/matrix/storage/idb/QueryTarget.ts index 91756129..e3b77810 100644 --- a/src/matrix/storage/idb/QueryTarget.ts +++ b/src/matrix/storage/idb/QueryTarget.ts @@ -21,12 +21,12 @@ 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; + 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; + get(key: IDBValidKey | IDBKeyRange): IDBRequest; + getKey(key: IDBValidKey | IDBKeyRange): IDBRequest; } export class QueryTarget { From 94ff76711c73a4769cd754ddf6f6b2b6570493f9 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Fri, 20 Aug 2021 10:04:22 -0700 Subject: [PATCH 09/18] Use 'in' to be more idiomatic --- src/matrix/storage/idb/Store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/storage/idb/Store.ts b/src/matrix/storage/idb/Store.ts index 890785bd..9c49dc57 100644 --- a/src/matrix/storage/idb/Store.ts +++ b/src/matrix/storage/idb/Store.ts @@ -35,7 +35,7 @@ class QueryTargetWrapper { } get keyPath(): string | string[] { - if (this._qt["objectStore"]) { + if ("objectStore" in this._qt) { return (this._qt as IDBIndex).objectStore.keyPath; } else { return this._qt.keyPath; From 50b7a8a3fd09e879777be4d0989c0378fb8baee7 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Fri, 20 Aug 2021 10:34:06 -0700 Subject: [PATCH 10/18] Add a comment explaining CursorIterator --- src/matrix/storage/idb/utils.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/matrix/storage/idb/utils.ts b/src/matrix/storage/idb/utils.ts index 83686864..ae2bd03f 100644 --- a/src/matrix/storage/idb/utils.ts +++ b/src/matrix/storage/idb/utils.ts @@ -127,6 +127,23 @@ export function txnAsPromise(txn): Promise { }); } +/** + * 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 = I extends IDBCursorWithValue ? (value: T, key: IDBValidKey, cursor: IDBCursorWithValue) => { done: boolean, jumpTo?: IDBValidKey } : (value: undefined, key: IDBValidKey, cursor: IDBCursor) => { done: boolean, jumpTo?: IDBValidKey } From 0b8acb51a403cd1c50cc43ee24596a02568f9424 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Fri, 20 Aug 2021 10:41:15 -0700 Subject: [PATCH 11/18] Switch errors to using nulls --- src/matrix/storage/common.ts | 4 ++-- src/matrix/storage/idb/error.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/matrix/storage/common.ts b/src/matrix/storage/common.ts index 926ccbbf..8d2dd2d2 100644 --- a/src/matrix/storage/common.ts +++ b/src/matrix/storage/common.ts @@ -42,9 +42,9 @@ export const STORE_MAP: Readonly<{ [name : string]: string }> = Object.freeze(ST export class StorageError extends Error { errcode?: string; - cause?: Error; + cause: Error | null; - constructor(message: string, cause?: Error) { + constructor(message: string, cause: Error | null = null) { super(message); if (cause) { this.errcode = cause.name; diff --git a/src/matrix/storage/idb/error.ts b/src/matrix/storage/idb/error.ts index 02953886..1c6875fb 100644 --- a/src/matrix/storage/idb/error.ts +++ b/src/matrix/storage/idb/error.ts @@ -21,7 +21,7 @@ export class IDBError extends StorageError { storeName: string; databaseName: string; - constructor(message: string, source, cause: DOMException | null) { + constructor(message: string, source, cause: DOMException | null = null) { const storeName = source?.name || ""; const databaseName = source?.transaction?.db?.name || ""; let fullMessage = `${message} on ${databaseName}.${storeName}`; @@ -37,7 +37,7 @@ export class IDBError extends StorageError { if (cause) { fullMessage += cause.message; } - super(fullMessage, cause || undefined); + super(fullMessage, cause); this.storeName = storeName; this.databaseName = databaseName; } From a2ff02e6c063816d4a4584f25955fd44097eee25 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Fri, 20 Aug 2021 12:33:06 -0700 Subject: [PATCH 12/18] Try using an enum for store names. --- src/matrix/storage/common.ts | 45 +++++++++++++++---------------- src/matrix/storage/idb/Storage.js | 8 ++---- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/src/matrix/storage/common.ts b/src/matrix/storage/common.ts index 8d2dd2d2..23bb0d31 100644 --- a/src/matrix/storage/common.ts +++ b/src/matrix/storage/common.ts @@ -14,31 +14,28 @@ See the License for the specific language governing permissions and limitations under the License. */ -export const STORE_NAMES: Readonly = 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: Readonly<{ [name : string]: string }> = 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 { errcode?: string; diff --git a/src/matrix/storage/idb/Storage.js b/src/matrix/storage/idb/Storage.js index cef636fb..a0814a53 100644 --- a/src/matrix/storage/idb/Storage.js +++ b/src/matrix/storage/idb/Storage.js @@ -15,7 +15,7 @@ limitations under the License. */ import {Transaction} from "./Transaction.js"; -import { STORE_NAMES, StorageError } from "../common"; +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) { From 1707df71df2c1d2701174486313ed202204f0a91 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Tue, 24 Aug 2021 11:11:30 -0700 Subject: [PATCH 13/18] Try to reduce repitition in CursorIterator --- src/matrix/storage/idb/utils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/matrix/storage/idb/utils.ts b/src/matrix/storage/idb/utils.ts index ae2bd03f..1339b6b9 100644 --- a/src/matrix/storage/idb/utils.ts +++ b/src/matrix/storage/idb/utils.ts @@ -144,9 +144,7 @@ export function txnAsPromise(txn): Promise { * 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 = I extends IDBCursorWithValue ? - (value: T, key: IDBValidKey, cursor: IDBCursorWithValue) => { done: boolean, jumpTo?: IDBValidKey } : - (value: undefined, key: IDBValidKey, cursor: IDBCursor) => { done: boolean, jumpTo?: IDBValidKey } +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?? From b7d232d56d88e29730cbb5c32bc29fd20d41e2bc Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Thu, 26 Aug 2021 16:56:03 -0700 Subject: [PATCH 14/18] Remove unnecessary cast and restrict constructor parameter type --- src/matrix/storage/idb/Store.ts | 2 +- src/matrix/storage/idb/error.ts | 26 ++++++++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/matrix/storage/idb/Store.ts b/src/matrix/storage/idb/Store.ts index 9c49dc57..64943d1b 100644 --- a/src/matrix/storage/idb/Store.ts +++ b/src/matrix/storage/idb/Store.ts @@ -36,7 +36,7 @@ class QueryTargetWrapper { get keyPath(): string | string[] { if ("objectStore" in this._qt) { - return (this._qt as IDBIndex).objectStore.keyPath; + return this._qt.objectStore.keyPath; } else { return this._qt.keyPath; } diff --git a/src/matrix/storage/idb/error.ts b/src/matrix/storage/idb/error.ts index 1c6875fb..bd3fb0a8 100644 --- a/src/matrix/storage/idb/error.ts +++ b/src/matrix/storage/idb/error.ts @@ -17,13 +17,31 @@ limitations under the License. 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 { storeName: string; databaseName: string; - constructor(message: string, source, cause: DOMException | null = null) { - const storeName = source?.name || ""; - const databaseName = source?.transaction?.db?.name || ""; + constructor(message: string, source: IDBIndex | IDBCursor | IDBObjectStore, cause: DOMException | null = null) { + let storeName: string, databaseName: string; + if (source instanceof IDBCursor) { + storeName = _sourceName(source.source); + databaseName = _sourceDatabase(source.source); + } else { + storeName = _sourceName(source); + databaseName = _sourceDatabase(source); + } let fullMessage = `${message} on ${databaseName}.${storeName}`; if (cause) { fullMessage += ": "; @@ -52,7 +70,7 @@ export class IDBRequestError extends IDBError { } export class IDBRequestAttemptError extends IDBError { - constructor(method: string, source, cause: DOMException, params: any[]) { + constructor(method: string, source: IDBIndex | IDBObjectStore, cause: DOMException, params: any[]) { super(`${method}(${params.map(p => JSON.stringify(p)).join(", ")}) failed`, source, cause); } } From 4c4687a05f85768b3b1118b9c8c85c44aca0c7a1 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Fri, 27 Aug 2021 09:29:02 -0700 Subject: [PATCH 15/18] Avoid unsafe (and error-prone) cast --- src/matrix/storage/idb/Store.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/matrix/storage/idb/Store.ts b/src/matrix/storage/idb/Store.ts index 64943d1b..8063a4c8 100644 --- a/src/matrix/storage/idb/Store.ts +++ b/src/matrix/storage/idb/Store.ts @@ -35,15 +35,14 @@ class QueryTargetWrapper { } get keyPath(): string | string[] { - if ("objectStore" in this._qt) { - return this._qt.objectStore.keyPath; - } else { - return this._qt.keyPath; - } + return this._qtStore.keyPath; } get _qtStore(): IDBObjectStore { - return this._qt as IDBObjectStore; + if ("objectStore" in this._qt) { + return this._qt.objectStore; + } + return this._qt; } supports(methodName: string): boolean { From 3ded5b20d3a812a62393e46ee6f73d8784ce8a1f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 31 Aug 2021 08:16:27 +0200 Subject: [PATCH 16/18] dedupe some code here --- src/matrix/storage/idb/error.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/matrix/storage/idb/error.ts b/src/matrix/storage/idb/error.ts index bd3fb0a8..0d723837 100644 --- a/src/matrix/storage/idb/error.ts +++ b/src/matrix/storage/idb/error.ts @@ -33,15 +33,10 @@ export class IDBError extends StorageError { storeName: string; databaseName: string; - constructor(message: string, source: IDBIndex | IDBCursor | IDBObjectStore, cause: DOMException | null = null) { - let storeName: string, databaseName: string; - if (source instanceof IDBCursor) { - storeName = _sourceName(source.source); - databaseName = _sourceDatabase(source.source); - } else { - storeName = _sourceName(source); - databaseName = _sourceDatabase(source); - } + 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 += ": "; From f466266a5f93e6ee637de9a9cd8cfb3efb82e71a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 31 Aug 2021 08:16:37 +0200 Subject: [PATCH 17/18] bring back extra caution --- src/matrix/storage/idb/error.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/storage/idb/error.ts b/src/matrix/storage/idb/error.ts index 0d723837..388ad4c0 100644 --- a/src/matrix/storage/idb/error.ts +++ b/src/matrix/storage/idb/error.ts @@ -25,8 +25,8 @@ function _sourceName(source: IDBIndex | IDBObjectStore): string { function _sourceDatabase(source: IDBIndex | IDBObjectStore): string { return "objectStore" in source ? - source.objectStore.transaction.db.name : - source.transaction.db.name; + source.objectStore?.transaction?.db?.name : + source.transaction?.db?.name; } export class IDBError extends StorageError { From 995ed23b3ef4d726e0cdcb12fb5ff4f69d548595 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 31 Aug 2021 08:43:39 +0200 Subject: [PATCH 18/18] tell TS we're certain to have a txn --- src/matrix/storage/idb/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/storage/idb/utils.ts b/src/matrix/storage/idb/utils.ts index 76c4d1ed..bd1683ea 100644 --- a/src/matrix/storage/idb/utils.ts +++ b/src/matrix/storage/idb/utils.ts @@ -74,7 +74,7 @@ export function openDatabase(name: string, createObjectStore: CreateObjectStore, req.onupgradeneeded = async (ev : IDBVersionChangeEvent) => { const req = ev.target as IDBRequest; const db = req.result; - const txn = req.transaction; + const txn = req.transaction!; const oldVersion = ev.oldVersion; try { await createObjectStore(db, txn, oldVersion, version);