From 5f3c9cda975e6ea6e6a62ab5556472c3e4261a02 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Thu, 12 Aug 2021 12:38:44 -0700 Subject: [PATCH 01/12] Migrate Transaction to TypeScript --- src/matrix/storage/idb/Storage.js | 2 +- .../idb/{Transaction.js => Transaction.ts} | 51 ++++++++++--------- 2 files changed, 29 insertions(+), 24 deletions(-) rename src/matrix/storage/idb/{Transaction.js => Transaction.ts} (78%) diff --git a/src/matrix/storage/idb/Storage.js b/src/matrix/storage/idb/Storage.js index a0814a53..d04183b6 100644 --- a/src/matrix/storage/idb/Storage.js +++ b/src/matrix/storage/idb/Storage.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {Transaction} from "./Transaction.js"; +import {Transaction} from "./Transaction"; import { STORE_NAMES, StoreNames, StorageError } from "../common"; import { reqAsPromise } from "./utils"; diff --git a/src/matrix/storage/idb/Transaction.js b/src/matrix/storage/idb/Transaction.ts similarity index 78% rename from src/matrix/storage/idb/Transaction.js rename to src/matrix/storage/idb/Transaction.ts index 8b85b795..b6fee2f2 100644 --- a/src/matrix/storage/idb/Transaction.js +++ b/src/matrix/storage/idb/Transaction.ts @@ -36,14 +36,19 @@ import {OperationStore} from "./stores/OperationStore"; import {AccountDataStore} from "./stores/AccountDataStore"; export class Transaction { - constructor(txn, allowedStoreNames, IDBKeyRange) { + private _txn: IDBTransaction; + private _allowedStoreNames: string[]; + private _stores: { [storeName : string] : any }; + + constructor(txn: IDBTransaction, allowedStoreNames: string[], IDBKeyRange) { this._txn = txn; this._allowedStoreNames = allowedStoreNames; this._stores = {}; + // @ts-ignore this.IDBKeyRange = IDBKeyRange; } - _idbStore(name) { + _idbStore(name: string): Store { if (!this._allowedStoreNames.includes(name)) { // more specific error? this is a bug, so maybe not ... throw new StorageError(`Invalid store for transaction: ${name}, only ${this._allowedStoreNames.join(", ")} are allowed.`); @@ -51,7 +56,7 @@ export class Transaction { return new Store(this._txn.objectStore(name), this); } - _store(name, mapStore) { + _store(name: string, mapStore: (idbStore: Store) => any): any { if (!this._stores[name]) { const idbStore = this._idbStore(name); this._stores[name] = mapStore(idbStore); @@ -59,83 +64,83 @@ export class Transaction { return this._stores[name]; } - get session() { + get session(): SessionStore { return this._store("session", idbStore => new SessionStore(idbStore)); } - get roomSummary() { + get roomSummary(): RoomSummaryStore { return this._store("roomSummary", idbStore => new RoomSummaryStore(idbStore)); } - get archivedRoomSummary() { + get archivedRoomSummary(): RoomSummaryStore { return this._store("archivedRoomSummary", idbStore => new RoomSummaryStore(idbStore)); } - get invites() { + get invites(): InviteStore { return this._store("invites", idbStore => new InviteStore(idbStore)); } - get timelineFragments() { + get timelineFragments(): TimelineFragmentStore { return this._store("timelineFragments", idbStore => new TimelineFragmentStore(idbStore)); } - get timelineEvents() { + get timelineEvents(): TimelineEventStore { return this._store("timelineEvents", idbStore => new TimelineEventStore(idbStore)); } - get timelineRelations() { + get timelineRelations(): TimelineRelationStore { return this._store("timelineRelations", idbStore => new TimelineRelationStore(idbStore)); } - get roomState() { + get roomState(): RoomStateStore { return this._store("roomState", idbStore => new RoomStateStore(idbStore)); } - get roomMembers() { + get roomMembers(): RoomMemberStore { return this._store("roomMembers", idbStore => new RoomMemberStore(idbStore)); } - get pendingEvents() { + get pendingEvents(): PendingEventStore { return this._store("pendingEvents", idbStore => new PendingEventStore(idbStore)); } - get userIdentities() { + get userIdentities(): UserIdentityStore { return this._store("userIdentities", idbStore => new UserIdentityStore(idbStore)); } - get deviceIdentities() { + get deviceIdentities(): DeviceIdentityStore { return this._store("deviceIdentities", idbStore => new DeviceIdentityStore(idbStore)); } - get olmSessions() { + get olmSessions(): OlmSessionStore { return this._store("olmSessions", idbStore => new OlmSessionStore(idbStore)); } - get inboundGroupSessions() { + get inboundGroupSessions(): InboundGroupSessionStore { return this._store("inboundGroupSessions", idbStore => new InboundGroupSessionStore(idbStore)); } - get outboundGroupSessions() { + get outboundGroupSessions(): OutboundGroupSessionStore { return this._store("outboundGroupSessions", idbStore => new OutboundGroupSessionStore(idbStore)); } - get groupSessionDecryptions() { + get groupSessionDecryptions(): GroupSessionDecryptionStore { return this._store("groupSessionDecryptions", idbStore => new GroupSessionDecryptionStore(idbStore)); } - get operations() { + get operations(): OperationStore { return this._store("operations", idbStore => new OperationStore(idbStore)); } - get accountData() { + get accountData(): AccountDataStore { return this._store("accountData", idbStore => new AccountDataStore(idbStore)); } - complete() { + complete(): Promise { return txnAsPromise(this._txn); } - abort() { + abort(): void { // TODO: should we wrap the exception in a StorageError? this._txn.abort(); } From eae820f91bfd9a9310e547b928e4d2e4c694a883 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Thu, 12 Aug 2021 13:06:09 -0700 Subject: [PATCH 02/12] Migrate Storage to TypeScript --- .../storage/idb/{Storage.js => Storage.ts} | 17 ++++++++++++----- src/matrix/storage/idb/StorageFactory.js | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) rename src/matrix/storage/idb/{Storage.js => Storage.ts} (83%) diff --git a/src/matrix/storage/idb/Storage.js b/src/matrix/storage/idb/Storage.ts similarity index 83% rename from src/matrix/storage/idb/Storage.js rename to src/matrix/storage/idb/Storage.ts index d04183b6..747a9e16 100644 --- a/src/matrix/storage/idb/Storage.js +++ b/src/matrix/storage/idb/Storage.ts @@ -21,21 +21,26 @@ import { reqAsPromise } from "./utils"; const WEBKITEARLYCLOSETXNBUG_BOGUS_KEY = "782rh281re38-boguskey"; export class Storage { - constructor(idbDatabase, IDBKeyRange, hasWebkitEarlyCloseTxnBug) { + private _db: IDBDatabase; + private _hasWebkitEarlyCloseTxnBug: boolean; + storeNames: typeof StoreNames; + + constructor(idbDatabase: IDBDatabase, IDBKeyRange, hasWebkitEarlyCloseTxnBug: boolean) { this._db = idbDatabase; + // @ts-ignore this._IDBKeyRange = IDBKeyRange; this._hasWebkitEarlyCloseTxnBug = hasWebkitEarlyCloseTxnBug; this.storeNames = StoreNames; } - _validateStoreNames(storeNames) { + _validateStoreNames(storeNames: StoreNames[]): void { const idx = storeNames.findIndex(name => !STORE_NAMES.includes(name)); if (idx !== -1) { throw new StorageError(`Tried top, a transaction unknown store ${storeNames[idx]}`); } } - async readTxn(storeNames) { + async readTxn(storeNames: StoreNames[]): Promise { this._validateStoreNames(storeNames); try { const txn = this._db.transaction(storeNames, "readonly"); @@ -44,13 +49,14 @@ export class Storage { if (this._hasWebkitEarlyCloseTxnBug) { await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY)); } + // @ts-ignore return new Transaction(txn, storeNames, this._IDBKeyRange); } catch(err) { throw new StorageError("readTxn failed", err); } } - async readWriteTxn(storeNames) { + async readWriteTxn(storeNames: StoreNames[]): Promise { this._validateStoreNames(storeNames); try { const txn = this._db.transaction(storeNames, "readwrite"); @@ -59,13 +65,14 @@ export class Storage { if (this._hasWebkitEarlyCloseTxnBug) { await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY)); } + // @ts-ignore return new Transaction(txn, storeNames, this._IDBKeyRange); } catch(err) { throw new StorageError("readWriteTxn failed", err); } } - close() { + close(): void { this._db.close(); } } diff --git a/src/matrix/storage/idb/StorageFactory.js b/src/matrix/storage/idb/StorageFactory.js index a9dc8eed..84b699eb 100644 --- a/src/matrix/storage/idb/StorageFactory.js +++ b/src/matrix/storage/idb/StorageFactory.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {Storage} from "./Storage.js"; +import {Storage} from "./Storage"; import { openDatabase, reqAsPromise } from "./utils"; import { exportSession, importSession } from "./export.js"; import { schema } from "./schema.js"; From 34b173a057028c789a786d732ebf0fb406d5344a Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Thu, 12 Aug 2021 13:28:36 -0700 Subject: [PATCH 03/12] Migrate schema to TypeScript --- src/matrix/storage/idb/StorageFactory.js | 2 +- .../storage/idb/{schema.js => schema.ts} | 42 +++++++++++-------- 2 files changed, 26 insertions(+), 18 deletions(-) rename src/matrix/storage/idb/{schema.js => schema.ts} (75%) diff --git a/src/matrix/storage/idb/StorageFactory.js b/src/matrix/storage/idb/StorageFactory.js index 84b699eb..e91c77ca 100644 --- a/src/matrix/storage/idb/StorageFactory.js +++ b/src/matrix/storage/idb/StorageFactory.js @@ -17,7 +17,7 @@ limitations under the License. import {Storage} from "./Storage"; import { openDatabase, reqAsPromise } from "./utils"; import { exportSession, importSession } from "./export.js"; -import { schema } from "./schema.js"; +import { schema } from "./schema"; import { detectWebkitEarlyCloseTxnBug } from "./quirks.js"; const sessionName = sessionId => `hydrogen_session_${sessionId}`; diff --git a/src/matrix/storage/idb/schema.js b/src/matrix/storage/idb/schema.ts similarity index 75% rename from src/matrix/storage/idb/schema.js rename to src/matrix/storage/idb/schema.ts index 7df2cb3e..2ff85747 100644 --- a/src/matrix/storage/idb/schema.js +++ b/src/matrix/storage/idb/schema.ts @@ -1,6 +1,7 @@ -import {iterateCursor, reqAsPromise} from "./utils"; +import {iterateCursor, NOT_DONE, reqAsPromise} from "./utils"; import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../room/members/RoomMember.js"; import {RoomMemberStore} from "./stores/RoomMemberStore"; +import {RoomStateEntry} from "./stores/RoomStateStore"; import {SessionStore} from "./stores/SessionStore"; import {encodeScopeTypeKey} from "./stores/OperationStore"; @@ -20,10 +21,12 @@ export const schema = [ ]; // TODO: how to deal with git merge conflicts of this array? +// TypeScript note: for now, do not bother introducing interfaces / alias +// for old schemas. Just take them as `any`. // how do we deal with schema updates vs existing data migration in a way that //v1 -function createInitialStores(db) { +function createInitialStores(db: IDBDatabase): void { db.createObjectStore("session", {keyPath: "key"}); // any way to make keys unique here? (just use put?) db.createObjectStore("roomSummary", {keyPath: "roomId"}); @@ -40,11 +43,12 @@ function createInitialStores(db) { db.createObjectStore("pendingEvents", {keyPath: "key"}); } //v2 -async function createMemberStore(db, txn) { - const roomMembers = new RoomMemberStore(db.createObjectStore("roomMembers", {keyPath: "key"})); +async function createMemberStore(db: IDBDatabase, txn: IDBTransaction): Promise { + // Cast ok here because only "set" is used + const roomMembers = new RoomMemberStore(db.createObjectStore("roomMembers", {keyPath: "key"}) as any); // migrate existing member state events over const roomState = txn.objectStore("roomState"); - await iterateCursor(roomState.openCursor(), entry => { + await iterateCursor(roomState.openCursor(), entry => { if (entry.event.type === MEMBER_EVENT_TYPE) { roomState.delete(entry.key); const member = RoomMember.fromMemberEvent(entry.roomId, entry.event); @@ -52,10 +56,11 @@ async function createMemberStore(db, txn) { roomMembers.set(member.serialize()); } } + return NOT_DONE; }); } //v3 -async function migrateSession(db, txn) { +async function migrateSession(db: IDBDatabase, txn: IDBTransaction): Promise { const session = txn.objectStore("session"); try { const PRE_MIGRATION_KEY = 1; @@ -63,7 +68,8 @@ async function migrateSession(db, txn) { if (entry) { session.delete(PRE_MIGRATION_KEY); const {syncToken, syncFilterId, serverVersions} = entry.value; - const store = new SessionStore(session); + // Cast ok here because only "set" is used and we don't look into return + const store = new SessionStore(session as any); store.set("sync", {token: syncToken, filterId: syncFilterId}); store.set("serverVersions", serverVersions); } @@ -73,7 +79,7 @@ async function migrateSession(db, txn) { } } //v4 -function createE2EEStores(db) { +function createE2EEStores(db: IDBDatabase): void { db.createObjectStore("userIdentities", {keyPath: "userId"}); const deviceIdentities = db.createObjectStore("deviceIdentities", {keyPath: "key"}); deviceIdentities.createIndex("byCurve25519Key", "curve25519Key", {unique: true}); @@ -86,13 +92,14 @@ function createE2EEStores(db) { } // v5 -async function migrateEncryptionFlag(db, txn) { +async function migrateEncryptionFlag(db: IDBDatabase, txn: IDBTransaction): Promise { // migrate room summary isEncrypted -> encryption prop const roomSummary = txn.objectStore("roomSummary"); const roomState = txn.objectStore("roomState"); - const summaries = []; - await iterateCursor(roomSummary.openCursor(), summary => { + const summaries: any[] = []; + await iterateCursor(roomSummary.openCursor(), summary => { summaries.push(summary); + return NOT_DONE; }); for (const summary of summaries) { const encryptionEntry = await reqAsPromise(roomState.get(`${summary.roomId}|m.room.encryption|`)); @@ -105,31 +112,32 @@ async function migrateEncryptionFlag(db, txn) { } // v6 -function createAccountDataStore(db) { +function createAccountDataStore(db: IDBDatabase): void { db.createObjectStore("accountData", {keyPath: "type"}); } // v7 -function createInviteStore(db) { +function createInviteStore(db: IDBDatabase): void { db.createObjectStore("invites", {keyPath: "roomId"}); } // v8 -function createArchivedRoomSummaryStore(db) { +function createArchivedRoomSummaryStore(db: IDBDatabase): void { db.createObjectStore("archivedRoomSummary", {keyPath: "summary.roomId"}); } // v9 -async function migrateOperationScopeIndex(db, txn) { +async function migrateOperationScopeIndex(db: IDBDatabase, txn: IDBTransaction): Promise { try { const operations = txn.objectStore("operations"); operations.deleteIndex("byTypeAndScope"); - await iterateCursor(operations.openCursor(), (op, key, cur) => { + await iterateCursor(operations.openCursor(), (op, key, cur) => { const {typeScopeKey} = op; delete op.typeScopeKey; const [type, scope] = typeScopeKey.split("|"); op.scopeTypeKey = encodeScopeTypeKey(scope, type); cur.update(op); + return NOT_DONE; }); operations.createIndex("byScopeAndType", "scopeTypeKey", {unique: false}); } catch (err) { @@ -139,6 +147,6 @@ async function migrateOperationScopeIndex(db, txn) { } //v10 -function createTimelineRelationsStore(db) { +function createTimelineRelationsStore(db: IDBDatabase) : void { db.createObjectStore("timelineRelations", {keyPath: "key"}); } From 04e39ef9e2378b686a76d795f107d5f8a3c4fa63 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Fri, 13 Aug 2021 10:04:03 -0700 Subject: [PATCH 04/12] Migrate quirks to TypeScript --- src/matrix/storage/idb/StorageFactory.js | 2 +- src/matrix/storage/idb/{quirks.js => quirks.ts} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/matrix/storage/idb/{quirks.js => quirks.ts} (94%) diff --git a/src/matrix/storage/idb/StorageFactory.js b/src/matrix/storage/idb/StorageFactory.js index e91c77ca..259c8c0f 100644 --- a/src/matrix/storage/idb/StorageFactory.js +++ b/src/matrix/storage/idb/StorageFactory.js @@ -18,7 +18,7 @@ import {Storage} from "./Storage"; import { openDatabase, reqAsPromise } from "./utils"; import { exportSession, importSession } from "./export.js"; import { schema } from "./schema"; -import { detectWebkitEarlyCloseTxnBug } from "./quirks.js"; +import { detectWebkitEarlyCloseTxnBug } from "./quirks"; const sessionName = sessionId => `hydrogen_session_${sessionId}`; const openDatabaseWithSessionId = function(sessionId, idbFactory) { diff --git a/src/matrix/storage/idb/quirks.js b/src/matrix/storage/idb/quirks.ts similarity index 94% rename from src/matrix/storage/idb/quirks.js rename to src/matrix/storage/idb/quirks.ts index 5eaa6836..e739da84 100644 --- a/src/matrix/storage/idb/quirks.js +++ b/src/matrix/storage/idb/quirks.ts @@ -18,7 +18,7 @@ limitations under the License. import {openDatabase, txnAsPromise, reqAsPromise} from "./utils"; // filed as https://bugs.webkit.org/show_bug.cgi?id=222746 -export async function detectWebkitEarlyCloseTxnBug(idbFactory) { +export async function detectWebkitEarlyCloseTxnBug(idbFactory: IDBFactory): Promise { const dbName = "hydrogen_webkit_test_inactive_txn_bug"; try { const db = await openDatabase(dbName, db => { From 5db9d1493aea79ee230a2e0bbe2966058ecf6cbe Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Thu, 12 Aug 2021 13:20:37 -0700 Subject: [PATCH 05/12] Migrate export to TypeScript --- src/matrix/storage/idb/StorageFactory.js | 2 +- src/matrix/storage/idb/{export.js => export.ts} | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) rename src/matrix/storage/idb/{export.js => export.ts} (73%) diff --git a/src/matrix/storage/idb/StorageFactory.js b/src/matrix/storage/idb/StorageFactory.js index 259c8c0f..fa5bbabc 100644 --- a/src/matrix/storage/idb/StorageFactory.js +++ b/src/matrix/storage/idb/StorageFactory.js @@ -16,7 +16,7 @@ limitations under the License. import {Storage} from "./Storage"; import { openDatabase, reqAsPromise } from "./utils"; -import { exportSession, importSession } from "./export.js"; +import { exportSession, importSession } from "./export"; import { schema } from "./schema"; import { detectWebkitEarlyCloseTxnBug } from "./quirks"; diff --git a/src/matrix/storage/idb/export.js b/src/matrix/storage/idb/export.ts similarity index 73% rename from src/matrix/storage/idb/export.js rename to src/matrix/storage/idb/export.ts index 27979ce0..c2880f3d 100644 --- a/src/matrix/storage/idb/export.js +++ b/src/matrix/storage/idb/export.ts @@ -14,17 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { iterateCursor, txnAsPromise } from "./utils"; +import { iterateCursor, NOT_DONE, txnAsPromise } from "./utils"; import { STORE_NAMES } from "../common"; -export async function exportSession(db) { - const NOT_DONE = {done: false}; +export async function exportSession(db: IDBDatabase): Promise<{ [storeName : string] : any }> { const txn = db.transaction(STORE_NAMES, "readonly"); const data = {}; await Promise.all(STORE_NAMES.map(async name => { - const results = data[name] = []; // initialize in deterministic order + const results: any[] = data[name] = []; // initialize in deterministic order const store = txn.objectStore(name); - await iterateCursor(store.openCursor(), (value) => { + await iterateCursor(store.openCursor(), (value) => { results.push(value); return NOT_DONE; }); @@ -32,7 +31,7 @@ export async function exportSession(db) { return data; } -export async function importSession(db, data) { +export async function importSession(db: IDBDatabase, data: { [storeName: string]: any }): Promise { const txn = db.transaction(STORE_NAMES, "readwrite"); for (const name of STORE_NAMES) { const store = txn.objectStore(name); From 9252f3bede549945ba1250c4433dd6ca06af1cf8 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Fri, 13 Aug 2021 10:13:40 -0700 Subject: [PATCH 06/12] Migrate StorageFactory to TypeScript --- .../{StorageFactory.js => StorageFactory.ts} | 27 ++++++++++++------- src/mocks/Storage.js | 4 +-- src/platform/web/Platform.js | 2 +- 3 files changed, 21 insertions(+), 12 deletions(-) rename src/matrix/storage/idb/{StorageFactory.js => StorageFactory.ts} (72%) diff --git a/src/matrix/storage/idb/StorageFactory.js b/src/matrix/storage/idb/StorageFactory.ts similarity index 72% rename from src/matrix/storage/idb/StorageFactory.js rename to src/matrix/storage/idb/StorageFactory.ts index fa5bbabc..ce181a0c 100644 --- a/src/matrix/storage/idb/StorageFactory.js +++ b/src/matrix/storage/idb/StorageFactory.ts @@ -20,12 +20,16 @@ import { exportSession, importSession } from "./export"; import { schema } from "./schema"; import { detectWebkitEarlyCloseTxnBug } from "./quirks"; -const sessionName = sessionId => `hydrogen_session_${sessionId}`; -const openDatabaseWithSessionId = function(sessionId, idbFactory) { +const sessionName = (sessionId: string) => `hydrogen_session_${sessionId}`; +const openDatabaseWithSessionId = function(sessionId: string, idbFactory: IDBFactory): Promise { return openDatabase(sessionName(sessionId), createStores, schema.length, idbFactory); } -async function requestPersistedStorage() { +interface ServiceWorkerHandler { + preventConcurrentSessionAccess: (sessionId: string) => Promise; +} + +async function requestPersistedStorage(): Promise { // don't assume browser so we can run in node with fake-idb const glob = this; if (glob?.navigator?.storage?.persist) { @@ -43,13 +47,17 @@ async function requestPersistedStorage() { } export class StorageFactory { - constructor(serviceWorkerHandler, idbFactory = window.indexedDB, IDBKeyRange = window.IDBKeyRange) { + private _serviceWorkerHandler: ServiceWorkerHandler; + private _idbFactory: IDBFactory; + + constructor(serviceWorkerHandler: ServiceWorkerHandler, idbFactory: IDBFactory = window.indexedDB, IDBKeyRange = window.IDBKeyRange) { this._serviceWorkerHandler = serviceWorkerHandler; this._idbFactory = idbFactory; + // @ts-ignore this._IDBKeyRange = IDBKeyRange; } - async create(sessionId) { + async create(sessionId: string): Promise { await this._serviceWorkerHandler?.preventConcurrentSessionAccess(sessionId); requestPersistedStorage().then(persisted => { // Firefox lies here though, and returns true even if the user denied the request @@ -60,27 +68,28 @@ export class StorageFactory { const hasWebkitEarlyCloseTxnBug = await detectWebkitEarlyCloseTxnBug(this._idbFactory); const db = await openDatabaseWithSessionId(sessionId, this._idbFactory); + // @ts-ignore return new Storage(db, this._IDBKeyRange, hasWebkitEarlyCloseTxnBug); } - delete(sessionId) { + delete(sessionId: string): Promise { const databaseName = sessionName(sessionId); const req = this._idbFactory.deleteDatabase(databaseName); return reqAsPromise(req); } - async export(sessionId) { + async export(sessionId: string): Promise<{ [storeName: string]: any }> { const db = await openDatabaseWithSessionId(sessionId, this._idbFactory); return await exportSession(db); } - async import(sessionId, data) { + async import(sessionId: string, data: { [storeName: string]: any }): Promise { const db = await openDatabaseWithSessionId(sessionId, this._idbFactory); return await importSession(db, data); } } -async function createStores(db, txn, oldVersion, version) { +async function createStores(db: IDBDatabase, txn: IDBTransaction, oldVersion: number | null, version: number): Promise { const startIdx = oldVersion || 0; for(let i = startIdx; i < version; ++i) { diff --git a/src/mocks/Storage.js b/src/mocks/Storage.js index 0cddc7bc..a84126cb 100644 --- a/src/mocks/Storage.js +++ b/src/mocks/Storage.js @@ -15,8 +15,8 @@ limitations under the License. */ import {FDBFactory, FDBKeyRange} from "../../lib/fake-indexeddb/index.js"; -import {StorageFactory} from "../matrix/storage/idb/StorageFactory.js"; +import {StorageFactory} from "../matrix/storage/idb/StorageFactory"; export function createMockStorage() { return new StorageFactory(null, new FDBFactory(), FDBKeyRange).create(1); -} \ No newline at end of file +} diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index 40f47101..1530ed12 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -16,7 +16,7 @@ limitations under the License. import {createFetchRequest} from "./dom/request/fetch.js"; import {xhrRequest} from "./dom/request/xhr.js"; -import {StorageFactory} from "../../matrix/storage/idb/StorageFactory.js"; +import {StorageFactory} from "../../matrix/storage/idb/StorageFactory"; import {SessionInfoStorage} from "../../matrix/sessioninfo/localstorage/SessionInfoStorage.js"; import {SettingsStorage} from "./dom/SettingsStorage.js"; import {Encoding} from "./utils/Encoding.js"; From 4eabb7c07436d1a32da0a2c16c1562e8dc765142 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Tue, 31 Aug 2021 15:32:33 -0700 Subject: [PATCH 07/12] Fix newly emerging type errors in schema --- src/matrix/storage/idb/schema.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index 03b1d1a2..a072e34f 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -1,7 +1,8 @@ import {iterateCursor, NOT_DONE, 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"; +import {SummaryData} from "../../room/RoomSummary"; +import {RoomMemberStore, MemberData} from "./stores/RoomMemberStore"; import {RoomStateEntry} from "./stores/RoomStateStore"; import {SessionStore} from "./stores/SessionStore"; import {encodeScopeTypeKey} from "./stores/OperationStore"; @@ -157,24 +158,26 @@ function createTimelineRelationsStore(db: IDBDatabase) : void { //v11 doesn't change the schema, but ensures all userIdentities have all the roomIds they should (see #470) async function fixMissingRoomsInUserIdentities(db, txn, log) { const roomSummaryStore = txn.objectStore("roomSummary"); - const trackedRoomIds = []; - await iterateCursor(roomSummaryStore.openCursor(), roomSummary => { + const trackedRoomIds: string[] = []; + await iterateCursor(roomSummaryStore.openCursor(), roomSummary => { if (roomSummary.isTrackingMembers) { trackedRoomIds.push(roomSummary.roomId); } + return NOT_DONE; }); const outboundGroupSessionsStore = txn.objectStore("outboundGroupSessions"); - const userIdentitiesStore = txn.objectStore("userIdentities"); + const userIdentitiesStore: IDBObjectStore = txn.objectStore("userIdentities"); const roomMemberStore = txn.objectStore("roomMembers"); for (const roomId of trackedRoomIds) { let foundMissing = false; - const joinedUserIds = []; + const joinedUserIds: string[] = []; const memberRange = IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true); await log.wrap({l: "room", id: roomId}, async log => { - await iterateCursor(roomMemberStore.openCursor(memberRange), member => { + await iterateCursor(roomMemberStore.openCursor(memberRange), member => { if (member.membership === "join") { joinedUserIds.push(member.userId); } + return NOT_DONE; }); log.set("joinedUserIds", joinedUserIds.length); for (const userId of joinedUserIds) { From de3807f69098391145403eb6735fd1fe81872148 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Tue, 31 Aug 2021 15:35:01 -0700 Subject: [PATCH 08/12] Fix IDBKeyRange type --- src/matrix/storage/idb/Storage.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/matrix/storage/idb/Storage.ts b/src/matrix/storage/idb/Storage.ts index 747a9e16..bffdb642 100644 --- a/src/matrix/storage/idb/Storage.ts +++ b/src/matrix/storage/idb/Storage.ts @@ -23,12 +23,12 @@ const WEBKITEARLYCLOSETXNBUG_BOGUS_KEY = "782rh281re38-boguskey"; export class Storage { private _db: IDBDatabase; private _hasWebkitEarlyCloseTxnBug: boolean; + private _IDBKeyRange: typeof IDBKeyRange storeNames: typeof StoreNames; - constructor(idbDatabase: IDBDatabase, IDBKeyRange, hasWebkitEarlyCloseTxnBug: boolean) { + constructor(idbDatabase: IDBDatabase, _IDBKeyRange: typeof IDBKeyRange, hasWebkitEarlyCloseTxnBug: boolean) { this._db = idbDatabase; - // @ts-ignore - this._IDBKeyRange = IDBKeyRange; + this._IDBKeyRange = _IDBKeyRange; this._hasWebkitEarlyCloseTxnBug = hasWebkitEarlyCloseTxnBug; this.storeNames = StoreNames; } @@ -49,7 +49,6 @@ export class Storage { if (this._hasWebkitEarlyCloseTxnBug) { await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY)); } - // @ts-ignore return new Transaction(txn, storeNames, this._IDBKeyRange); } catch(err) { throw new StorageError("readTxn failed", err); @@ -65,7 +64,6 @@ export class Storage { if (this._hasWebkitEarlyCloseTxnBug) { await reqAsPromise(txn.objectStore(storeNames[0]).get(WEBKITEARLYCLOSETXNBUG_BOGUS_KEY)); } - // @ts-ignore return new Transaction(txn, storeNames, this._IDBKeyRange); } catch(err) { throw new StorageError("readWriteTxn failed", err); From 2262e6be306b89074e3edead569cd693107f4584 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Tue, 31 Aug 2021 15:44:03 -0700 Subject: [PATCH 09/12] Use store name enum for saving stores --- src/matrix/storage/idb/Transaction.ts | 47 ++++++++++++++------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/matrix/storage/idb/Transaction.ts b/src/matrix/storage/idb/Transaction.ts index b6fee2f2..73dfb7b3 100644 --- a/src/matrix/storage/idb/Transaction.ts +++ b/src/matrix/storage/idb/Transaction.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {StoreNames} from "../common"; import {txnAsPromise} from "./utils"; import {StorageError} from "../common"; import {Store} from "./Store"; @@ -37,10 +38,10 @@ import {AccountDataStore} from "./stores/AccountDataStore"; export class Transaction { private _txn: IDBTransaction; - private _allowedStoreNames: string[]; - private _stores: { [storeName : string] : any }; + private _allowedStoreNames: StoreNames[]; + private _stores: { [storeName in StoreNames]?: any }; - constructor(txn: IDBTransaction, allowedStoreNames: string[], IDBKeyRange) { + constructor(txn: IDBTransaction, allowedStoreNames: StoreNames[], IDBKeyRange) { this._txn = txn; this._allowedStoreNames = allowedStoreNames; this._stores = {}; @@ -48,7 +49,7 @@ export class Transaction { this.IDBKeyRange = IDBKeyRange; } - _idbStore(name: string): Store { + _idbStore(name: StoreNames): Store { if (!this._allowedStoreNames.includes(name)) { // more specific error? this is a bug, so maybe not ... throw new StorageError(`Invalid store for transaction: ${name}, only ${this._allowedStoreNames.join(", ")} are allowed.`); @@ -56,7 +57,7 @@ export class Transaction { return new Store(this._txn.objectStore(name), this); } - _store(name: string, mapStore: (idbStore: Store) => any): any { + _store(name: StoreNames, mapStore: (idbStore: Store) => any): any { if (!this._stores[name]) { const idbStore = this._idbStore(name); this._stores[name] = mapStore(idbStore); @@ -65,75 +66,75 @@ export class Transaction { } get session(): SessionStore { - return this._store("session", idbStore => new SessionStore(idbStore)); + return this._store(StoreNames.session, idbStore => new SessionStore(idbStore)); } get roomSummary(): RoomSummaryStore { - return this._store("roomSummary", idbStore => new RoomSummaryStore(idbStore)); + return this._store(StoreNames.roomSummary, idbStore => new RoomSummaryStore(idbStore)); } get archivedRoomSummary(): RoomSummaryStore { - return this._store("archivedRoomSummary", idbStore => new RoomSummaryStore(idbStore)); + return this._store(StoreNames.archivedRoomSummary, idbStore => new RoomSummaryStore(idbStore)); } get invites(): InviteStore { - return this._store("invites", idbStore => new InviteStore(idbStore)); + return this._store(StoreNames.invites, idbStore => new InviteStore(idbStore)); } get timelineFragments(): TimelineFragmentStore { - return this._store("timelineFragments", idbStore => new TimelineFragmentStore(idbStore)); + return this._store(StoreNames.timelineFragments, idbStore => new TimelineFragmentStore(idbStore)); } get timelineEvents(): TimelineEventStore { - return this._store("timelineEvents", idbStore => new TimelineEventStore(idbStore)); + return this._store(StoreNames.timelineEvents, idbStore => new TimelineEventStore(idbStore)); } get timelineRelations(): TimelineRelationStore { - return this._store("timelineRelations", idbStore => new TimelineRelationStore(idbStore)); + return this._store(StoreNames.timelineRelations, idbStore => new TimelineRelationStore(idbStore)); } get roomState(): RoomStateStore { - return this._store("roomState", idbStore => new RoomStateStore(idbStore)); + return this._store(StoreNames.roomState, idbStore => new RoomStateStore(idbStore)); } get roomMembers(): RoomMemberStore { - return this._store("roomMembers", idbStore => new RoomMemberStore(idbStore)); + return this._store(StoreNames.roomMembers, idbStore => new RoomMemberStore(idbStore)); } get pendingEvents(): PendingEventStore { - return this._store("pendingEvents", idbStore => new PendingEventStore(idbStore)); + return this._store(StoreNames.pendingEvents, idbStore => new PendingEventStore(idbStore)); } get userIdentities(): UserIdentityStore { - return this._store("userIdentities", idbStore => new UserIdentityStore(idbStore)); + return this._store(StoreNames.userIdentities, idbStore => new UserIdentityStore(idbStore)); } get deviceIdentities(): DeviceIdentityStore { - return this._store("deviceIdentities", idbStore => new DeviceIdentityStore(idbStore)); + return this._store(StoreNames.deviceIdentities, idbStore => new DeviceIdentityStore(idbStore)); } get olmSessions(): OlmSessionStore { - return this._store("olmSessions", idbStore => new OlmSessionStore(idbStore)); + return this._store(StoreNames.olmSessions, idbStore => new OlmSessionStore(idbStore)); } get inboundGroupSessions(): InboundGroupSessionStore { - return this._store("inboundGroupSessions", idbStore => new InboundGroupSessionStore(idbStore)); + return this._store(StoreNames.inboundGroupSessions, idbStore => new InboundGroupSessionStore(idbStore)); } get outboundGroupSessions(): OutboundGroupSessionStore { - return this._store("outboundGroupSessions", idbStore => new OutboundGroupSessionStore(idbStore)); + return this._store(StoreNames.outboundGroupSessions, idbStore => new OutboundGroupSessionStore(idbStore)); } get groupSessionDecryptions(): GroupSessionDecryptionStore { - return this._store("groupSessionDecryptions", idbStore => new GroupSessionDecryptionStore(idbStore)); + return this._store(StoreNames.groupSessionDecryptions, idbStore => new GroupSessionDecryptionStore(idbStore)); } get operations(): OperationStore { - return this._store("operations", idbStore => new OperationStore(idbStore)); + return this._store(StoreNames.operations, idbStore => new OperationStore(idbStore)); } get accountData(): AccountDataStore { - return this._store("accountData", idbStore => new AccountDataStore(idbStore)); + return this._store(StoreNames.accountData, idbStore => new AccountDataStore(idbStore)); } complete(): Promise { From 78fb8fdadf7064014ee4e8d11c6d3865a8f4c199 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Tue, 31 Aug 2021 15:50:57 -0700 Subject: [PATCH 10/12] Make export types more precise --- src/matrix/storage/idb/StorageFactory.ts | 6 +++--- src/matrix/storage/idb/export.ts | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/matrix/storage/idb/StorageFactory.ts b/src/matrix/storage/idb/StorageFactory.ts index 261338a1..8f1e80e2 100644 --- a/src/matrix/storage/idb/StorageFactory.ts +++ b/src/matrix/storage/idb/StorageFactory.ts @@ -16,7 +16,7 @@ limitations under the License. import {Storage} from "./Storage"; import { openDatabase, reqAsPromise } from "./utils"; -import { exportSession, importSession } from "./export"; +import { exportSession, importSession, Export } from "./export"; import { schema } from "./schema"; import { detectWebkitEarlyCloseTxnBug } from "./quirks"; @@ -80,12 +80,12 @@ export class StorageFactory { return reqAsPromise(req); } - async export(sessionId: string): Promise<{ [storeName: string]: any }> { + async export(sessionId: string): Promise { const db = await openDatabaseWithSessionId(sessionId, this._idbFactory); return await exportSession(db); } - async import(sessionId: string, data: { [storeName: string]: any }): Promise { + async import(sessionId: string, data: Export): Promise { const db = await openDatabaseWithSessionId(sessionId, this._idbFactory); return await importSession(db, data); } diff --git a/src/matrix/storage/idb/export.ts b/src/matrix/storage/idb/export.ts index c2880f3d..a9d58ee4 100644 --- a/src/matrix/storage/idb/export.ts +++ b/src/matrix/storage/idb/export.ts @@ -15,9 +15,11 @@ limitations under the License. */ import { iterateCursor, NOT_DONE, txnAsPromise } from "./utils"; -import { STORE_NAMES } from "../common"; +import { STORE_NAMES, StoreNames } from "../common"; -export async function exportSession(db: IDBDatabase): Promise<{ [storeName : string] : any }> { +export type Export = { [storeName in StoreNames] : any[] } + +export async function exportSession(db: IDBDatabase): Promise { const txn = db.transaction(STORE_NAMES, "readonly"); const data = {}; await Promise.all(STORE_NAMES.map(async name => { @@ -28,10 +30,10 @@ export async function exportSession(db: IDBDatabase): Promise<{ [storeName : str return NOT_DONE; }); })); - return data; + return data as Export; } -export async function importSession(db: IDBDatabase, data: { [storeName: string]: any }): Promise { +export async function importSession(db: IDBDatabase, data: Export): Promise { const txn = db.transaction(STORE_NAMES, "readwrite"); for (const name of STORE_NAMES) { const store = txn.objectStore(name); From 5b9fd5de9489080aaae25410ff2e196a7b330236 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Tue, 31 Aug 2021 16:01:13 -0700 Subject: [PATCH 11/12] Import BaseLogger instead of explicitly defining 'any' type --- src/matrix/storage/idb/StorageFactory.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/matrix/storage/idb/StorageFactory.ts b/src/matrix/storage/idb/StorageFactory.ts index 8f1e80e2..6db78c36 100644 --- a/src/matrix/storage/idb/StorageFactory.ts +++ b/src/matrix/storage/idb/StorageFactory.ts @@ -19,11 +19,10 @@ import { openDatabase, reqAsPromise } from "./utils"; import { exportSession, importSession, Export } from "./export"; import { schema } from "./schema"; import { detectWebkitEarlyCloseTxnBug } from "./quirks"; - -type LogType = any +import { BaseLogger } from "../../../logging/BaseLogger.js"; const sessionName = (sessionId: string) => `hydrogen_session_${sessionId}`; -const openDatabaseWithSessionId = function(sessionId: string, idbFactory: IDBFactory, log?: LogType) { +const openDatabaseWithSessionId = function(sessionId: string, idbFactory: IDBFactory, log?: BaseLogger) { const create = (db, txn, oldVersion, version) => createStores(db, txn, oldVersion, version, log); return openDatabase(sessionName(sessionId), create, schema.length, idbFactory); } @@ -60,7 +59,7 @@ export class StorageFactory { this._IDBKeyRange = _IDBKeyRange; } - async create(sessionId: string, log?: LogType): Promise { + async create(sessionId: string, log?: BaseLogger): Promise { await this._serviceWorkerHandler?.preventConcurrentSessionAccess(sessionId); requestPersistedStorage().then(persisted => { // Firefox lies here though, and returns true even if the user denied the request @@ -91,7 +90,7 @@ export class StorageFactory { } } -async function createStores(db: IDBDatabase, txn: IDBTransaction, oldVersion: number | null, version: number, log?: LogType): Promise { +async function createStores(db: IDBDatabase, txn: IDBTransaction, oldVersion: number | null, version: number, log?: BaseLogger): Promise { const startIdx = oldVersion || 0; return log.wrap({l: "storage migration", oldVersion, version}, async log => { for(let i = startIdx; i < version; ++i) { From 36da02c14ec57383b0047e35a5accd41d10c35bd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 6 Sep 2021 13:01:32 +0200 Subject: [PATCH 12/12] use generics here to say return type of method is same as callback --- src/matrix/storage/idb/Transaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/storage/idb/Transaction.ts b/src/matrix/storage/idb/Transaction.ts index 73dfb7b3..ea21b745 100644 --- a/src/matrix/storage/idb/Transaction.ts +++ b/src/matrix/storage/idb/Transaction.ts @@ -57,7 +57,7 @@ export class Transaction { return new Store(this._txn.objectStore(name), this); } - _store(name: StoreNames, mapStore: (idbStore: Store) => any): any { + _store(name: StoreNames, mapStore: (idbStore: Store) => T): T { if (!this._stores[name]) { const idbStore = this._idbStore(name); this._stores[name] = mapStore(idbStore);