summaryrefslogtreecommitdiff
path: root/includes/external/matrix/node_modules/matrix-js-sdk/src/store
diff options
context:
space:
mode:
authorRaindropsSys <contact@minteck.org>2023-04-24 14:03:36 +0200
committerRaindropsSys <contact@minteck.org>2023-04-24 14:03:36 +0200
commit633c92eae865e957121e08de634aeee11a8b3992 (patch)
tree09d881bee1dae0b6eee49db1dfaf0f500240606c /includes/external/matrix/node_modules/matrix-js-sdk/src/store
parentc4657e4509733699c0f26a3c900bab47e915d5a0 (diff)
downloadpluralconnect-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')
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/store/index.ts248
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-backend.ts40
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-local-backend.ts597
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-remote-backend.ts203
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb-store-worker.ts157
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/store/indexeddb.ts383
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/store/local-storage-events-emitter.ts46
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/store/memory.ts436
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/store/stub.ts267
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();
+ }
+}