diff options
author | RaindropsSys <contact@minteck.org> | 2023-04-24 14:03:36 +0200 |
---|---|---|
committer | RaindropsSys <contact@minteck.org> | 2023-04-24 14:03:36 +0200 |
commit | 633c92eae865e957121e08de634aeee11a8b3992 (patch) | |
tree | 09d881bee1dae0b6eee49db1dfaf0f500240606c /includes/external/matrix/node_modules/matrix-js-sdk/src/store | |
parent | c4657e4509733699c0f26a3c900bab47e915d5a0 (diff) | |
download | pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.tar.gz pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.tar.bz2 pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.zip |
Updated 18 files, added 1692 files and deleted includes/system/compare.inc (automated)
Diffstat (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/src/store')
9 files changed, 2377 insertions, 0 deletions
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/index.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/index.ts new file mode 100644 index 0000000..650dd9a --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/index.ts @@ -0,0 +1,248 @@ +/* +Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. + +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 { EventType } from "../@types/event"; +import { Room } from "../models/room"; +import { User } from "../models/user"; +import { IEvent, MatrixEvent } from "../models/event"; +import { Filter } from "../filter"; +import { RoomSummary } from "../models/room-summary"; +import { IMinimalEvent, IRooms, ISyncResponse } from "../sync-accumulator"; +import { IStartClientOpts } from "../client"; +import { IStateEventWithRoomId } from "../@types/search"; +import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage"; +import { EventEmitterEvents } from "../models/typed-event-emitter"; + +export interface ISavedSync { + nextBatch: string; + roomsData: IRooms; + accountData: IMinimalEvent[]; +} + +/** + * A store for most of the data js-sdk needs to store, apart from crypto data + */ +export interface IStore { + readonly accountData: Map<string, MatrixEvent>; // type : content + + // XXX: The indexeddb store exposes a non-standard emitter for: + // "degraded" event for when it falls back to being a memory store due to errors. + // "closed" event for when the database closes unexpectedly + on?: (event: EventEmitterEvents | "degraded" | "closed", handler: (...args: any[]) => void) => void; + + /** @returns whether or not the database was newly created in this session. */ + isNewlyCreated(): Promise<boolean>; + + /** + * Get the sync token. + */ + getSyncToken(): string | null; + + /** + * Set the sync token. + */ + setSyncToken(token: string): void; + + /** + * Store the given room. + * @param room - The room to be stored. All properties must be stored. + */ + storeRoom(room: Room): void; + + /** + * Retrieve a room by its' room ID. + * @param roomId - The room ID. + * @returns The room or null. + */ + getRoom(roomId: string): Room | null; + + /** + * Retrieve all known rooms. + * @returns A list of rooms, which may be empty. + */ + getRooms(): Room[]; + + /** + * Permanently delete a room. + */ + removeRoom(roomId: string): void; + + /** + * Retrieve a summary of all the rooms. + * @returns A summary of each room. + */ + getRoomSummaries(): RoomSummary[]; + + /** + * Store a User. + * @param user - The user to store. + */ + storeUser(user: User): void; + + /** + * Retrieve a User by its' user ID. + * @param userId - The user ID. + * @returns The user or null. + */ + getUser(userId: string): User | null; + + /** + * Retrieve all known users. + * @returns A list of users, which may be empty. + */ + getUsers(): User[]; + + /** + * Retrieve scrollback for this room. + * @param room - The matrix room + * @param limit - The max number of old events to retrieve. + * @returns An array of objects which will be at most 'limit' + * length and at least 0. The objects are the raw event JSON. + */ + scrollback(room: Room, limit: number): MatrixEvent[]; + + /** + * Store events for a room. + * @param room - The room to store events for. + * @param events - The events to store. + * @param token - The token associated with these events. + * @param toStart - True if these are paginated results. + */ + storeEvents(room: Room, events: MatrixEvent[], token: string | null, toStart: boolean): void; + + /** + * Store a filter. + */ + storeFilter(filter: Filter): void; + + /** + * Retrieve a filter. + * @returns A filter or null. + */ + getFilter(userId: string, filterId: string): Filter | null; + + /** + * Retrieve a filter ID with the given name. + * @param filterName - The filter name. + * @returns The filter ID or null. + */ + getFilterIdByName(filterName: string): string | null; + + /** + * Set a filter name to ID mapping. + */ + setFilterIdByName(filterName: string, filterId?: string): void; + + /** + * Store user-scoped account data events + * @param events - The events to store. + */ + storeAccountDataEvents(events: MatrixEvent[]): void; + + /** + * Get account data event by event type + * @param eventType - The event type being queried + */ + getAccountData(eventType: EventType | string): MatrixEvent | undefined; + + /** + * setSyncData does nothing as there is no backing data store. + * + * @param syncData - The sync data + * @returns An immediately resolved promise. + */ + setSyncData(syncData: ISyncResponse): Promise<void>; + + /** + * We never want to save because we have nothing to save to. + * + * @returns If the store wants to save + */ + wantsSave(): boolean; + + /** + * Save does nothing as there is no backing data store. + */ + save(force?: boolean): void; + + /** + * Startup does nothing. + * @returns An immediately resolved promise. + */ + startup(): Promise<void>; + + /** + * @returns Promise which resolves with a sync response to restore the + * client state to where it was at the last save, or null if there + * is no saved sync data. + */ + getSavedSync(): Promise<ISavedSync | null>; + + /** + * @returns If there is a saved sync, the nextBatch token + * for this sync, otherwise null. + */ + getSavedSyncToken(): Promise<string | null>; + + /** + * Delete all data from this store. Does nothing since this store + * doesn't store anything. + * @returns An immediately resolved promise. + */ + deleteAllData(): Promise<void>; + + /** + * Returns the out-of-band membership events for this room that + * were previously loaded. + * @returns the events, potentially an empty array if OOB loading didn't yield any new members + * @returns in case the members for this room haven't been stored yet + */ + getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null>; + + /** + * Stores the out-of-band membership events for this room. Note that + * it still makes sense to store an empty array as the OOB status for the room is + * marked as fetched, and getOutOfBandMembers will return an empty array instead of null + * @param membershipEvents - the membership events to store + * @returns when all members have been stored + */ + setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void>; + + clearOutOfBandMembers(roomId: string): Promise<void>; + + getClientOptions(): Promise<IStartClientOpts | undefined>; + + storeClientOptions(options: IStartClientOpts): Promise<void>; + + getPendingEvents(roomId: string): Promise<Partial<IEvent>[]>; + + setPendingEvents(roomId: string, events: Partial<IEvent>[]): Promise<void>; + + /** + * Stores batches of outgoing to-device messages + */ + saveToDeviceBatches(batch: ToDeviceBatchWithTxnId[]): Promise<void>; + + /** + * Fetches the oldest batch of to-device messages in the queue + */ + getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null>; + + /** + * Removes a specific batch of to-device messages from the queue + */ + removeToDeviceBatch(id: number): Promise<void>; +} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-backend.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-backend.ts new file mode 100644 index 0000000..008867d --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-backend.ts @@ -0,0 +1,40 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +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 { ISavedSync } from "./index"; +import { IEvent, IStateEventWithRoomId, IStoredClientOpts, ISyncResponse } from "../matrix"; +import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage"; + +export interface IIndexedDBBackend { + connect(onClose?: () => void): Promise<void>; + syncToDatabase(userTuples: UserTuple[]): Promise<void>; + isNewlyCreated(): Promise<boolean>; + setSyncData(syncData: ISyncResponse): Promise<void>; + getSavedSync(): Promise<ISavedSync | null>; + getNextBatchToken(): Promise<string>; + clearDatabase(): Promise<void>; + getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null>; + setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void>; + clearOutOfBandMembers(roomId: string): Promise<void>; + getUserPresenceEvents(): Promise<UserTuple[]>; + getClientOptions(): Promise<IStoredClientOpts | undefined>; + storeClientOptions(options: IStoredClientOpts): Promise<void>; + saveToDeviceBatches(batches: ToDeviceBatchWithTxnId[]): Promise<void>; + getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null>; + removeToDeviceBatch(id: number): Promise<void>; +} + +export type UserTuple = [userId: string, presenceEvent: Partial<IEvent>]; diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-local-backend.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-local-backend.ts new file mode 100644 index 0000000..80fed44 --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-local-backend.ts @@ -0,0 +1,597 @@ +/* +Copyright 2017 - 2021 The Matrix.org Foundation C.I.C. + +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 { IMinimalEvent, ISyncData, ISyncResponse, SyncAccumulator } from "../sync-accumulator"; +import * as utils from "../utils"; +import * as IndexedDBHelpers from "../indexeddb-helpers"; +import { logger } from "../logger"; +import { IStateEventWithRoomId, IStoredClientOpts } from "../matrix"; +import { ISavedSync } from "./index"; +import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend"; +import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage"; + +type DbMigration = (db: IDBDatabase) => void; +const DB_MIGRATIONS: DbMigration[] = [ + (db): void => { + // Make user store, clobber based on user ID. (userId property of User objects) + db.createObjectStore("users", { keyPath: ["userId"] }); + + // Make account data store, clobber based on event type. + // (event.type property of MatrixEvent objects) + db.createObjectStore("accountData", { keyPath: ["type"] }); + + // Make /sync store (sync tokens, room data, etc), always clobber (const key). + db.createObjectStore("sync", { keyPath: ["clobber"] }); + }, + (db): void => { + const oobMembersStore = db.createObjectStore("oob_membership_events", { + keyPath: ["room_id", "state_key"], + }); + oobMembersStore.createIndex("room", "room_id"); + }, + (db): void => { + db.createObjectStore("client_options", { keyPath: ["clobber"] }); + }, + (db): void => { + db.createObjectStore("to_device_queue", { autoIncrement: true }); + }, + // Expand as needed. +]; +const VERSION = DB_MIGRATIONS.length; + +/** + * Helper method to collect results from a Cursor and promiseify it. + * @param store - The store to perform openCursor on. + * @param keyRange - Optional key range to apply on the cursor. + * @param resultMapper - A function which is repeatedly called with a + * Cursor. + * Return the data you want to keep. + * @returns Promise which resolves to an array of whatever you returned from + * resultMapper. + */ +function selectQuery<T>( + store: IDBObjectStore, + keyRange: IDBKeyRange | IDBValidKey | undefined, + resultMapper: (cursor: IDBCursorWithValue) => T, +): Promise<T[]> { + const query = store.openCursor(keyRange); + return new Promise((resolve, reject) => { + const results: T[] = []; + query.onerror = (): void => { + reject(new Error("Query failed: " + query.error)); + }; + // collect results + query.onsuccess = (): void => { + const cursor = query.result; + if (!cursor) { + resolve(results); + return; // end of results + } + results.push(resultMapper(cursor)); + cursor.continue(); + }; + }); +} + +function txnAsPromise(txn: IDBTransaction): Promise<Event> { + return new Promise((resolve, reject) => { + txn.oncomplete = function (event): void { + resolve(event); + }; + txn.onerror = function (): void { + reject(txn.error); + }; + }); +} + +function reqAsEventPromise(req: IDBRequest): Promise<Event> { + return new Promise((resolve, reject) => { + req.onsuccess = function (event): void { + resolve(event); + }; + req.onerror = function (): void { + reject(req.error); + }; + }); +} + +function reqAsPromise(req: IDBRequest): Promise<IDBRequest> { + return new Promise((resolve, reject) => { + req.onsuccess = (): void => resolve(req); + req.onerror = (err): void => reject(err); + }); +} + +function reqAsCursorPromise<T>(req: IDBRequest<T>): Promise<T> { + return reqAsEventPromise(req).then((event) => req.result); +} + +export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { + public static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> { + dbName = "matrix-js-sdk:" + (dbName || "default"); + return IndexedDBHelpers.exists(indexedDB, dbName); + } + + private readonly dbName: string; + private readonly syncAccumulator: SyncAccumulator; + private db?: IDBDatabase; + private disconnected = true; + private _isNewlyCreated = false; + private syncToDatabasePromise?: Promise<void>; + private pendingUserPresenceData: UserTuple[] = []; + + /** + * Does the actual reading from and writing to the indexeddb + * + * Construct a new Indexed Database store backend. This requires a call to + * `connect()` before this store can be used. + * @param indexedDB - The Indexed DB interface e.g + * `window.indexedDB` + * @param dbName - Optional database name. The same name must be used + * to open the same database. + */ + public constructor(private readonly indexedDB: IDBFactory, dbName = "default") { + this.dbName = "matrix-js-sdk:" + dbName; + this.syncAccumulator = new SyncAccumulator(); + } + + /** + * Attempt to connect to the database. This can fail if the user does not + * grant permission. + * @returns Promise which resolves if successfully connected. + */ + public connect(onClose?: () => void): Promise<void> { + if (!this.disconnected) { + logger.log(`LocalIndexedDBStoreBackend.connect: already connected or connecting`); + return Promise.resolve(); + } + + this.disconnected = false; + + logger.log(`LocalIndexedDBStoreBackend.connect: connecting...`); + const req = this.indexedDB.open(this.dbName, VERSION); + req.onupgradeneeded = (ev): void => { + const db = req.result; + const oldVersion = ev.oldVersion; + logger.log(`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`); + if (oldVersion < 1) { + // The database did not previously exist + this._isNewlyCreated = true; + } + DB_MIGRATIONS.forEach((migration, index) => { + if (oldVersion <= index) migration(db); + }); + }; + + req.onblocked = (): void => { + logger.log(`can't yet open LocalIndexedDBStoreBackend because it is open elsewhere`); + }; + + logger.log(`LocalIndexedDBStoreBackend.connect: awaiting connection...`); + return reqAsEventPromise(req).then(async () => { + logger.log(`LocalIndexedDBStoreBackend.connect: connected`); + this.db = req.result; + + // add a poorly-named listener for when deleteDatabase is called + // so we can close our db connections. + this.db.onversionchange = (): void => { + this.db?.close(); // this does not call onclose + this.disconnected = true; + this.db = undefined; + onClose?.(); + }; + this.db.onclose = (): void => { + this.disconnected = true; + this.db = undefined; + onClose?.(); + }; + + await this.init(); + }); + } + + /** @returns whether or not the database was newly created in this session. */ + public isNewlyCreated(): Promise<boolean> { + return Promise.resolve(this._isNewlyCreated); + } + + /** + * Having connected, load initial data from the database and prepare for use + * @returns Promise which resolves on success + */ + private init(): Promise<unknown> { + return Promise.all([this.loadAccountData(), this.loadSyncData()]).then(([accountData, syncData]) => { + logger.log(`LocalIndexedDBStoreBackend: loaded initial data`); + this.syncAccumulator.accumulate( + { + next_batch: syncData.nextBatch, + rooms: syncData.roomsData, + account_data: { + events: accountData, + }, + }, + true, + ); + }); + } + + /** + * Returns the out-of-band membership events for this room that + * were previously loaded. + * @returns the events, potentially an empty array if OOB loading didn't yield any new members + * @returns in case the members for this room haven't been stored yet + */ + public getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null> { + return new Promise<IStateEventWithRoomId[] | null>((resolve, reject) => { + const tx = this.db!.transaction(["oob_membership_events"], "readonly"); + const store = tx.objectStore("oob_membership_events"); + const roomIndex = store.index("room"); + const range = IDBKeyRange.only(roomId); + const request = roomIndex.openCursor(range); + + const membershipEvents: IStateEventWithRoomId[] = []; + // did we encounter the oob_written marker object + // amongst the results? That means OOB member + // loading already happened for this room + // but there were no members to persist as they + // were all known already + let oobWritten = false; + + request.onsuccess = (): void => { + const cursor = request.result; + if (!cursor) { + // Unknown room + if (!membershipEvents.length && !oobWritten) { + return resolve(null); + } + return resolve(membershipEvents); + } + const record = cursor.value; + if (record.oob_written) { + oobWritten = true; + } else { + membershipEvents.push(record); + } + cursor.continue(); + }; + request.onerror = (err): void => { + reject(err); + }; + }).then((events) => { + logger.log(`LL: got ${events?.length} membershipEvents from storage for room ${roomId} ...`); + return events; + }); + } + + /** + * Stores the out-of-band membership events for this room. Note that + * it still makes sense to store an empty array as the OOB status for the room is + * marked as fetched, and getOutOfBandMembers will return an empty array instead of null + * @param membershipEvents - the membership events to store + */ + public async setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> { + logger.log(`LL: backend about to store ${membershipEvents.length}` + ` members for ${roomId}`); + const tx = this.db!.transaction(["oob_membership_events"], "readwrite"); + const store = tx.objectStore("oob_membership_events"); + membershipEvents.forEach((e) => { + store.put(e); + }); + // aside from all the events, we also write a marker object to the store + // to mark the fact that OOB members have been written for this room. + // It's possible that 0 members need to be written as all where previously know + // but we still need to know whether to return null or [] from getOutOfBandMembers + // where null means out of band members haven't been stored yet for this room + const markerObject = { + room_id: roomId, + oob_written: true, + state_key: 0, + }; + store.put(markerObject); + await txnAsPromise(tx); + logger.log(`LL: backend done storing for ${roomId}!`); + } + + public async clearOutOfBandMembers(roomId: string): Promise<void> { + // the approach to delete all members for a room + // is to get the min and max state key from the index + // for that room, and then delete between those + // keys in the store. + // this should be way faster than deleting every member + // individually for a large room. + const readTx = this.db!.transaction(["oob_membership_events"], "readonly"); + const store = readTx.objectStore("oob_membership_events"); + const roomIndex = store.index("room"); + const roomRange = IDBKeyRange.only(roomId); + + const minStateKeyProm = reqAsCursorPromise(roomIndex.openKeyCursor(roomRange, "next")).then( + (cursor) => (<IDBValidKey[]>cursor?.primaryKey)[1], + ); + const maxStateKeyProm = reqAsCursorPromise(roomIndex.openKeyCursor(roomRange, "prev")).then( + (cursor) => (<IDBValidKey[]>cursor?.primaryKey)[1], + ); + const [minStateKey, maxStateKey] = await Promise.all([minStateKeyProm, maxStateKeyProm]); + + const writeTx = this.db!.transaction(["oob_membership_events"], "readwrite"); + const writeStore = writeTx.objectStore("oob_membership_events"); + const membersKeyRange = IDBKeyRange.bound([roomId, minStateKey], [roomId, maxStateKey]); + + logger.log( + `LL: Deleting all users + marker in storage for room ${roomId}, with key range:`, + [roomId, minStateKey], + [roomId, maxStateKey], + ); + await reqAsPromise(writeStore.delete(membersKeyRange)); + } + + /** + * Clear the entire database. This should be used when logging out of a client + * to prevent mixing data between accounts. + * @returns Resolved when the database is cleared. + */ + public clearDatabase(): Promise<void> { + return new Promise((resolve) => { + logger.log(`Removing indexeddb instance: ${this.dbName}`); + const req = this.indexedDB.deleteDatabase(this.dbName); + + req.onblocked = (): void => { + logger.log(`can't yet delete indexeddb ${this.dbName} because it is open elsewhere`); + }; + + req.onerror = (): void => { + // in firefox, with indexedDB disabled, this fails with a + // DOMError. We treat this as non-fatal, so that we can still + // use the app. + logger.warn(`unable to delete js-sdk store indexeddb: ${req.error}`); + resolve(); + }; + + req.onsuccess = (): void => { + logger.log(`Removed indexeddb instance: ${this.dbName}`); + resolve(); + }; + }); + } + + /** + * @param copy - If false, the data returned is from internal + * buffers and must not be mutated. Otherwise, a copy is made before + * returning such that the data can be safely mutated. Default: true. + * + * @returns Promise which resolves with a sync response to restore the + * client state to where it was at the last save, or null if there + * is no saved sync data. + */ + public getSavedSync(copy = true): Promise<ISavedSync | null> { + const data = this.syncAccumulator.getJSON(); + if (!data.nextBatch) return Promise.resolve(null); + if (copy) { + // We must deep copy the stored data so that the /sync processing code doesn't + // corrupt the internal state of the sync accumulator (it adds non-clonable keys) + return Promise.resolve(utils.deepCopy(data)); + } else { + return Promise.resolve(data); + } + } + + public getNextBatchToken(): Promise<string> { + return Promise.resolve(this.syncAccumulator.getNextBatchToken()); + } + + public setSyncData(syncData: ISyncResponse): Promise<void> { + return Promise.resolve().then(() => { + this.syncAccumulator.accumulate(syncData); + }); + } + + /** + * Sync users and all accumulated sync data to the database. + * If a previous sync is in flight, the new data will be added to the + * next sync and the current sync's promise will be returned. + * @param userTuples - The user tuples + * @returns Promise which resolves if the data was persisted. + */ + public async syncToDatabase(userTuples: UserTuple[]): Promise<void> { + if (this.syncToDatabasePromise) { + logger.warn("Skipping syncToDatabase() as persist already in flight"); + this.pendingUserPresenceData.push(...userTuples); + return this.syncToDatabasePromise; + } + userTuples.unshift(...this.pendingUserPresenceData); + this.syncToDatabasePromise = this.doSyncToDatabase(userTuples); + return this.syncToDatabasePromise; + } + + private async doSyncToDatabase(userTuples: UserTuple[]): Promise<void> { + try { + const syncData = this.syncAccumulator.getJSON(true); + await Promise.all([ + this.persistUserPresenceEvents(userTuples), + this.persistAccountData(syncData.accountData), + this.persistSyncData(syncData.nextBatch, syncData.roomsData), + ]); + } finally { + this.syncToDatabasePromise = undefined; + } + } + + /** + * Persist rooms /sync data along with the next batch token. + * @param nextBatch - The next_batch /sync value. + * @param roomsData - The 'rooms' /sync data from a SyncAccumulator + * @returns Promise which resolves if the data was persisted. + */ + private persistSyncData(nextBatch: string, roomsData: ISyncResponse["rooms"]): Promise<void> { + logger.log("Persisting sync data up to", nextBatch); + return utils.promiseTry<void>(() => { + const txn = this.db!.transaction(["sync"], "readwrite"); + const store = txn.objectStore("sync"); + store.put({ + clobber: "-", // constant key so will always clobber + nextBatch, + roomsData, + }); // put == UPSERT + return txnAsPromise(txn).then(() => { + logger.log("Persisted sync data up to", nextBatch); + }); + }); + } + + /** + * Persist a list of account data events. Events with the same 'type' will + * be replaced. + * @param accountData - An array of raw user-scoped account data events + * @returns Promise which resolves if the events were persisted. + */ + private persistAccountData(accountData: IMinimalEvent[]): Promise<void> { + return utils.promiseTry<void>(() => { + const txn = this.db!.transaction(["accountData"], "readwrite"); + const store = txn.objectStore("accountData"); + for (const event of accountData) { + store.put(event); // put == UPSERT + } + return txnAsPromise(txn).then(); + }); + } + + /** + * Persist a list of [user id, presence event] they are for. + * Users with the same 'userId' will be replaced. + * Presence events should be the event in its raw form (not the Event + * object) + * @param tuples - An array of [userid, event] tuples + * @returns Promise which resolves if the users were persisted. + */ + private persistUserPresenceEvents(tuples: UserTuple[]): Promise<void> { + return utils.promiseTry<void>(() => { + const txn = this.db!.transaction(["users"], "readwrite"); + const store = txn.objectStore("users"); + for (const tuple of tuples) { + store.put({ + userId: tuple[0], + event: tuple[1], + }); // put == UPSERT + } + return txnAsPromise(txn).then(); + }); + } + + /** + * Load all user presence events from the database. This is not cached. + * FIXME: It would probably be more sensible to store the events in the + * sync. + * @returns A list of presence events in their raw form. + */ + public getUserPresenceEvents(): Promise<UserTuple[]> { + return utils.promiseTry<UserTuple[]>(() => { + const txn = this.db!.transaction(["users"], "readonly"); + const store = txn.objectStore("users"); + return selectQuery(store, undefined, (cursor) => { + return [cursor.value.userId, cursor.value.event]; + }); + }); + } + + /** + * Load all the account data events from the database. This is not cached. + * @returns A list of raw global account events. + */ + private loadAccountData(): Promise<IMinimalEvent[]> { + logger.log(`LocalIndexedDBStoreBackend: loading account data...`); + return utils.promiseTry<IMinimalEvent[]>(() => { + const txn = this.db!.transaction(["accountData"], "readonly"); + const store = txn.objectStore("accountData"); + return selectQuery(store, undefined, (cursor) => { + return cursor.value; + }).then((result: IMinimalEvent[]) => { + logger.log(`LocalIndexedDBStoreBackend: loaded account data`); + return result; + }); + }); + } + + /** + * Load the sync data from the database. + * @returns An object with "roomsData" and "nextBatch" keys. + */ + private loadSyncData(): Promise<ISyncData> { + logger.log(`LocalIndexedDBStoreBackend: loading sync data...`); + return utils.promiseTry<ISyncData>(() => { + const txn = this.db!.transaction(["sync"], "readonly"); + const store = txn.objectStore("sync"); + return selectQuery(store, undefined, (cursor) => { + return cursor.value; + }).then((results: ISyncData[]) => { + logger.log(`LocalIndexedDBStoreBackend: loaded sync data`); + if (results.length > 1) { + logger.warn("loadSyncData: More than 1 sync row found."); + } + return results.length > 0 ? results[0] : ({} as ISyncData); + }); + }); + } + + public getClientOptions(): Promise<IStoredClientOpts | undefined> { + return Promise.resolve().then(() => { + const txn = this.db!.transaction(["client_options"], "readonly"); + const store = txn.objectStore("client_options"); + return selectQuery(store, undefined, (cursor) => { + return cursor.value?.options; + }).then((results) => results[0]); + }); + } + + public async storeClientOptions(options: IStoredClientOpts): Promise<void> { + const txn = this.db!.transaction(["client_options"], "readwrite"); + const store = txn.objectStore("client_options"); + store.put({ + clobber: "-", // constant key so will always clobber + options: options, + }); // put == UPSERT + await txnAsPromise(txn); + } + + public async saveToDeviceBatches(batches: ToDeviceBatchWithTxnId[]): Promise<void> { + const txn = this.db!.transaction(["to_device_queue"], "readwrite"); + const store = txn.objectStore("to_device_queue"); + for (const batch of batches) { + store.add(batch); + } + await txnAsPromise(txn); + } + + public async getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null> { + const txn = this.db!.transaction(["to_device_queue"], "readonly"); + const store = txn.objectStore("to_device_queue"); + const cursor = await reqAsCursorPromise(store.openCursor()); + if (!cursor) return null; + + const resultBatch = cursor.value as ToDeviceBatchWithTxnId; + + return { + id: cursor.key as number, + txnId: resultBatch.txnId, + eventType: resultBatch.eventType, + batch: resultBatch.batch, + }; + } + + public async removeToDeviceBatch(id: number): Promise<void> { + const txn = this.db!.transaction(["to_device_queue"], "readwrite"); + const store = txn.objectStore("to_device_queue"); + store.delete(id); + await txnAsPromise(txn); + } +} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-remote-backend.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-remote-backend.ts new file mode 100644 index 0000000..7e2aa0c --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-remote-backend.ts @@ -0,0 +1,203 @@ +/* +Copyright 2017 - 2021 The Matrix.org Foundation C.I.C. + +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 { logger } from "../logger"; +import { defer, IDeferred } from "../utils"; +import { ISavedSync } from "./index"; +import { IStoredClientOpts } from "../client"; +import { IStateEventWithRoomId, ISyncResponse } from "../matrix"; +import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend"; +import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage"; + +export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend { + private worker?: Worker; + private nextSeq = 0; + // The currently in-flight requests to the actual backend + private inFlight: Record<number, IDeferred<any>> = {}; // seq: promise + // Once we start connecting, we keep the promise and re-use it + // if we try to connect again + private startPromise?: Promise<void>; + // Callback for when the IndexedDB gets closed unexpectedly + private onClose?(): void; + + /** + * An IndexedDB store backend where the actual backend sits in a web + * worker. + * + * Construct a new Indexed Database store backend. This requires a call to + * `connect()` before this store can be used. + * @param workerFactory - Factory which produces a Worker + * @param dbName - Optional database name. The same name must be used + * to open the same database. + */ + public constructor(private readonly workerFactory: () => Worker, private readonly dbName?: string) {} + + /** + * Attempt to connect to the database. This can fail if the user does not + * grant permission. + * @returns Promise which resolves if successfully connected. + */ + public connect(onClose?: () => void): Promise<void> { + this.onClose = onClose; + return this.ensureStarted().then(() => this.doCmd("connect")); + } + + /** + * Clear the entire database. This should be used when logging out of a client + * to prevent mixing data between accounts. + * @returns Resolved when the database is cleared. + */ + public clearDatabase(): Promise<void> { + return this.ensureStarted().then(() => this.doCmd("clearDatabase")); + } + + /** @returns whether or not the database was newly created in this session. */ + public isNewlyCreated(): Promise<boolean> { + return this.doCmd("isNewlyCreated"); + } + + /** + * @returns Promise which resolves with a sync response to restore the + * client state to where it was at the last save, or null if there + * is no saved sync data. + */ + public getSavedSync(): Promise<ISavedSync> { + return this.doCmd("getSavedSync"); + } + + public getNextBatchToken(): Promise<string> { + return this.doCmd("getNextBatchToken"); + } + + public setSyncData(syncData: ISyncResponse): Promise<void> { + return this.doCmd("setSyncData", [syncData]); + } + + public syncToDatabase(userTuples: UserTuple[]): Promise<void> { + return this.doCmd("syncToDatabase", [userTuples]); + } + + /** + * Returns the out-of-band membership events for this room that + * were previously loaded. + * @returns the events, potentially an empty array if OOB loading didn't yield any new members + * @returns in case the members for this room haven't been stored yet + */ + public getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null> { + return this.doCmd("getOutOfBandMembers", [roomId]); + } + + /** + * Stores the out-of-band membership events for this room. Note that + * it still makes sense to store an empty array as the OOB status for the room is + * marked as fetched, and getOutOfBandMembers will return an empty array instead of null + * @param membershipEvents - the membership events to store + * @returns when all members have been stored + */ + public setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> { + return this.doCmd("setOutOfBandMembers", [roomId, membershipEvents]); + } + + public clearOutOfBandMembers(roomId: string): Promise<void> { + return this.doCmd("clearOutOfBandMembers", [roomId]); + } + + public getClientOptions(): Promise<IStoredClientOpts | undefined> { + return this.doCmd("getClientOptions"); + } + + public storeClientOptions(options: IStoredClientOpts): Promise<void> { + return this.doCmd("storeClientOptions", [options]); + } + + /** + * Load all user presence events from the database. This is not cached. + * @returns A list of presence events in their raw form. + */ + public getUserPresenceEvents(): Promise<UserTuple[]> { + return this.doCmd("getUserPresenceEvents"); + } + + public async saveToDeviceBatches(batches: ToDeviceBatchWithTxnId[]): Promise<void> { + return this.doCmd("saveToDeviceBatches", [batches]); + } + + public async getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null> { + return this.doCmd("getOldestToDeviceBatch"); + } + + public async removeToDeviceBatch(id: number): Promise<void> { + return this.doCmd("removeToDeviceBatch", [id]); + } + + private ensureStarted(): Promise<void> { + if (!this.startPromise) { + this.worker = this.workerFactory(); + this.worker.onmessage = this.onWorkerMessage; + + // tell the worker the db name. + this.startPromise = this.doCmd("setupWorker", [this.dbName]).then(() => { + logger.log("IndexedDB worker is ready"); + }); + } + return this.startPromise; + } + + private doCmd<T>(command: string, args?: any): Promise<T> { + // wrap in a q so if the postMessage throws, + // the promise automatically gets rejected + return Promise.resolve().then(() => { + const seq = this.nextSeq++; + const def = defer<T>(); + + this.inFlight[seq] = def; + + this.worker?.postMessage({ command, seq, args }); + + return def.promise; + }); + } + + private onWorkerMessage = (ev: MessageEvent): void => { + const msg = ev.data; + + if (msg.command == "closed") { + this.onClose?.(); + } else if (msg.command == "cmd_success" || msg.command == "cmd_fail") { + if (msg.seq === undefined) { + logger.error("Got reply from worker with no seq"); + return; + } + + const def = this.inFlight[msg.seq]; + if (def === undefined) { + logger.error("Got reply for unknown seq " + msg.seq); + return; + } + delete this.inFlight[msg.seq]; + + if (msg.command == "cmd_success") { + def.resolve(msg.result); + } else { + const error = new Error(msg.error.message); + error.name = msg.error.name; + def.reject(error); + } + } else { + logger.warn("Unrecognised message from worker: ", msg); + } + }; +} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-store-worker.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-store-worker.ts new file mode 100644 index 0000000..52a7fa6 --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-store-worker.ts @@ -0,0 +1,157 @@ +/* +Copyright 2017 - 2021 The Matrix.org Foundation C.I.C. + +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 { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend"; +import { logger } from "../logger"; + +interface ICmd { + command: string; + seq: number; + args: any[]; +} + +/** + * This class lives in the webworker and drives a LocalIndexedDBStoreBackend + * controlled by messages from the main process. + * + * @example + * It should be instantiated by a web worker script provided by the application + * in a script, for example: + * ``` + * import {IndexedDBStoreWorker} from 'matrix-js-sdk/lib/indexeddb-worker.js'; + * const remoteWorker = new IndexedDBStoreWorker(postMessage); + * onmessage = remoteWorker.onMessage; + * ``` + * + * Note that it is advisable to import this class by referencing the file directly to + * avoid a dependency on the whole js-sdk. + * + */ +export class IndexedDBStoreWorker { + private backend?: LocalIndexedDBStoreBackend; + + /** + * @param postMessage - The web worker postMessage function that + * should be used to communicate back to the main script. + */ + public constructor(private readonly postMessage: InstanceType<typeof Worker>["postMessage"]) {} + + private onClose = (): void => { + this.postMessage.call(null, { + command: "closed", + }); + }; + + /** + * Passes a message event from the main script into the class. This method + * can be directly assigned to the web worker `onmessage` variable. + * + * @param ev - The message event + */ + public onMessage = (ev: MessageEvent): void => { + const msg: ICmd = ev.data; + let prom: Promise<any> | undefined; + + switch (msg.command) { + case "setupWorker": + // this is the 'indexedDB' global (where global != window + // because it's a web worker and there is no window). + this.backend = new LocalIndexedDBStoreBackend(indexedDB, msg.args[0]); + prom = Promise.resolve(); + break; + case "connect": + prom = this.backend?.connect(this.onClose); + break; + case "isNewlyCreated": + prom = this.backend?.isNewlyCreated(); + break; + case "clearDatabase": + prom = this.backend?.clearDatabase(); + break; + case "getSavedSync": + prom = this.backend?.getSavedSync(false); + break; + case "setSyncData": + prom = this.backend?.setSyncData(msg.args[0]); + break; + case "syncToDatabase": + prom = this.backend?.syncToDatabase(msg.args[0]); + break; + case "getUserPresenceEvents": + prom = this.backend?.getUserPresenceEvents(); + break; + case "getNextBatchToken": + prom = this.backend?.getNextBatchToken(); + break; + case "getOutOfBandMembers": + prom = this.backend?.getOutOfBandMembers(msg.args[0]); + break; + case "clearOutOfBandMembers": + prom = this.backend?.clearOutOfBandMembers(msg.args[0]); + break; + case "setOutOfBandMembers": + prom = this.backend?.setOutOfBandMembers(msg.args[0], msg.args[1]); + break; + case "getClientOptions": + prom = this.backend?.getClientOptions(); + break; + case "storeClientOptions": + prom = this.backend?.storeClientOptions(msg.args[0]); + break; + case "saveToDeviceBatches": + prom = this.backend?.saveToDeviceBatches(msg.args[0]); + break; + case "getOldestToDeviceBatch": + prom = this.backend?.getOldestToDeviceBatch(); + break; + case "removeToDeviceBatch": + prom = this.backend?.removeToDeviceBatch(msg.args[0]); + break; + } + + if (prom === undefined) { + this.postMessage({ + command: "cmd_fail", + seq: msg.seq, + // Can't be an Error because they're not structured cloneable + error: "Unrecognised command", + }); + return; + } + + prom.then( + (ret) => { + this.postMessage.call(null, { + command: "cmd_success", + seq: msg.seq, + result: ret, + }); + }, + (err) => { + logger.error("Error running command: " + msg.command, err); + this.postMessage.call(null, { + command: "cmd_fail", + seq: msg.seq, + // Just send a string because Error objects aren't cloneable + error: { + message: err.message, + name: err.name, + }, + }); + }, + ); + }; +} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb.ts new file mode 100644 index 0000000..cc77bf9 --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb.ts @@ -0,0 +1,383 @@ +/* +Copyright 2017 - 2021 Vector Creations Ltd + +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. +*/ + +/* eslint-disable @babel/no-invalid-this */ + +import { MemoryStore, IOpts as IBaseOpts } from "./memory"; +import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend"; +import { RemoteIndexedDBStoreBackend } from "./indexeddb-remote-backend"; +import { User } from "../models/user"; +import { IEvent, MatrixEvent } from "../models/event"; +import { logger } from "../logger"; +import { ISavedSync } from "./index"; +import { IIndexedDBBackend } from "./indexeddb-backend"; +import { ISyncResponse } from "../sync-accumulator"; +import { TypedEventEmitter } from "../models/typed-event-emitter"; +import { IStateEventWithRoomId } from "../@types/search"; +import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage"; +import { IStoredClientOpts } from "../client"; + +/** + * This is an internal module. See {@link IndexedDBStore} for the public class. + */ + +// If this value is too small we'll be writing very often which will cause +// noticeable stop-the-world pauses. If this value is too big we'll be writing +// so infrequently that the /sync size gets bigger on reload. Writing more +// often does not affect the length of the pause since the entire /sync +// response is persisted each time. +const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes + +interface IOpts extends IBaseOpts { + /** The Indexed DB interface e.g. `window.indexedDB` */ + indexedDB: IDBFactory; + /** Optional database name. The same name must be used to open the same database. */ + dbName?: string; + /** Optional factory to spin up a Worker to execute the IDB transactions within. */ + workerFactory?: () => Worker; +} + +type EventHandlerMap = { + // Fired when an IDB command fails on a degradable path, and the store falls back to MemoryStore + // This signals the potential for data volatility. + degraded: (e: Error) => void; + // Fired when the IndexedDB gets closed unexpectedly, for example, if the underlying storage is removed or + // if the user clears the database in the browser's history preferences. + closed: () => void; +}; + +export class IndexedDBStore extends MemoryStore { + public static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> { + return LocalIndexedDBStoreBackend.exists(indexedDB, dbName); + } + + /** + * The backend instance. + * Call through to this API if you need to perform specific indexeddb actions like deleting the database. + */ + public readonly backend: IIndexedDBBackend; + + private startedUp = false; + private syncTs = 0; + // Records the last-modified-time of each user at the last point we saved + // the database, such that we can derive the set if users that have been + // modified since we last saved. + private userModifiedMap: Record<string, number> = {}; // user_id : timestamp + private emitter = new TypedEventEmitter<keyof EventHandlerMap, EventHandlerMap>(); + + /** + * Construct a new Indexed Database store, which extends MemoryStore. + * + * This store functions like a MemoryStore except it periodically persists + * the contents of the store to an IndexedDB backend. + * + * All data is still kept in-memory but can be loaded from disk by calling + * `startup()`. This can make startup times quicker as a complete + * sync from the server is not required. This does not reduce memory usage as all + * the data is eagerly fetched when `startup()` is called. + * ``` + * let opts = { indexedDB: window.indexedDB, localStorage: window.localStorage }; + * let store = new IndexedDBStore(opts); + * await store.startup(); // load from indexed db + * let client = sdk.createClient({ + * store: store, + * }); + * client.startClient(); + * client.on("sync", function(state, prevState, data) { + * if (state === "PREPARED") { + * console.log("Started up, now with go faster stripes!"); + * } + * }); + * ``` + * + * @param opts - Options object. + */ + public constructor(opts: IOpts) { + super(opts); + + if (!opts.indexedDB) { + throw new Error("Missing required option: indexedDB"); + } + + if (opts.workerFactory) { + this.backend = new RemoteIndexedDBStoreBackend(opts.workerFactory, opts.dbName); + } else { + this.backend = new LocalIndexedDBStoreBackend(opts.indexedDB, opts.dbName); + } + } + + public on = this.emitter.on.bind(this.emitter); + + /** + * @returns Resolved when loaded from indexed db. + */ + public startup(): Promise<void> { + if (this.startedUp) { + logger.log(`IndexedDBStore.startup: already started`); + return Promise.resolve(); + } + + logger.log(`IndexedDBStore.startup: connecting to backend`); + return this.backend + .connect(this.onClose) + .then(() => { + logger.log(`IndexedDBStore.startup: loading presence events`); + return this.backend.getUserPresenceEvents(); + }) + .then((userPresenceEvents) => { + logger.log(`IndexedDBStore.startup: processing presence events`); + userPresenceEvents.forEach(([userId, rawEvent]) => { + const u = new User(userId); + if (rawEvent) { + u.setPresenceEvent(new MatrixEvent(rawEvent)); + } + this.userModifiedMap[u.userId] = u.getLastModifiedTime(); + this.storeUser(u); + }); + this.startedUp = true; + }); + } + + private onClose = (): void => { + this.emitter.emit("closed"); + }; + + /** + * @returns Promise which resolves with a sync response to restore the + * client state to where it was at the last save, or null if there + * is no saved sync data. + */ + public getSavedSync = this.degradable((): Promise<ISavedSync | null> => { + return this.backend.getSavedSync(); + }, "getSavedSync"); + + /** @returns whether or not the database was newly created in this session. */ + public isNewlyCreated = this.degradable((): Promise<boolean> => { + return this.backend.isNewlyCreated(); + }, "isNewlyCreated"); + + /** + * @returns If there is a saved sync, the nextBatch token + * for this sync, otherwise null. + */ + public getSavedSyncToken = this.degradable((): Promise<string | null> => { + return this.backend.getNextBatchToken(); + }, "getSavedSyncToken"); + + /** + * Delete all data from this store. + * @returns Promise which resolves if the data was deleted from the database. + */ + public deleteAllData = this.degradable((): Promise<void> => { + super.deleteAllData(); + return this.backend.clearDatabase().then( + () => { + logger.log("Deleted indexeddb data."); + }, + (err) => { + logger.error(`Failed to delete indexeddb data: ${err}`); + throw err; + }, + ); + }); + + /** + * Whether this store would like to save its data + * Note that obviously whether the store wants to save or + * not could change between calling this function and calling + * save(). + * + * @returns True if calling save() will actually save + * (at the time this function is called). + */ + public wantsSave(): boolean { + const now = Date.now(); + return now - this.syncTs > WRITE_DELAY_MS; + } + + /** + * Possibly write data to the database. + * + * @param force - True to force a save to happen + * @returns Promise resolves after the write completes + * (or immediately if no write is performed) + */ + public save(force = false): Promise<void> { + if (force || this.wantsSave()) { + return this.reallySave(); + } + return Promise.resolve(); + } + + private reallySave = this.degradable((): Promise<void> => { + this.syncTs = Date.now(); // set now to guard against multi-writes + + // work out changed users (this doesn't handle deletions but you + // can't 'delete' users as they are just presence events). + const userTuples: [userId: string, presenceEvent: Partial<IEvent>][] = []; + for (const u of this.getUsers()) { + if (this.userModifiedMap[u.userId] === u.getLastModifiedTime()) continue; + if (!u.events.presence) continue; + + userTuples.push([u.userId, u.events.presence.event]); + + // note that we've saved this version of the user + this.userModifiedMap[u.userId] = u.getLastModifiedTime(); + } + + return this.backend.syncToDatabase(userTuples); + }); + + public setSyncData = this.degradable((syncData: ISyncResponse): Promise<void> => { + return this.backend.setSyncData(syncData); + }, "setSyncData"); + + /** + * Returns the out-of-band membership events for this room that + * were previously loaded. + * @returns the events, potentially an empty array if OOB loading didn't yield any new members + * @returns in case the members for this room haven't been stored yet + */ + public getOutOfBandMembers = this.degradable((roomId: string): Promise<IStateEventWithRoomId[] | null> => { + return this.backend.getOutOfBandMembers(roomId); + }, "getOutOfBandMembers"); + + /** + * Stores the out-of-band membership events for this room. Note that + * it still makes sense to store an empty array as the OOB status for the room is + * marked as fetched, and getOutOfBandMembers will return an empty array instead of null + * @param membershipEvents - the membership events to store + * @returns when all members have been stored + */ + public setOutOfBandMembers = this.degradable( + (roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> => { + super.setOutOfBandMembers(roomId, membershipEvents); + return this.backend.setOutOfBandMembers(roomId, membershipEvents); + }, + "setOutOfBandMembers", + ); + + public clearOutOfBandMembers = this.degradable((roomId: string) => { + super.clearOutOfBandMembers(roomId); + return this.backend.clearOutOfBandMembers(roomId); + }, "clearOutOfBandMembers"); + + public getClientOptions = this.degradable((): Promise<IStoredClientOpts | undefined> => { + return this.backend.getClientOptions(); + }, "getClientOptions"); + + public storeClientOptions = this.degradable((options: IStoredClientOpts): Promise<void> => { + super.storeClientOptions(options); + return this.backend.storeClientOptions(options); + }, "storeClientOptions"); + + /** + * All member functions of `IndexedDBStore` that access the backend use this wrapper to + * watch for failures after initial store startup, including `QuotaExceededError` as + * free disk space changes, etc. + * + * When IndexedDB fails via any of these paths, we degrade this back to a `MemoryStore` + * in place so that the current operation and all future ones are in-memory only. + * + * @param func - The degradable work to do. + * @param fallback - The method name for fallback. + * @returns A wrapped member function. + */ + private degradable<A extends Array<any>, R = void>( + func: DegradableFn<A, R>, + fallback?: keyof MemoryStore, + ): DegradableFn<A, R> { + const fallbackFn = fallback ? (super[fallback] as Function) : null; + + return async (...args) => { + try { + return await func.call(this, ...args); + } catch (e) { + logger.error("IndexedDBStore failure, degrading to MemoryStore", e); + this.emitter.emit("degraded", e as Error); + try { + // We try to delete IndexedDB after degrading since this store is only a + // cache (the app will still function correctly without the data). + // It's possible that deleting repair IndexedDB for the next app load, + // potentially by making a little more space available. + logger.log("IndexedDBStore trying to delete degraded data"); + await this.backend.clearDatabase(); + logger.log("IndexedDBStore delete after degrading succeeded"); + } catch (e) { + logger.warn("IndexedDBStore delete after degrading failed", e); + } + // Degrade the store from being an instance of `IndexedDBStore` to instead be + // an instance of `MemoryStore` so that future API calls use the memory path + // directly and skip IndexedDB entirely. This should be safe as + // `IndexedDBStore` already extends from `MemoryStore`, so we are making the + // store become its parent type in a way. The mutator methods of + // `IndexedDBStore` also maintain the state that `MemoryStore` uses (many are + // not overridden at all). + if (fallbackFn) { + return fallbackFn.call(this, ...args); + } + } + }; + } + + // XXX: ideally these would be stored in indexeddb as part of the room but, + // we don't store rooms as such and instead accumulate entire sync responses atm. + public async getPendingEvents(roomId: string): Promise<Partial<IEvent>[]> { + if (!this.localStorage) return super.getPendingEvents(roomId); + + const serialized = this.localStorage.getItem(pendingEventsKey(roomId)); + if (serialized) { + try { + return JSON.parse(serialized); + } catch (e) { + logger.error("Could not parse persisted pending events", e); + } + } + return []; + } + + public async setPendingEvents(roomId: string, events: Partial<IEvent>[]): Promise<void> { + if (!this.localStorage) return super.setPendingEvents(roomId, events); + + if (events.length > 0) { + this.localStorage.setItem(pendingEventsKey(roomId), JSON.stringify(events)); + } else { + this.localStorage.removeItem(pendingEventsKey(roomId)); + } + } + + public saveToDeviceBatches(batches: ToDeviceBatchWithTxnId[]): Promise<void> { + return this.backend.saveToDeviceBatches(batches); + } + + public getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null> { + return this.backend.getOldestToDeviceBatch(); + } + + public removeToDeviceBatch(id: number): Promise<void> { + return this.backend.removeToDeviceBatch(id); + } +} + +/** + * @param roomId - ID of the current room + * @returns Storage key to retrieve pending events + */ +function pendingEventsKey(roomId: string): string { + return `mx_pending_events_${roomId}`; +} + +type DegradableFn<A extends Array<any>, T> = (...args: A) => Promise<T>; diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/local-storage-events-emitter.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/local-storage-events-emitter.ts new file mode 100644 index 0000000..adb70cb --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/local-storage-events-emitter.ts @@ -0,0 +1,46 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +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 { TypedEventEmitter } from "../models/typed-event-emitter"; + +export enum LocalStorageErrors { + Global = "Global", + SetItemError = "setItem", + GetItemError = "getItem", + RemoveItemError = "removeItem", + ClearError = "clear", + QuotaExceededError = "QuotaExceededError", +} + +type EventHandlerMap = { + [LocalStorageErrors.Global]: (error: Error) => void; + [LocalStorageErrors.SetItemError]: (error: Error) => void; + [LocalStorageErrors.GetItemError]: (error: Error) => void; + [LocalStorageErrors.RemoveItemError]: (error: Error) => void; + [LocalStorageErrors.ClearError]: (error: Error) => void; + [LocalStorageErrors.QuotaExceededError]: (error: Error) => void; +}; + +/** + * Used in element-web as a temporary hack to handle all the localStorage errors on the highest level possible + * As of 15.11.2021 (DD/MM/YYYY) we're not properly handling local storage exceptions anywhere. + * This store, as an event emitter, is used to re-emit local storage exceptions so that we can handle them + * and show some kind of a "It's dead Jim" modal to the users, telling them that hey, + * maybe you should check out your disk, as it's probably dying and your session may die with it. + * See: https://github.com/vector-im/element-web/issues/18423 + */ +class LocalStorageErrorsEventsEmitter extends TypedEventEmitter<LocalStorageErrors, EventHandlerMap> {} +export const localStorageErrorsEventsEmitter = new LocalStorageErrorsEventsEmitter(); diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/memory.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/memory.ts new file mode 100644 index 0000000..d859ddd --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/memory.ts @@ -0,0 +1,436 @@ +/* +Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. + +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. +*/ + +/** + * This is an internal module. See {@link MemoryStore} for the public class. + */ + +import { EventType } from "../@types/event"; +import { Room } from "../models/room"; +import { User } from "../models/user"; +import { IEvent, MatrixEvent } from "../models/event"; +import { RoomState, RoomStateEvent } from "../models/room-state"; +import { RoomMember } from "../models/room-member"; +import { Filter } from "../filter"; +import { ISavedSync, IStore } from "./index"; +import { RoomSummary } from "../models/room-summary"; +import { ISyncResponse } from "../sync-accumulator"; +import { IStateEventWithRoomId } from "../@types/search"; +import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage"; +import { IStoredClientOpts } from "../client"; +import { MapWithDefault } from "../utils"; + +function isValidFilterId(filterId?: string | number | null): boolean { + const isValidStr = + typeof filterId === "string" && + !!filterId && + filterId !== "undefined" && // exclude these as we've serialized undefined in localStorage before + filterId !== "null"; + + return isValidStr || typeof filterId === "number"; +} + +export interface IOpts { + /** The local storage instance to persist some forms of data such as tokens. Rooms will NOT be stored. */ + localStorage?: Storage; +} + +export class MemoryStore implements IStore { + private rooms: Record<string, Room> = {}; // roomId: Room + private users: Record<string, User> = {}; // userId: User + private syncToken: string | null = null; + // userId: { + // filterId: Filter + // } + private filters: MapWithDefault<string, Map<string, Filter>> = new MapWithDefault(() => new Map()); + public accountData: Map<string, MatrixEvent> = new Map(); // type: content + protected readonly localStorage?: Storage; + private oobMembers: Map<string, IStateEventWithRoomId[]> = new Map(); // roomId: [member events] + private pendingEvents: { [roomId: string]: Partial<IEvent>[] } = {}; + private clientOptions?: IStoredClientOpts; + private pendingToDeviceBatches: IndexedToDeviceBatch[] = []; + private nextToDeviceBatchId = 0; + + /** + * Construct a new in-memory data store for the Matrix Client. + * @param opts - Config options + */ + public constructor(opts: IOpts = {}) { + this.localStorage = opts.localStorage; + } + + /** + * Retrieve the token to stream from. + * @returns The token or null. + */ + public getSyncToken(): string | null { + return this.syncToken; + } + + /** @returns whether or not the database was newly created in this session. */ + public isNewlyCreated(): Promise<boolean> { + return Promise.resolve(true); + } + + /** + * Set the token to stream from. + * @param token - The token to stream from. + */ + public setSyncToken(token: string): void { + this.syncToken = token; + } + + /** + * Store the given room. + * @param room - The room to be stored. All properties must be stored. + */ + public storeRoom(room: Room): void { + this.rooms[room.roomId] = room; + // add listeners for room member changes so we can keep the room member + // map up-to-date. + room.currentState.on(RoomStateEvent.Members, this.onRoomMember); + // add existing members + room.currentState.getMembers().forEach((m) => { + this.onRoomMember(null, room.currentState, m); + }); + } + + /** + * Called when a room member in a room being tracked by this store has been + * updated. + */ + private onRoomMember = (event: MatrixEvent | null, state: RoomState, member: RoomMember): void => { + if (member.membership === "invite") { + // We do NOT add invited members because people love to typo user IDs + // which would then show up in these lists (!) + return; + } + + const user = this.users[member.userId] || new User(member.userId); + if (member.name) { + user.setDisplayName(member.name); + if (member.events.member) { + user.setRawDisplayName(member.events.member.getDirectionalContent().displayname); + } + } + if (member.events.member && member.events.member.getContent().avatar_url) { + user.setAvatarUrl(member.events.member.getContent().avatar_url); + } + this.users[user.userId] = user; + }; + + /** + * Retrieve a room by its' room ID. + * @param roomId - The room ID. + * @returns The room or null. + */ + public getRoom(roomId: string): Room | null { + return this.rooms[roomId] || null; + } + + /** + * Retrieve all known rooms. + * @returns A list of rooms, which may be empty. + */ + public getRooms(): Room[] { + return Object.values(this.rooms); + } + + /** + * Permanently delete a room. + */ + public removeRoom(roomId: string): void { + if (this.rooms[roomId]) { + this.rooms[roomId].currentState.removeListener(RoomStateEvent.Members, this.onRoomMember); + } + delete this.rooms[roomId]; + } + + /** + * Retrieve a summary of all the rooms. + * @returns A summary of each room. + */ + public getRoomSummaries(): RoomSummary[] { + return Object.values(this.rooms).map(function (room) { + return room.summary!; + }); + } + + /** + * Store a User. + * @param user - The user to store. + */ + public storeUser(user: User): void { + this.users[user.userId] = user; + } + + /** + * Retrieve a User by its' user ID. + * @param userId - The user ID. + * @returns The user or null. + */ + public getUser(userId: string): User | null { + return this.users[userId] || null; + } + + /** + * Retrieve all known users. + * @returns A list of users, which may be empty. + */ + public getUsers(): User[] { + return Object.values(this.users); + } + + /** + * Retrieve scrollback for this room. + * @param room - The matrix room + * @param limit - The max number of old events to retrieve. + * @returns An array of objects which will be at most 'limit' + * length and at least 0. The objects are the raw event JSON. + */ + public scrollback(room: Room, limit: number): MatrixEvent[] { + return []; + } + + /** + * Store events for a room. The events have already been added to the timeline + * @param room - The room to store events for. + * @param events - The events to store. + * @param token - The token associated with these events. + * @param toStart - True if these are paginated results. + */ + public storeEvents(room: Room, events: MatrixEvent[], token: string | null, toStart: boolean): void { + // no-op because they've already been added to the room instance. + } + + /** + * Store a filter. + */ + public storeFilter(filter: Filter): void { + if (!filter?.userId || !filter?.filterId) return; + this.filters.getOrCreate(filter.userId).set(filter.filterId, filter); + } + + /** + * Retrieve a filter. + * @returns A filter or null. + */ + public getFilter(userId: string, filterId: string): Filter | null { + return this.filters.get(userId)?.get(filterId) || null; + } + + /** + * Retrieve a filter ID with the given name. + * @param filterName - The filter name. + * @returns The filter ID or null. + */ + public getFilterIdByName(filterName: string): string | null { + if (!this.localStorage) { + return null; + } + const key = "mxjssdk_memory_filter_" + filterName; + // XXX Storage.getItem doesn't throw ... + // or are we using something different + // than window.localStorage in some cases + // that does throw? + // that would be very naughty + try { + const value = this.localStorage.getItem(key); + if (isValidFilterId(value)) { + return value; + } + } catch (e) {} + return null; + } + + /** + * Set a filter name to ID mapping. + */ + public setFilterIdByName(filterName: string, filterId?: string): void { + if (!this.localStorage) { + return; + } + const key = "mxjssdk_memory_filter_" + filterName; + try { + if (isValidFilterId(filterId)) { + this.localStorage.setItem(key, filterId!); + } else { + this.localStorage.removeItem(key); + } + } catch (e) {} + } + + /** + * Store user-scoped account data events. + * N.B. that account data only allows a single event per type, so multiple + * events with the same type will replace each other. + * @param events - The events to store. + */ + public storeAccountDataEvents(events: MatrixEvent[]): void { + events.forEach((event) => { + // MSC3391: an event with content of {} should be interpreted as deleted + const isDeleted = !Object.keys(event.getContent()).length; + if (isDeleted) { + this.accountData.delete(event.getType()); + } else { + this.accountData.set(event.getType(), event); + } + }); + } + + /** + * Get account data event by event type + * @param eventType - The event type being queried + * @returns the user account_data event of given type, if any + */ + public getAccountData(eventType: EventType | string): MatrixEvent | undefined { + return this.accountData.get(eventType); + } + + /** + * setSyncData does nothing as there is no backing data store. + * + * @param syncData - The sync data + * @returns An immediately resolved promise. + */ + public setSyncData(syncData: ISyncResponse): Promise<void> { + return Promise.resolve(); + } + + /** + * We never want to save becase we have nothing to save to. + * + * @returns If the store wants to save + */ + public wantsSave(): boolean { + return false; + } + + /** + * Save does nothing as there is no backing data store. + * @param force - True to force a save (but the memory + * store still can't save anything) + */ + public save(force: boolean): void {} + + /** + * Startup does nothing as this store doesn't require starting up. + * @returns An immediately resolved promise. + */ + public startup(): Promise<void> { + return Promise.resolve(); + } + + /** + * @returns Promise which resolves with a sync response to restore the + * client state to where it was at the last save, or null if there + * is no saved sync data. + */ + public getSavedSync(): Promise<ISavedSync | null> { + return Promise.resolve(null); + } + + /** + * @returns If there is a saved sync, the nextBatch token + * for this sync, otherwise null. + */ + public getSavedSyncToken(): Promise<string | null> { + return Promise.resolve(null); + } + + /** + * Delete all data from this store. + * @returns An immediately resolved promise. + */ + public deleteAllData(): Promise<void> { + this.rooms = { + // roomId: Room + }; + this.users = { + // userId: User + }; + this.syncToken = null; + this.filters = new MapWithDefault(() => new Map()); + this.accountData = new Map(); // type : content + return Promise.resolve(); + } + + /** + * Returns the out-of-band membership events for this room that + * were previously loaded. + * @returns the events, potentially an empty array if OOB loading didn't yield any new members + * @returns in case the members for this room haven't been stored yet + */ + public getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null> { + return Promise.resolve(this.oobMembers.get(roomId) || null); + } + + /** + * Stores the out-of-band membership events for this room. Note that + * it still makes sense to store an empty array as the OOB status for the room is + * marked as fetched, and getOutOfBandMembers will return an empty array instead of null + * @param membershipEvents - the membership events to store + * @returns when all members have been stored + */ + public setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> { + this.oobMembers.set(roomId, membershipEvents); + return Promise.resolve(); + } + + public clearOutOfBandMembers(roomId: string): Promise<void> { + this.oobMembers.delete(roomId); + return Promise.resolve(); + } + + public getClientOptions(): Promise<IStoredClientOpts | undefined> { + return Promise.resolve(this.clientOptions); + } + + public storeClientOptions(options: IStoredClientOpts): Promise<void> { + this.clientOptions = Object.assign({}, options); + return Promise.resolve(); + } + + public async getPendingEvents(roomId: string): Promise<Partial<IEvent>[]> { + return this.pendingEvents[roomId] ?? []; + } + + public async setPendingEvents(roomId: string, events: Partial<IEvent>[]): Promise<void> { + this.pendingEvents[roomId] = events; + } + + public saveToDeviceBatches(batches: ToDeviceBatchWithTxnId[]): Promise<void> { + for (const batch of batches) { + this.pendingToDeviceBatches.push({ + id: this.nextToDeviceBatchId++, + eventType: batch.eventType, + txnId: batch.txnId, + batch: batch.batch, + }); + } + return Promise.resolve(); + } + + public async getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null> { + if (this.pendingToDeviceBatches.length === 0) return null; + return this.pendingToDeviceBatches[0]; + } + + public removeToDeviceBatch(id: number): Promise<void> { + this.pendingToDeviceBatches = this.pendingToDeviceBatches.filter((batch) => batch.id !== id); + return Promise.resolve(); + } +} diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/store/stub.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/stub.ts new file mode 100644 index 0000000..e4402ed --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/store/stub.ts @@ -0,0 +1,267 @@ +/* +Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. + +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. +*/ + +/** + * This is an internal module. + */ + +import { EventType } from "../@types/event"; +import { Room } from "../models/room"; +import { User } from "../models/user"; +import { IEvent, MatrixEvent } from "../models/event"; +import { Filter } from "../filter"; +import { ISavedSync, IStore } from "./index"; +import { RoomSummary } from "../models/room-summary"; +import { ISyncResponse } from "../sync-accumulator"; +import { IStateEventWithRoomId } from "../@types/search"; +import { IndexedToDeviceBatch, ToDeviceBatch } from "../models/ToDeviceMessage"; +import { IStoredClientOpts } from "../client"; + +/** + * Construct a stub store. This does no-ops on most store methods. + */ +export class StubStore implements IStore { + public readonly accountData = new Map(); // stub + private fromToken: string | null = null; + + /** @returns whether or not the database was newly created in this session. */ + public isNewlyCreated(): Promise<boolean> { + return Promise.resolve(true); + } + + /** + * Get the sync token. + */ + public getSyncToken(): string | null { + return this.fromToken; + } + + /** + * Set the sync token. + */ + public setSyncToken(token: string): void { + this.fromToken = token; + } + + /** + * No-op. + */ + public storeRoom(room: Room): void {} + + /** + * No-op. + */ + public getRoom(roomId: string): Room | null { + return null; + } + + /** + * No-op. + * @returns An empty array. + */ + public getRooms(): Room[] { + return []; + } + + /** + * Permanently delete a room. + */ + public removeRoom(roomId: string): void { + return; + } + + /** + * No-op. + * @returns An empty array. + */ + public getRoomSummaries(): RoomSummary[] { + return []; + } + + /** + * No-op. + */ + public storeUser(user: User): void {} + + /** + * No-op. + */ + public getUser(userId: string): User | null { + return null; + } + + /** + * No-op. + */ + public getUsers(): User[] { + return []; + } + + /** + * No-op. + */ + public scrollback(room: Room, limit: number): MatrixEvent[] { + return []; + } + + /** + * Store events for a room. + * @param room - The room to store events for. + * @param events - The events to store. + * @param token - The token associated with these events. + * @param toStart - True if these are paginated results. + */ + public storeEvents(room: Room, events: MatrixEvent[], token: string | null, toStart: boolean): void {} + + /** + * Store a filter. + */ + public storeFilter(filter: Filter): void {} + + /** + * Retrieve a filter. + * @returns A filter or null. + */ + public getFilter(userId: string, filterId: string): Filter | null { + return null; + } + + /** + * Retrieve a filter ID with the given name. + * @param filterName - The filter name. + * @returns The filter ID or null. + */ + public getFilterIdByName(filterName: string): string | null { + return null; + } + + /** + * Set a filter name to ID mapping. + */ + public setFilterIdByName(filterName: string, filterId?: string): void {} + + /** + * Store user-scoped account data events + * @param events - The events to store. + */ + public storeAccountDataEvents(events: MatrixEvent[]): void {} + + /** + * Get account data event by event type + * @param eventType - The event type being queried + */ + public getAccountData(eventType: EventType | string): MatrixEvent | undefined { + return undefined; + } + + /** + * setSyncData does nothing as there is no backing data store. + * + * @param syncData - The sync data + * @returns An immediately resolved promise. + */ + public setSyncData(syncData: ISyncResponse): Promise<void> { + return Promise.resolve(); + } + + /** + * We never want to save because we have nothing to save to. + * + * @returns If the store wants to save + */ + public wantsSave(): boolean { + return false; + } + + /** + * Save does nothing as there is no backing data store. + */ + public save(): void {} + + /** + * Startup does nothing. + * @returns An immediately resolved promise. + */ + public startup(): Promise<void> { + return Promise.resolve(); + } + + /** + * @returns Promise which resolves with a sync response to restore the + * client state to where it was at the last save, or null if there + * is no saved sync data. + */ + public getSavedSync(): Promise<ISavedSync | null> { + return Promise.resolve(null); + } + + /** + * @returns If there is a saved sync, the nextBatch token + * for this sync, otherwise null. + */ + public getSavedSyncToken(): Promise<string | null> { + return Promise.resolve(null); + } + + /** + * Delete all data from this store. Does nothing since this store + * doesn't store anything. + * @returns An immediately resolved promise. + */ + public deleteAllData(): Promise<void> { + return Promise.resolve(); + } + + public getOutOfBandMembers(): Promise<IStateEventWithRoomId[] | null> { + return Promise.resolve(null); + } + + public setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> { + return Promise.resolve(); + } + + public clearOutOfBandMembers(): Promise<void> { + return Promise.resolve(); + } + + public getClientOptions(): Promise<IStoredClientOpts | undefined> { + return Promise.resolve(undefined); + } + + public storeClientOptions(options: IStoredClientOpts): Promise<void> { + return Promise.resolve(); + } + + public async getPendingEvents(roomId: string): Promise<Partial<IEvent>[]> { + return []; + } + + public setPendingEvents(roomId: string, events: Partial<IEvent>[]): Promise<void> { + return Promise.resolve(); + } + + public async saveToDeviceBatches(batch: ToDeviceBatch[]): Promise<void> { + return Promise.resolve(); + } + + public getOldestToDeviceBatch(): Promise<IndexedToDeviceBatch | null> { + return Promise.resolve(null); + } + + public async removeToDeviceBatch(id: number): Promise<void> { + return Promise.resolve(); + } +} |