Merge pull request #475 from vector-im/snowpack-ts-storage-4

Snowpack + Typescript conversion (Part 4)
This commit is contained in:
Bruno Windels 2021-09-06 13:05:08 +02:00 committed by GitHub
commit ed082c9869
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 229 additions and 197 deletions

View file

@ -14,28 +14,33 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {Transaction} from "./Transaction.js"; import {Transaction} from "./Transaction";
import { STORE_NAMES, StoreNames, StorageError } from "../common"; import { STORE_NAMES, StoreNames, StorageError } from "../common";
import { reqAsPromise } from "./utils"; import { reqAsPromise } from "./utils";
const WEBKITEARLYCLOSETXNBUG_BOGUS_KEY = "782rh281re38-boguskey"; const WEBKITEARLYCLOSETXNBUG_BOGUS_KEY = "782rh281re38-boguskey";
export class Storage { export class Storage {
constructor(idbDatabase, IDBKeyRange, hasWebkitEarlyCloseTxnBug) { private _db: IDBDatabase;
private _hasWebkitEarlyCloseTxnBug: boolean;
private _IDBKeyRange: typeof IDBKeyRange
storeNames: typeof StoreNames;
constructor(idbDatabase: IDBDatabase, _IDBKeyRange: typeof IDBKeyRange, hasWebkitEarlyCloseTxnBug: boolean) {
this._db = idbDatabase; this._db = idbDatabase;
this._IDBKeyRange = IDBKeyRange; this._IDBKeyRange = _IDBKeyRange;
this._hasWebkitEarlyCloseTxnBug = hasWebkitEarlyCloseTxnBug; this._hasWebkitEarlyCloseTxnBug = hasWebkitEarlyCloseTxnBug;
this.storeNames = StoreNames; this.storeNames = StoreNames;
} }
_validateStoreNames(storeNames) { _validateStoreNames(storeNames: StoreNames[]): void {
const idx = storeNames.findIndex(name => !STORE_NAMES.includes(name)); const idx = storeNames.findIndex(name => !STORE_NAMES.includes(name));
if (idx !== -1) { if (idx !== -1) {
throw new StorageError(`Tried top, a transaction unknown store ${storeNames[idx]}`); throw new StorageError(`Tried top, a transaction unknown store ${storeNames[idx]}`);
} }
} }
async readTxn(storeNames) { async readTxn(storeNames: StoreNames[]): Promise<Transaction> {
this._validateStoreNames(storeNames); this._validateStoreNames(storeNames);
try { try {
const txn = this._db.transaction(storeNames, "readonly"); const txn = this._db.transaction(storeNames, "readonly");
@ -50,7 +55,7 @@ export class Storage {
} }
} }
async readWriteTxn(storeNames) { async readWriteTxn(storeNames: StoreNames[]): Promise<Transaction> {
this._validateStoreNames(storeNames); this._validateStoreNames(storeNames);
try { try {
const txn = this._db.transaction(storeNames, "readwrite"); const txn = this._db.transaction(storeNames, "readwrite");
@ -65,7 +70,7 @@ export class Storage {
} }
} }
close() { close(): void {
this._db.close(); this._db.close();
} }
} }

View file

@ -14,19 +14,24 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {Storage} from "./Storage.js"; import {Storage} from "./Storage";
import { openDatabase, reqAsPromise } from "./utils"; import { openDatabase, reqAsPromise } from "./utils";
import { exportSession, importSession } from "./export.js"; import { exportSession, importSession, Export } from "./export";
import { schema } from "./schema.js"; import { schema } from "./schema";
import { detectWebkitEarlyCloseTxnBug } from "./quirks.js"; import { detectWebkitEarlyCloseTxnBug } from "./quirks";
import { BaseLogger } from "../../../logging/BaseLogger.js";
const sessionName = sessionId => `hydrogen_session_${sessionId}`; const sessionName = (sessionId: string) => `hydrogen_session_${sessionId}`;
const openDatabaseWithSessionId = function(sessionId, idbFactory, log) { const openDatabaseWithSessionId = function(sessionId: string, idbFactory: IDBFactory, log?: BaseLogger) {
const create = (db, txn, oldVersion, version) => createStores(db, txn, oldVersion, version, log); const create = (db, txn, oldVersion, version) => createStores(db, txn, oldVersion, version, log);
return openDatabase(sessionName(sessionId), create, schema.length, idbFactory); return openDatabase(sessionName(sessionId), create, schema.length, idbFactory);
} }
async function requestPersistedStorage() { interface ServiceWorkerHandler {
preventConcurrentSessionAccess: (sessionId: string) => Promise<void>;
}
async function requestPersistedStorage(): Promise<boolean> {
// don't assume browser so we can run in node with fake-idb // don't assume browser so we can run in node with fake-idb
const glob = this; const glob = this;
if (glob?.navigator?.storage?.persist) { if (glob?.navigator?.storage?.persist) {
@ -44,13 +49,17 @@ async function requestPersistedStorage() {
} }
export class StorageFactory { export class StorageFactory {
constructor(serviceWorkerHandler, idbFactory = window.indexedDB, IDBKeyRange = window.IDBKeyRange) { private _serviceWorkerHandler: ServiceWorkerHandler;
private _idbFactory: IDBFactory;
private _IDBKeyRange: typeof IDBKeyRange
constructor(serviceWorkerHandler: ServiceWorkerHandler, idbFactory: IDBFactory = window.indexedDB, _IDBKeyRange = window.IDBKeyRange) {
this._serviceWorkerHandler = serviceWorkerHandler; this._serviceWorkerHandler = serviceWorkerHandler;
this._idbFactory = idbFactory; this._idbFactory = idbFactory;
this._IDBKeyRange = IDBKeyRange; this._IDBKeyRange = _IDBKeyRange;
} }
async create(sessionId, log) { async create(sessionId: string, log?: BaseLogger): Promise<Storage> {
await this._serviceWorkerHandler?.preventConcurrentSessionAccess(sessionId); await this._serviceWorkerHandler?.preventConcurrentSessionAccess(sessionId);
requestPersistedStorage().then(persisted => { requestPersistedStorage().then(persisted => {
// Firefox lies here though, and returns true even if the user denied the request // Firefox lies here though, and returns true even if the user denied the request
@ -64,24 +73,24 @@ export class StorageFactory {
return new Storage(db, this._IDBKeyRange, hasWebkitEarlyCloseTxnBug); return new Storage(db, this._IDBKeyRange, hasWebkitEarlyCloseTxnBug);
} }
delete(sessionId) { delete(sessionId: string): Promise<IDBDatabase> {
const databaseName = sessionName(sessionId); const databaseName = sessionName(sessionId);
const req = this._idbFactory.deleteDatabase(databaseName); const req = this._idbFactory.deleteDatabase(databaseName);
return reqAsPromise(req); return reqAsPromise(req);
} }
async export(sessionId) { async export(sessionId: string): Promise<Export> {
const db = await openDatabaseWithSessionId(sessionId, this._idbFactory); const db = await openDatabaseWithSessionId(sessionId, this._idbFactory);
return await exportSession(db); return await exportSession(db);
} }
async import(sessionId, data) { async import(sessionId: string, data: Export): Promise<void> {
const db = await openDatabaseWithSessionId(sessionId, this._idbFactory); const db = await openDatabaseWithSessionId(sessionId, this._idbFactory);
return await importSession(db, data); return await importSession(db, data);
} }
} }
async function createStores(db, txn, oldVersion, version, log) { async function createStores(db: IDBDatabase, txn: IDBTransaction, oldVersion: number | null, version: number, log?: BaseLogger): Promise<void> {
const startIdx = oldVersion || 0; const startIdx = oldVersion || 0;
return log.wrap({l: "storage migration", oldVersion, version}, async log => { return log.wrap({l: "storage migration", oldVersion, version}, async log => {
for(let i = startIdx; i < version; ++i) { for(let i = startIdx; i < version; ++i) {

View file

@ -1,142 +0,0 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {txnAsPromise} from "./utils";
import {StorageError} from "../common";
import {Store} from "./Store";
import {SessionStore} from "./stores/SessionStore";
import {RoomSummaryStore} from "./stores/RoomSummaryStore";
import {InviteStore} from "./stores/InviteStore";
import {TimelineEventStore} from "./stores/TimelineEventStore";
import {TimelineRelationStore} from "./stores/TimelineRelationStore";
import {RoomStateStore} from "./stores/RoomStateStore";
import {RoomMemberStore} from "./stores/RoomMemberStore";
import {TimelineFragmentStore} from "./stores/TimelineFragmentStore";
import {PendingEventStore} from "./stores/PendingEventStore";
import {UserIdentityStore} from "./stores/UserIdentityStore";
import {DeviceIdentityStore} from "./stores/DeviceIdentityStore";
import {OlmSessionStore} from "./stores/OlmSessionStore";
import {InboundGroupSessionStore} from "./stores/InboundGroupSessionStore";
import {OutboundGroupSessionStore} from "./stores/OutboundGroupSessionStore";
import {GroupSessionDecryptionStore} from "./stores/GroupSessionDecryptionStore";
import {OperationStore} from "./stores/OperationStore";
import {AccountDataStore} from "./stores/AccountDataStore";
export class Transaction {
constructor(txn, allowedStoreNames, IDBKeyRange) {
this._txn = txn;
this._allowedStoreNames = allowedStoreNames;
this._stores = {};
this.IDBKeyRange = IDBKeyRange;
}
_idbStore(name) {
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.`);
}
return new Store(this._txn.objectStore(name), this);
}
_store(name, mapStore) {
if (!this._stores[name]) {
const idbStore = this._idbStore(name);
this._stores[name] = mapStore(idbStore);
}
return this._stores[name];
}
get session() {
return this._store("session", idbStore => new SessionStore(idbStore));
}
get roomSummary() {
return this._store("roomSummary", idbStore => new RoomSummaryStore(idbStore));
}
get archivedRoomSummary() {
return this._store("archivedRoomSummary", idbStore => new RoomSummaryStore(idbStore));
}
get invites() {
return this._store("invites", idbStore => new InviteStore(idbStore));
}
get timelineFragments() {
return this._store("timelineFragments", idbStore => new TimelineFragmentStore(idbStore));
}
get timelineEvents() {
return this._store("timelineEvents", idbStore => new TimelineEventStore(idbStore));
}
get timelineRelations() {
return this._store("timelineRelations", idbStore => new TimelineRelationStore(idbStore));
}
get roomState() {
return this._store("roomState", idbStore => new RoomStateStore(idbStore));
}
get roomMembers() {
return this._store("roomMembers", idbStore => new RoomMemberStore(idbStore));
}
get pendingEvents() {
return this._store("pendingEvents", idbStore => new PendingEventStore(idbStore));
}
get userIdentities() {
return this._store("userIdentities", idbStore => new UserIdentityStore(idbStore));
}
get deviceIdentities() {
return this._store("deviceIdentities", idbStore => new DeviceIdentityStore(idbStore));
}
get olmSessions() {
return this._store("olmSessions", idbStore => new OlmSessionStore(idbStore));
}
get inboundGroupSessions() {
return this._store("inboundGroupSessions", idbStore => new InboundGroupSessionStore(idbStore));
}
get outboundGroupSessions() {
return this._store("outboundGroupSessions", idbStore => new OutboundGroupSessionStore(idbStore));
}
get groupSessionDecryptions() {
return this._store("groupSessionDecryptions", idbStore => new GroupSessionDecryptionStore(idbStore));
}
get operations() {
return this._store("operations", idbStore => new OperationStore(idbStore));
}
get accountData() {
return this._store("accountData", idbStore => new AccountDataStore(idbStore));
}
complete() {
return txnAsPromise(this._txn);
}
abort() {
// TODO: should we wrap the exception in a StorageError?
this._txn.abort();
}
}

View file

@ -0,0 +1,148 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {StoreNames} from "../common";
import {txnAsPromise} from "./utils";
import {StorageError} from "../common";
import {Store} from "./Store";
import {SessionStore} from "./stores/SessionStore";
import {RoomSummaryStore} from "./stores/RoomSummaryStore";
import {InviteStore} from "./stores/InviteStore";
import {TimelineEventStore} from "./stores/TimelineEventStore";
import {TimelineRelationStore} from "./stores/TimelineRelationStore";
import {RoomStateStore} from "./stores/RoomStateStore";
import {RoomMemberStore} from "./stores/RoomMemberStore";
import {TimelineFragmentStore} from "./stores/TimelineFragmentStore";
import {PendingEventStore} from "./stores/PendingEventStore";
import {UserIdentityStore} from "./stores/UserIdentityStore";
import {DeviceIdentityStore} from "./stores/DeviceIdentityStore";
import {OlmSessionStore} from "./stores/OlmSessionStore";
import {InboundGroupSessionStore} from "./stores/InboundGroupSessionStore";
import {OutboundGroupSessionStore} from "./stores/OutboundGroupSessionStore";
import {GroupSessionDecryptionStore} from "./stores/GroupSessionDecryptionStore";
import {OperationStore} from "./stores/OperationStore";
import {AccountDataStore} from "./stores/AccountDataStore";
export class Transaction {
private _txn: IDBTransaction;
private _allowedStoreNames: StoreNames[];
private _stores: { [storeName in StoreNames]?: any };
constructor(txn: IDBTransaction, allowedStoreNames: StoreNames[], IDBKeyRange) {
this._txn = txn;
this._allowedStoreNames = allowedStoreNames;
this._stores = {};
// @ts-ignore
this.IDBKeyRange = IDBKeyRange;
}
_idbStore(name: StoreNames): Store<any> {
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.`);
}
return new Store(this._txn.objectStore(name), this);
}
_store<T>(name: StoreNames, mapStore: (idbStore: Store<any>) => T): T {
if (!this._stores[name]) {
const idbStore = this._idbStore(name);
this._stores[name] = mapStore(idbStore);
}
return this._stores[name];
}
get session(): SessionStore {
return this._store(StoreNames.session, idbStore => new SessionStore(idbStore));
}
get roomSummary(): RoomSummaryStore {
return this._store(StoreNames.roomSummary, idbStore => new RoomSummaryStore(idbStore));
}
get archivedRoomSummary(): RoomSummaryStore {
return this._store(StoreNames.archivedRoomSummary, idbStore => new RoomSummaryStore(idbStore));
}
get invites(): InviteStore {
return this._store(StoreNames.invites, idbStore => new InviteStore(idbStore));
}
get timelineFragments(): TimelineFragmentStore {
return this._store(StoreNames.timelineFragments, idbStore => new TimelineFragmentStore(idbStore));
}
get timelineEvents(): TimelineEventStore {
return this._store(StoreNames.timelineEvents, idbStore => new TimelineEventStore(idbStore));
}
get timelineRelations(): TimelineRelationStore {
return this._store(StoreNames.timelineRelations, idbStore => new TimelineRelationStore(idbStore));
}
get roomState(): RoomStateStore {
return this._store(StoreNames.roomState, idbStore => new RoomStateStore(idbStore));
}
get roomMembers(): RoomMemberStore {
return this._store(StoreNames.roomMembers, idbStore => new RoomMemberStore(idbStore));
}
get pendingEvents(): PendingEventStore {
return this._store(StoreNames.pendingEvents, idbStore => new PendingEventStore(idbStore));
}
get userIdentities(): UserIdentityStore {
return this._store(StoreNames.userIdentities, idbStore => new UserIdentityStore(idbStore));
}
get deviceIdentities(): DeviceIdentityStore {
return this._store(StoreNames.deviceIdentities, idbStore => new DeviceIdentityStore(idbStore));
}
get olmSessions(): OlmSessionStore {
return this._store(StoreNames.olmSessions, idbStore => new OlmSessionStore(idbStore));
}
get inboundGroupSessions(): InboundGroupSessionStore {
return this._store(StoreNames.inboundGroupSessions, idbStore => new InboundGroupSessionStore(idbStore));
}
get outboundGroupSessions(): OutboundGroupSessionStore {
return this._store(StoreNames.outboundGroupSessions, idbStore => new OutboundGroupSessionStore(idbStore));
}
get groupSessionDecryptions(): GroupSessionDecryptionStore {
return this._store(StoreNames.groupSessionDecryptions, idbStore => new GroupSessionDecryptionStore(idbStore));
}
get operations(): OperationStore {
return this._store(StoreNames.operations, idbStore => new OperationStore(idbStore));
}
get accountData(): AccountDataStore {
return this._store(StoreNames.accountData, idbStore => new AccountDataStore(idbStore));
}
complete(): Promise<void> {
return txnAsPromise(this._txn);
}
abort(): void {
// TODO: should we wrap the exception in a StorageError?
this._txn.abort();
}
}

View file

@ -14,25 +14,26 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { iterateCursor, txnAsPromise } from "./utils"; import { iterateCursor, NOT_DONE, txnAsPromise } from "./utils";
import { STORE_NAMES } from "../common"; import { STORE_NAMES, StoreNames } from "../common";
export async function exportSession(db) { export type Export = { [storeName in StoreNames] : any[] }
const NOT_DONE = {done: false};
export async function exportSession(db: IDBDatabase): Promise<Export> {
const txn = db.transaction(STORE_NAMES, "readonly"); const txn = db.transaction(STORE_NAMES, "readonly");
const data = {}; const data = {};
await Promise.all(STORE_NAMES.map(async name => { 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); const store = txn.objectStore(name);
await iterateCursor(store.openCursor(), (value) => { await iterateCursor<any>(store.openCursor(), (value) => {
results.push(value); results.push(value);
return NOT_DONE; return NOT_DONE;
}); });
})); }));
return data; return data as Export;
} }
export async function importSession(db, data) { export async function importSession(db: IDBDatabase, data: Export): Promise<void> {
const txn = db.transaction(STORE_NAMES, "readwrite"); const txn = db.transaction(STORE_NAMES, "readwrite");
for (const name of STORE_NAMES) { for (const name of STORE_NAMES) {
const store = txn.objectStore(name); const store = txn.objectStore(name);

View file

@ -18,7 +18,7 @@ limitations under the License.
import {openDatabase, txnAsPromise, reqAsPromise} from "./utils"; import {openDatabase, txnAsPromise, reqAsPromise} from "./utils";
// filed as https://bugs.webkit.org/show_bug.cgi?id=222746 // filed as https://bugs.webkit.org/show_bug.cgi?id=222746
export async function detectWebkitEarlyCloseTxnBug(idbFactory) { export async function detectWebkitEarlyCloseTxnBug(idbFactory: IDBFactory): Promise<boolean> {
const dbName = "hydrogen_webkit_test_inactive_txn_bug"; const dbName = "hydrogen_webkit_test_inactive_txn_bug";
try { try {
const db = await openDatabase(dbName, db => { const db = await openDatabase(dbName, db => {

View file

@ -1,7 +1,9 @@
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 {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../room/members/RoomMember.js";
import {addRoomToIdentity} from "../../e2ee/DeviceTracker.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 {SessionStore} from "./stores/SessionStore";
import {encodeScopeTypeKey} from "./stores/OperationStore"; import {encodeScopeTypeKey} from "./stores/OperationStore";
import {MAX_UNICODE} from "./stores/common"; import {MAX_UNICODE} from "./stores/common";
@ -23,10 +25,12 @@ export const schema = [
]; ];
// TODO: how to deal with git merge conflicts of this array? // 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 // how do we deal with schema updates vs existing data migration in a way that
//v1 //v1
function createInitialStores(db) { function createInitialStores(db: IDBDatabase): void {
db.createObjectStore("session", {keyPath: "key"}); db.createObjectStore("session", {keyPath: "key"});
// any way to make keys unique here? (just use put?) // any way to make keys unique here? (just use put?)
db.createObjectStore("roomSummary", {keyPath: "roomId"}); db.createObjectStore("roomSummary", {keyPath: "roomId"});
@ -43,11 +47,12 @@ function createInitialStores(db) {
db.createObjectStore("pendingEvents", {keyPath: "key"}); db.createObjectStore("pendingEvents", {keyPath: "key"});
} }
//v2 //v2
async function createMemberStore(db, txn) { async function createMemberStore(db: IDBDatabase, txn: IDBTransaction): Promise<void> {
const roomMembers = new RoomMemberStore(db.createObjectStore("roomMembers", {keyPath: "key"})); // 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 // migrate existing member state events over
const roomState = txn.objectStore("roomState"); const roomState = txn.objectStore("roomState");
await iterateCursor(roomState.openCursor(), entry => { await iterateCursor<RoomStateEntry>(roomState.openCursor(), entry => {
if (entry.event.type === MEMBER_EVENT_TYPE) { if (entry.event.type === MEMBER_EVENT_TYPE) {
roomState.delete(entry.key); roomState.delete(entry.key);
const member = RoomMember.fromMemberEvent(entry.roomId, entry.event); const member = RoomMember.fromMemberEvent(entry.roomId, entry.event);
@ -55,10 +60,11 @@ async function createMemberStore(db, txn) {
roomMembers.set(member.serialize()); roomMembers.set(member.serialize());
} }
} }
return NOT_DONE;
}); });
} }
//v3 //v3
async function migrateSession(db, txn) { async function migrateSession(db: IDBDatabase, txn: IDBTransaction): Promise<void> {
const session = txn.objectStore("session"); const session = txn.objectStore("session");
try { try {
const PRE_MIGRATION_KEY = 1; const PRE_MIGRATION_KEY = 1;
@ -66,7 +72,8 @@ async function migrateSession(db, txn) {
if (entry) { if (entry) {
session.delete(PRE_MIGRATION_KEY); session.delete(PRE_MIGRATION_KEY);
const {syncToken, syncFilterId, serverVersions} = entry.value; 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("sync", {token: syncToken, filterId: syncFilterId});
store.set("serverVersions", serverVersions); store.set("serverVersions", serverVersions);
} }
@ -76,7 +83,7 @@ async function migrateSession(db, txn) {
} }
} }
//v4 //v4
function createE2EEStores(db) { function createE2EEStores(db: IDBDatabase): void {
db.createObjectStore("userIdentities", {keyPath: "userId"}); db.createObjectStore("userIdentities", {keyPath: "userId"});
const deviceIdentities = db.createObjectStore("deviceIdentities", {keyPath: "key"}); const deviceIdentities = db.createObjectStore("deviceIdentities", {keyPath: "key"});
deviceIdentities.createIndex("byCurve25519Key", "curve25519Key", {unique: true}); deviceIdentities.createIndex("byCurve25519Key", "curve25519Key", {unique: true});
@ -89,13 +96,14 @@ function createE2EEStores(db) {
} }
// v5 // v5
async function migrateEncryptionFlag(db, txn) { async function migrateEncryptionFlag(db: IDBDatabase, txn: IDBTransaction): Promise<void> {
// migrate room summary isEncrypted -> encryption prop // migrate room summary isEncrypted -> encryption prop
const roomSummary = txn.objectStore("roomSummary"); const roomSummary = txn.objectStore("roomSummary");
const roomState = txn.objectStore("roomState"); const roomState = txn.objectStore("roomState");
const summaries = []; const summaries: any[] = [];
await iterateCursor(roomSummary.openCursor(), summary => { await iterateCursor<any>(roomSummary.openCursor(), summary => {
summaries.push(summary); summaries.push(summary);
return NOT_DONE;
}); });
for (const summary of summaries) { for (const summary of summaries) {
const encryptionEntry = await reqAsPromise(roomState.get(`${summary.roomId}|m.room.encryption|`)); const encryptionEntry = await reqAsPromise(roomState.get(`${summary.roomId}|m.room.encryption|`));
@ -108,31 +116,32 @@ async function migrateEncryptionFlag(db, txn) {
} }
// v6 // v6
function createAccountDataStore(db) { function createAccountDataStore(db: IDBDatabase): void {
db.createObjectStore("accountData", {keyPath: "type"}); db.createObjectStore("accountData", {keyPath: "type"});
} }
// v7 // v7
function createInviteStore(db) { function createInviteStore(db: IDBDatabase): void {
db.createObjectStore("invites", {keyPath: "roomId"}); db.createObjectStore("invites", {keyPath: "roomId"});
} }
// v8 // v8
function createArchivedRoomSummaryStore(db) { function createArchivedRoomSummaryStore(db: IDBDatabase): void {
db.createObjectStore("archivedRoomSummary", {keyPath: "summary.roomId"}); db.createObjectStore("archivedRoomSummary", {keyPath: "summary.roomId"});
} }
// v9 // v9
async function migrateOperationScopeIndex(db, txn) { async function migrateOperationScopeIndex(db: IDBDatabase, txn: IDBTransaction): Promise<void> {
try { try {
const operations = txn.objectStore("operations"); const operations = txn.objectStore("operations");
operations.deleteIndex("byTypeAndScope"); operations.deleteIndex("byTypeAndScope");
await iterateCursor(operations.openCursor(), (op, key, cur) => { await iterateCursor<any>(operations.openCursor(), (op, key, cur) => {
const {typeScopeKey} = op; const {typeScopeKey} = op;
delete op.typeScopeKey; delete op.typeScopeKey;
const [type, scope] = typeScopeKey.split("|"); const [type, scope] = typeScopeKey.split("|");
op.scopeTypeKey = encodeScopeTypeKey(scope, type); op.scopeTypeKey = encodeScopeTypeKey(scope, type);
cur.update(op); cur.update(op);
return NOT_DONE;
}); });
operations.createIndex("byScopeAndType", "scopeTypeKey", {unique: false}); operations.createIndex("byScopeAndType", "scopeTypeKey", {unique: false});
} catch (err) { } catch (err) {
@ -142,31 +151,33 @@ async function migrateOperationScopeIndex(db, txn) {
} }
//v10 //v10
function createTimelineRelationsStore(db) { function createTimelineRelationsStore(db: IDBDatabase) : void {
db.createObjectStore("timelineRelations", {keyPath: "key"}); db.createObjectStore("timelineRelations", {keyPath: "key"});
} }
//v11 doesn't change the schema, but ensures all userIdentities have all the roomIds they should (see #470) //v11 doesn't change the schema, but ensures all userIdentities have all the roomIds they should (see #470)
async function fixMissingRoomsInUserIdentities(db, txn, log) { async function fixMissingRoomsInUserIdentities(db, txn, log) {
const roomSummaryStore = txn.objectStore("roomSummary"); const roomSummaryStore = txn.objectStore("roomSummary");
const trackedRoomIds = []; const trackedRoomIds: string[] = [];
await iterateCursor(roomSummaryStore.openCursor(), roomSummary => { await iterateCursor<SummaryData>(roomSummaryStore.openCursor(), roomSummary => {
if (roomSummary.isTrackingMembers) { if (roomSummary.isTrackingMembers) {
trackedRoomIds.push(roomSummary.roomId); trackedRoomIds.push(roomSummary.roomId);
} }
return NOT_DONE;
}); });
const outboundGroupSessionsStore = txn.objectStore("outboundGroupSessions"); const outboundGroupSessionsStore = txn.objectStore("outboundGroupSessions");
const userIdentitiesStore = txn.objectStore("userIdentities"); const userIdentitiesStore: IDBObjectStore = txn.objectStore("userIdentities");
const roomMemberStore = txn.objectStore("roomMembers"); const roomMemberStore = txn.objectStore("roomMembers");
for (const roomId of trackedRoomIds) { for (const roomId of trackedRoomIds) {
let foundMissing = false; let foundMissing = false;
const joinedUserIds = []; const joinedUserIds: string[] = [];
const memberRange = IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true); const memberRange = IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true);
await log.wrap({l: "room", id: roomId}, async log => { await log.wrap({l: "room", id: roomId}, async log => {
await iterateCursor(roomMemberStore.openCursor(memberRange), member => { await iterateCursor<MemberData>(roomMemberStore.openCursor(memberRange), member => {
if (member.membership === "join") { if (member.membership === "join") {
joinedUserIds.push(member.userId); joinedUserIds.push(member.userId);
} }
return NOT_DONE;
}); });
log.set("joinedUserIds", joinedUserIds.length); log.set("joinedUserIds", joinedUserIds.length);
for (const userId of joinedUserIds) { for (const userId of joinedUserIds) {

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import {FDBFactory, FDBKeyRange} from "../../lib/fake-indexeddb/index.js"; import {FDBFactory, FDBKeyRange} from "../../lib/fake-indexeddb/index.js";
import {StorageFactory} from "../matrix/storage/idb/StorageFactory.js"; import {StorageFactory} from "../matrix/storage/idb/StorageFactory";
import {NullLogItem} from "../logging/NullLogger.js"; import {NullLogItem} from "../logging/NullLogger.js";
export function createMockStorage() { export function createMockStorage() {

View file

@ -16,7 +16,7 @@ limitations under the License.
import {createFetchRequest} from "./dom/request/fetch.js"; import {createFetchRequest} from "./dom/request/fetch.js";
import {xhrRequest} from "./dom/request/xhr.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 {SessionInfoStorage} from "../../matrix/sessioninfo/localstorage/SessionInfoStorage.js";
import {SettingsStorage} from "./dom/SettingsStorage.js"; import {SettingsStorage} from "./dom/SettingsStorage.js";
import {Encoding} from "./utils/Encoding.js"; import {Encoding} from "./utils/Encoding.js";