summaryrefslogtreecommitdiffstats
path: root/comm/chat/protocols/matrix/lib/matrix-sdk/store
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/chat/protocols/matrix/lib/matrix-sdk/store
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/chat/protocols/matrix/lib/matrix-sdk/store')
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/store/index.js5
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-backend.js5
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-local-backend.js569
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-remote-backend.js200
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-store-worker.js151
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb.js329
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/store/local-storage-events-emitter.js43
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/store/memory.js418
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/store/stub.js262
9 files changed, 1982 insertions, 0 deletions
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/store/index.js b/comm/chat/protocols/matrix/lib/matrix-sdk/store/index.js
new file mode 100644
index 0000000000..430afc16cd
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/store/index.js
@@ -0,0 +1,5 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+}); \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-backend.js b/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-backend.js
new file mode 100644
index 0000000000..430afc16cd
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-backend.js
@@ -0,0 +1,5 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+}); \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-local-backend.js b/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-local-backend.js
new file mode 100644
index 0000000000..ecc5538734
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-local-backend.js
@@ -0,0 +1,569 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.LocalIndexedDBStoreBackend = void 0;
+var _syncAccumulator = require("../sync-accumulator");
+var _utils = require("../utils");
+var _indexeddbHelpers = require("../indexeddb-helpers");
+var _logger = require("../logger");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+const DB_MIGRATIONS = [db => {
+ // 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 => {
+ const oobMembersStore = db.createObjectStore("oob_membership_events", {
+ keyPath: ["room_id", "state_key"]
+ });
+ oobMembersStore.createIndex("room", "room_id");
+}, db => {
+ db.createObjectStore("client_options", {
+ keyPath: ["clobber"]
+ });
+}, db => {
+ 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(store, keyRange, resultMapper) {
+ const query = store.openCursor(keyRange);
+ return new Promise((resolve, reject) => {
+ const results = [];
+ query.onerror = () => {
+ reject(new Error("Query failed: " + query.error));
+ };
+ // collect results
+ query.onsuccess = () => {
+ const cursor = query.result;
+ if (!cursor) {
+ resolve(results);
+ return; // end of results
+ }
+
+ results.push(resultMapper(cursor));
+ cursor.continue();
+ };
+ });
+}
+function txnAsPromise(txn) {
+ return new Promise((resolve, reject) => {
+ txn.oncomplete = function (event) {
+ resolve(event);
+ };
+ txn.onerror = function () {
+ reject(txn.error);
+ };
+ });
+}
+function reqAsEventPromise(req) {
+ return new Promise((resolve, reject) => {
+ req.onsuccess = function (event) {
+ resolve(event);
+ };
+ req.onerror = function () {
+ reject(req.error);
+ };
+ });
+}
+function reqAsPromise(req) {
+ return new Promise((resolve, reject) => {
+ req.onsuccess = () => resolve(req);
+ req.onerror = err => reject(err);
+ });
+}
+function reqAsCursorPromise(req) {
+ return reqAsEventPromise(req).then(event => req.result);
+}
+class LocalIndexedDBStoreBackend {
+ static exists(indexedDB, dbName) {
+ dbName = "matrix-js-sdk:" + (dbName || "default");
+ return (0, _indexeddbHelpers.exists)(indexedDB, dbName);
+ }
+ /**
+ * 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.
+ */
+ constructor(indexedDB, dbName = "default") {
+ this.indexedDB = indexedDB;
+ _defineProperty(this, "dbName", void 0);
+ _defineProperty(this, "syncAccumulator", void 0);
+ _defineProperty(this, "db", void 0);
+ _defineProperty(this, "disconnected", true);
+ _defineProperty(this, "_isNewlyCreated", false);
+ _defineProperty(this, "syncToDatabasePromise", void 0);
+ _defineProperty(this, "pendingUserPresenceData", []);
+ this.dbName = "matrix-js-sdk:" + dbName;
+ this.syncAccumulator = new _syncAccumulator.SyncAccumulator();
+ }
+
+ /**
+ * Attempt to connect to the database. This can fail if the user does not
+ * grant permission.
+ * @returns Promise which resolves if successfully connected.
+ */
+ connect(onClose) {
+ if (!this.disconnected) {
+ _logger.logger.log(`LocalIndexedDBStoreBackend.connect: already connected or connecting`);
+ return Promise.resolve();
+ }
+ this.disconnected = false;
+ _logger.logger.log(`LocalIndexedDBStoreBackend.connect: connecting...`);
+ const req = this.indexedDB.open(this.dbName, VERSION);
+ req.onupgradeneeded = ev => {
+ const db = req.result;
+ const oldVersion = ev.oldVersion;
+ _logger.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 = () => {
+ _logger.logger.log(`can't yet open LocalIndexedDBStoreBackend because it is open elsewhere`);
+ };
+ _logger.logger.log(`LocalIndexedDBStoreBackend.connect: awaiting connection...`);
+ return reqAsEventPromise(req).then(async () => {
+ _logger.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 = () => {
+ this.db?.close(); // this does not call onclose
+ this.disconnected = true;
+ this.db = undefined;
+ onClose?.();
+ };
+ this.db.onclose = () => {
+ this.disconnected = true;
+ this.db = undefined;
+ onClose?.();
+ };
+ await this.init();
+ });
+ }
+
+ /** @returns whether or not the database was newly created in this session. */
+ isNewlyCreated() {
+ return Promise.resolve(this._isNewlyCreated);
+ }
+
+ /**
+ * Having connected, load initial data from the database and prepare for use
+ * @returns Promise which resolves on success
+ */
+ init() {
+ return Promise.all([this.loadAccountData(), this.loadSyncData()]).then(([accountData, syncData]) => {
+ _logger.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
+ */
+ getOutOfBandMembers(roomId) {
+ return new Promise((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 = [];
+ // 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 = () => {
+ 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 => {
+ reject(err);
+ };
+ }).then(events => {
+ _logger.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
+ */
+ async setOutOfBandMembers(roomId, membershipEvents) {
+ _logger.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.logger.log(`LL: backend done storing for ${roomId}!`);
+ }
+ async clearOutOfBandMembers(roomId) {
+ // 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 => (cursor?.primaryKey)[1]);
+ const maxStateKeyProm = reqAsCursorPromise(roomIndex.openKeyCursor(roomRange, "prev")).then(cursor => (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.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.
+ */
+ clearDatabase() {
+ return new Promise(resolve => {
+ _logger.logger.log(`Removing indexeddb instance: ${this.dbName}`);
+ const req = this.indexedDB.deleteDatabase(this.dbName);
+ req.onblocked = () => {
+ _logger.logger.log(`can't yet delete indexeddb ${this.dbName} because it is open elsewhere`);
+ };
+ req.onerror = () => {
+ // 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.logger.warn(`unable to delete js-sdk store indexeddb: ${req.error}`);
+ resolve();
+ };
+ req.onsuccess = () => {
+ _logger.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.
+ */
+ getSavedSync(copy = true) {
+ 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((0, _utils.deepCopy)(data));
+ } else {
+ return Promise.resolve(data);
+ }
+ }
+ getNextBatchToken() {
+ return Promise.resolve(this.syncAccumulator.getNextBatchToken());
+ }
+ setSyncData(syncData) {
+ 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.
+ */
+ async syncToDatabase(userTuples) {
+ if (this.syncToDatabasePromise) {
+ _logger.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;
+ }
+ async doSyncToDatabase(userTuples) {
+ 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.
+ */
+ persistSyncData(nextBatch, roomsData) {
+ _logger.logger.log("Persisting sync data up to", nextBatch);
+ return (0, _utils.promiseTry)(() => {
+ 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.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.
+ */
+ persistAccountData(accountData) {
+ return (0, _utils.promiseTry)(() => {
+ 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.
+ */
+ persistUserPresenceEvents(tuples) {
+ return (0, _utils.promiseTry)(() => {
+ 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.
+ */
+ getUserPresenceEvents() {
+ return (0, _utils.promiseTry)(() => {
+ 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.
+ */
+ loadAccountData() {
+ _logger.logger.log(`LocalIndexedDBStoreBackend: loading account data...`);
+ return (0, _utils.promiseTry)(() => {
+ const txn = this.db.transaction(["accountData"], "readonly");
+ const store = txn.objectStore("accountData");
+ return selectQuery(store, undefined, cursor => {
+ return cursor.value;
+ }).then(result => {
+ _logger.logger.log(`LocalIndexedDBStoreBackend: loaded account data`);
+ return result;
+ });
+ });
+ }
+
+ /**
+ * Load the sync data from the database.
+ * @returns An object with "roomsData" and "nextBatch" keys.
+ */
+ loadSyncData() {
+ _logger.logger.log(`LocalIndexedDBStoreBackend: loading sync data...`);
+ return (0, _utils.promiseTry)(() => {
+ const txn = this.db.transaction(["sync"], "readonly");
+ const store = txn.objectStore("sync");
+ return selectQuery(store, undefined, cursor => {
+ return cursor.value;
+ }).then(results => {
+ _logger.logger.log(`LocalIndexedDBStoreBackend: loaded sync data`);
+ if (results.length > 1) {
+ _logger.logger.warn("loadSyncData: More than 1 sync row found.");
+ }
+ return results.length > 0 ? results[0] : {};
+ });
+ });
+ }
+ getClientOptions() {
+ 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]);
+ });
+ }
+ async storeClientOptions(options) {
+ 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);
+ }
+ async saveToDeviceBatches(batches) {
+ 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);
+ }
+ async getOldestToDeviceBatch() {
+ 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;
+ return {
+ id: cursor.key,
+ txnId: resultBatch.txnId,
+ eventType: resultBatch.eventType,
+ batch: resultBatch.batch
+ };
+ }
+ async removeToDeviceBatch(id) {
+ const txn = this.db.transaction(["to_device_queue"], "readwrite");
+ const store = txn.objectStore("to_device_queue");
+ store.delete(id);
+ await txnAsPromise(txn);
+ }
+
+ /*
+ * Close the database
+ */
+ async destroy() {
+ this.db?.close();
+ }
+}
+exports.LocalIndexedDBStoreBackend = LocalIndexedDBStoreBackend; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-remote-backend.js b/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-remote-backend.js
new file mode 100644
index 0000000000..378a41e8d1
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-remote-backend.js
@@ -0,0 +1,200 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.RemoteIndexedDBStoreBackend = void 0;
+var _logger = require("../logger");
+var _utils = require("../utils");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+class RemoteIndexedDBStoreBackend {
+ // Callback for when the IndexedDB gets closed unexpectedly
+
+ /**
+ * 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.
+ */
+ constructor(workerFactory, dbName) {
+ this.workerFactory = workerFactory;
+ this.dbName = dbName;
+ _defineProperty(this, "worker", void 0);
+ _defineProperty(this, "nextSeq", 0);
+ // The currently in-flight requests to the actual backend
+ _defineProperty(this, "inFlight", {});
+ // seq: promise
+ // Once we start connecting, we keep the promise and re-use it
+ // if we try to connect again
+ _defineProperty(this, "startPromise", void 0);
+ _defineProperty(this, "onWorkerMessage", ev => {
+ 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.logger.error("Got reply from worker with no seq");
+ return;
+ }
+ const def = this.inFlight[msg.seq];
+ if (def === undefined) {
+ _logger.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.logger.warn("Unrecognised message from worker: ", msg);
+ }
+ });
+ }
+
+ /**
+ * Attempt to connect to the database. This can fail if the user does not
+ * grant permission.
+ * @returns Promise which resolves if successfully connected.
+ */
+ connect(onClose) {
+ 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.
+ */
+ clearDatabase() {
+ return this.ensureStarted().then(() => this.doCmd("clearDatabase"));
+ }
+
+ /** @returns whether or not the database was newly created in this session. */
+ isNewlyCreated() {
+ 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.
+ */
+ getSavedSync() {
+ return this.doCmd("getSavedSync");
+ }
+ getNextBatchToken() {
+ return this.doCmd("getNextBatchToken");
+ }
+ setSyncData(syncData) {
+ return this.doCmd("setSyncData", [syncData]);
+ }
+ syncToDatabase(userTuples) {
+ 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
+ */
+ getOutOfBandMembers(roomId) {
+ 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
+ */
+ setOutOfBandMembers(roomId, membershipEvents) {
+ return this.doCmd("setOutOfBandMembers", [roomId, membershipEvents]);
+ }
+ clearOutOfBandMembers(roomId) {
+ return this.doCmd("clearOutOfBandMembers", [roomId]);
+ }
+ getClientOptions() {
+ return this.doCmd("getClientOptions");
+ }
+ storeClientOptions(options) {
+ 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.
+ */
+ getUserPresenceEvents() {
+ return this.doCmd("getUserPresenceEvents");
+ }
+ async saveToDeviceBatches(batches) {
+ return this.doCmd("saveToDeviceBatches", [batches]);
+ }
+ async getOldestToDeviceBatch() {
+ return this.doCmd("getOldestToDeviceBatch");
+ }
+ async removeToDeviceBatch(id) {
+ return this.doCmd("removeToDeviceBatch", [id]);
+ }
+ ensureStarted() {
+ 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.logger.log("IndexedDB worker is ready");
+ });
+ }
+ return this.startPromise;
+ }
+ doCmd(command, args) {
+ // wrap in a q so if the postMessage throws,
+ // the promise automatically gets rejected
+ return Promise.resolve().then(() => {
+ const seq = this.nextSeq++;
+ const def = (0, _utils.defer)();
+ this.inFlight[seq] = def;
+ this.worker?.postMessage({
+ command,
+ seq,
+ args
+ });
+ return def.promise;
+ });
+ }
+ /*
+ * Destroy the web worker
+ */
+ async destroy() {
+ this.worker?.terminate();
+ }
+}
+exports.RemoteIndexedDBStoreBackend = RemoteIndexedDBStoreBackend; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-store-worker.js b/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-store-worker.js
new file mode 100644
index 0000000000..4708d58936
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-store-worker.js
@@ -0,0 +1,151 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.IndexedDBStoreWorker = void 0;
+var _indexeddbLocalBackend = require("./indexeddb-local-backend");
+var _logger = require("../logger");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+/**
+ * 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.
+ *
+ */
+class IndexedDBStoreWorker {
+ /**
+ * @param postMessage - The web worker postMessage function that
+ * should be used to communicate back to the main script.
+ */
+ constructor(postMessage) {
+ this.postMessage = postMessage;
+ _defineProperty(this, "backend", void 0);
+ _defineProperty(this, "onClose", () => {
+ 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
+ */
+ _defineProperty(this, "onMessage", ev => {
+ const msg = ev.data;
+ let prom;
+ 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 _indexeddbLocalBackend.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.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
+ }
+ });
+ });
+ });
+ }
+}
+exports.IndexedDBStoreWorker = IndexedDBStoreWorker; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb.js b/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb.js
new file mode 100644
index 0000000000..3c52a70546
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb.js
@@ -0,0 +1,329 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.IndexedDBStore = void 0;
+var _memory = require("./memory");
+var _indexeddbLocalBackend = require("./indexeddb-local-backend");
+var _indexeddbRemoteBackend = require("./indexeddb-remote-backend");
+var _user = require("../models/user");
+var _event = require("../models/event");
+var _logger = require("../logger");
+var _typedEventEmitter = require("../models/typed-event-emitter");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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 */
+/**
+ * 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
+
+class IndexedDBStore extends _memory.MemoryStore {
+ static exists(indexedDB, dbName) {
+ return _indexeddbLocalBackend.LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
+ }
+
+ /**
+ * The backend instance.
+ * Call through to this API if you need to perform specific indexeddb actions like deleting the database.
+ */
+
+ /**
+ * 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.
+ */
+ constructor(opts) {
+ super(opts);
+ _defineProperty(this, "backend", void 0);
+ _defineProperty(this, "startedUp", false);
+ _defineProperty(this, "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.
+ _defineProperty(this, "userModifiedMap", {});
+ // user_id : timestamp
+ _defineProperty(this, "emitter", new _typedEventEmitter.TypedEventEmitter());
+ _defineProperty(this, "on", this.emitter.on.bind(this.emitter));
+ _defineProperty(this, "onClose", () => {
+ 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.
+ */
+ _defineProperty(this, "getSavedSync", this.degradable(() => {
+ return this.backend.getSavedSync();
+ }, "getSavedSync"));
+ /** @returns whether or not the database was newly created in this session. */
+ _defineProperty(this, "isNewlyCreated", this.degradable(() => {
+ return this.backend.isNewlyCreated();
+ }, "isNewlyCreated"));
+ /**
+ * @returns If there is a saved sync, the nextBatch token
+ * for this sync, otherwise null.
+ */
+ _defineProperty(this, "getSavedSyncToken", this.degradable(() => {
+ return this.backend.getNextBatchToken();
+ }, "getSavedSyncToken"));
+ /**
+ * Delete all data from this store.
+ * @returns Promise which resolves if the data was deleted from the database.
+ */
+ _defineProperty(this, "deleteAllData", this.degradable(() => {
+ super.deleteAllData();
+ return this.backend.clearDatabase().then(() => {
+ _logger.logger.log("Deleted indexeddb data.");
+ }, err => {
+ _logger.logger.error(`Failed to delete indexeddb data: ${err}`);
+ throw err;
+ });
+ }));
+ _defineProperty(this, "reallySave", this.degradable(() => {
+ 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 = [];
+ 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);
+ }));
+ _defineProperty(this, "setSyncData", this.degradable(syncData => {
+ 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
+ */
+ _defineProperty(this, "getOutOfBandMembers", this.degradable(roomId => {
+ 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
+ */
+ _defineProperty(this, "setOutOfBandMembers", this.degradable((roomId, membershipEvents) => {
+ super.setOutOfBandMembers(roomId, membershipEvents);
+ return this.backend.setOutOfBandMembers(roomId, membershipEvents);
+ }, "setOutOfBandMembers"));
+ _defineProperty(this, "clearOutOfBandMembers", this.degradable(roomId => {
+ super.clearOutOfBandMembers(roomId);
+ return this.backend.clearOutOfBandMembers(roomId);
+ }, "clearOutOfBandMembers"));
+ _defineProperty(this, "getClientOptions", this.degradable(() => {
+ return this.backend.getClientOptions();
+ }, "getClientOptions"));
+ _defineProperty(this, "storeClientOptions", this.degradable(options => {
+ super.storeClientOptions(options);
+ return this.backend.storeClientOptions(options);
+ }, "storeClientOptions"));
+ if (!opts.indexedDB) {
+ throw new Error("Missing required option: indexedDB");
+ }
+ if (opts.workerFactory) {
+ this.backend = new _indexeddbRemoteBackend.RemoteIndexedDBStoreBackend(opts.workerFactory, opts.dbName);
+ } else {
+ this.backend = new _indexeddbLocalBackend.LocalIndexedDBStoreBackend(opts.indexedDB, opts.dbName);
+ }
+ }
+ /**
+ * @returns Resolved when loaded from indexed db.
+ */
+ startup() {
+ if (this.startedUp) {
+ _logger.logger.log(`IndexedDBStore.startup: already started`);
+ return Promise.resolve();
+ }
+ _logger.logger.log(`IndexedDBStore.startup: connecting to backend`);
+ return this.backend.connect(this.onClose).then(() => {
+ _logger.logger.log(`IndexedDBStore.startup: loading presence events`);
+ return this.backend.getUserPresenceEvents();
+ }).then(userPresenceEvents => {
+ _logger.logger.log(`IndexedDBStore.startup: processing presence events`);
+ userPresenceEvents.forEach(([userId, rawEvent]) => {
+ const u = new _user.User(userId);
+ if (rawEvent) {
+ u.setPresenceEvent(new _event.MatrixEvent(rawEvent));
+ }
+ this.userModifiedMap[u.userId] = u.getLastModifiedTime();
+ this.storeUser(u);
+ });
+ this.startedUp = true;
+ });
+ }
+
+ /*
+ * Close the database and destroy any associated workers
+ */
+ destroy() {
+ return this.backend.destroy();
+ }
+ /**
+ * 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).
+ */
+ wantsSave() {
+ 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)
+ */
+ save(force = false) {
+ if (force || this.wantsSave()) {
+ return this.reallySave();
+ }
+ return Promise.resolve();
+ }
+ /**
+ * 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.
+ */
+ degradable(func, fallback) {
+ const fallbackFn = fallback ? super[fallback] : null;
+ return async (...args) => {
+ try {
+ return await func.call(this, ...args);
+ } catch (e) {
+ _logger.logger.error("IndexedDBStore failure, degrading to MemoryStore", e);
+ this.emitter.emit("degraded", e);
+ 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.logger.log("IndexedDBStore trying to delete degraded data");
+ await this.backend.clearDatabase();
+ _logger.logger.log("IndexedDBStore delete after degrading succeeded");
+ } catch (e) {
+ _logger.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.
+ async getPendingEvents(roomId) {
+ if (!this.localStorage) return super.getPendingEvents(roomId);
+ const serialized = this.localStorage.getItem(pendingEventsKey(roomId));
+ if (serialized) {
+ try {
+ return JSON.parse(serialized);
+ } catch (e) {
+ _logger.logger.error("Could not parse persisted pending events", e);
+ }
+ }
+ return [];
+ }
+ async setPendingEvents(roomId, events) {
+ 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));
+ }
+ }
+ saveToDeviceBatches(batches) {
+ return this.backend.saveToDeviceBatches(batches);
+ }
+ getOldestToDeviceBatch() {
+ return this.backend.getOldestToDeviceBatch();
+ }
+ removeToDeviceBatch(id) {
+ return this.backend.removeToDeviceBatch(id);
+ }
+}
+
+/**
+ * @param roomId - ID of the current room
+ * @returns Storage key to retrieve pending events
+ */
+exports.IndexedDBStore = IndexedDBStore;
+function pendingEventsKey(roomId) {
+ return `mx_pending_events_${roomId}`;
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/store/local-storage-events-emitter.js b/comm/chat/protocols/matrix/lib/matrix-sdk/store/local-storage-events-emitter.js
new file mode 100644
index 0000000000..bb366d32dd
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/store/local-storage-events-emitter.js
@@ -0,0 +1,43 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.localStorageErrorsEventsEmitter = exports.LocalStorageErrors = void 0;
+var _typedEventEmitter = require("../models/typed-event-emitter");
+/*
+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.
+*/
+let LocalStorageErrors = /*#__PURE__*/function (LocalStorageErrors) {
+ LocalStorageErrors["Global"] = "Global";
+ LocalStorageErrors["SetItemError"] = "setItem";
+ LocalStorageErrors["GetItemError"] = "getItem";
+ LocalStorageErrors["RemoveItemError"] = "removeItem";
+ LocalStorageErrors["ClearError"] = "clear";
+ LocalStorageErrors["QuotaExceededError"] = "QuotaExceededError";
+ return LocalStorageErrors;
+}({});
+exports.LocalStorageErrors = LocalStorageErrors;
+/**
+ * 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.TypedEventEmitter {}
+const localStorageErrorsEventsEmitter = new LocalStorageErrorsEventsEmitter();
+exports.localStorageErrorsEventsEmitter = localStorageErrorsEventsEmitter; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/store/memory.js b/comm/chat/protocols/matrix/lib/matrix-sdk/store/memory.js
new file mode 100644
index 0000000000..68836ac093
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/store/memory.js
@@ -0,0 +1,418 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.MemoryStore = void 0;
+var _user = require("../models/user");
+var _roomState = require("../models/room-state");
+var _utils = require("../utils");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+function isValidFilterId(filterId) {
+ 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";
+}
+class MemoryStore {
+ /**
+ * Construct a new in-memory data store for the Matrix Client.
+ * @param opts - Config options
+ */
+ constructor(opts = {}) {
+ _defineProperty(this, "rooms", {});
+ // roomId: Room
+ _defineProperty(this, "users", {});
+ // userId: User
+ _defineProperty(this, "syncToken", null);
+ // userId: {
+ // filterId: Filter
+ // }
+ _defineProperty(this, "filters", new _utils.MapWithDefault(() => new Map()));
+ _defineProperty(this, "accountData", new Map());
+ // type: content
+ _defineProperty(this, "localStorage", void 0);
+ _defineProperty(this, "oobMembers", new Map());
+ // roomId: [member events]
+ _defineProperty(this, "pendingEvents", {});
+ _defineProperty(this, "clientOptions", void 0);
+ _defineProperty(this, "pendingToDeviceBatches", []);
+ _defineProperty(this, "nextToDeviceBatchId", 0);
+ /**
+ * Called when a room member in a room being tracked by this store has been
+ * updated.
+ */
+ _defineProperty(this, "onRoomMember", (event, state, member) => {
+ 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.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;
+ });
+ this.localStorage = opts.localStorage;
+ }
+
+ /**
+ * Retrieve the token to stream from.
+ * @returns The token or null.
+ */
+ getSyncToken() {
+ return this.syncToken;
+ }
+
+ /** @returns whether or not the database was newly created in this session. */
+ isNewlyCreated() {
+ return Promise.resolve(true);
+ }
+
+ /**
+ * Set the token to stream from.
+ * @param token - The token to stream from.
+ */
+ setSyncToken(token) {
+ this.syncToken = token;
+ }
+
+ /**
+ * Store the given room.
+ * @param room - The room to be stored. All properties must be stored.
+ */
+ storeRoom(room) {
+ 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(_roomState.RoomStateEvent.Members, this.onRoomMember);
+ // add existing members
+ room.currentState.getMembers().forEach(m => {
+ this.onRoomMember(null, room.currentState, m);
+ });
+ }
+ /**
+ * Retrieve a room by its' room ID.
+ * @param roomId - The room ID.
+ * @returns The room or null.
+ */
+ getRoom(roomId) {
+ return this.rooms[roomId] || null;
+ }
+
+ /**
+ * Retrieve all known rooms.
+ * @returns A list of rooms, which may be empty.
+ */
+ getRooms() {
+ return Object.values(this.rooms);
+ }
+
+ /**
+ * Permanently delete a room.
+ */
+ removeRoom(roomId) {
+ if (this.rooms[roomId]) {
+ this.rooms[roomId].currentState.removeListener(_roomState.RoomStateEvent.Members, this.onRoomMember);
+ }
+ delete this.rooms[roomId];
+ }
+
+ /**
+ * Retrieve a summary of all the rooms.
+ * @returns A summary of each room.
+ */
+ getRoomSummaries() {
+ return Object.values(this.rooms).map(function (room) {
+ return room.summary;
+ });
+ }
+
+ /**
+ * Store a User.
+ * @param user - The user to store.
+ */
+ storeUser(user) {
+ this.users[user.userId] = user;
+ }
+
+ /**
+ * Retrieve a User by its' user ID.
+ * @param userId - The user ID.
+ * @returns The user or null.
+ */
+ getUser(userId) {
+ return this.users[userId] || null;
+ }
+
+ /**
+ * Retrieve all known users.
+ * @returns A list of users, which may be empty.
+ */
+ getUsers() {
+ 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.
+ */
+ scrollback(room, limit) {
+ 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.
+ */
+ storeEvents(room, events, token, toStart) {
+ // no-op because they've already been added to the room instance.
+ }
+
+ /**
+ * Store a filter.
+ */
+ storeFilter(filter) {
+ if (!filter?.userId || !filter?.filterId) return;
+ this.filters.getOrCreate(filter.userId).set(filter.filterId, filter);
+ }
+
+ /**
+ * Retrieve a filter.
+ * @returns A filter or null.
+ */
+ getFilter(userId, filterId) {
+ 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.
+ */
+ getFilterIdByName(filterName) {
+ 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.
+ */
+ setFilterIdByName(filterName, filterId) {
+ 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.
+ */
+ storeAccountDataEvents(events) {
+ 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
+ */
+ getAccountData(eventType) {
+ 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.
+ */
+ setSyncData(syncData) {
+ return Promise.resolve();
+ }
+
+ /**
+ * We never want to save becase we have nothing to save to.
+ *
+ * @returns If the store wants to save
+ */
+ wantsSave() {
+ 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)
+ */
+ save(force) {
+ return Promise.resolve();
+ }
+
+ /**
+ * Startup does nothing as this store doesn't require starting up.
+ * @returns An immediately resolved promise.
+ */
+ startup() {
+ 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.
+ */
+ getSavedSync() {
+ return Promise.resolve(null);
+ }
+
+ /**
+ * @returns If there is a saved sync, the nextBatch token
+ * for this sync, otherwise null.
+ */
+ getSavedSyncToken() {
+ return Promise.resolve(null);
+ }
+
+ /**
+ * Delete all data from this store.
+ * @returns An immediately resolved promise.
+ */
+ deleteAllData() {
+ this.rooms = {
+ // roomId: Room
+ };
+ this.users = {
+ // userId: User
+ };
+ this.syncToken = null;
+ this.filters = new _utils.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
+ */
+ getOutOfBandMembers(roomId) {
+ 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
+ */
+ setOutOfBandMembers(roomId, membershipEvents) {
+ this.oobMembers.set(roomId, membershipEvents);
+ return Promise.resolve();
+ }
+ clearOutOfBandMembers(roomId) {
+ this.oobMembers.delete(roomId);
+ return Promise.resolve();
+ }
+ getClientOptions() {
+ return Promise.resolve(this.clientOptions);
+ }
+ storeClientOptions(options) {
+ this.clientOptions = Object.assign({}, options);
+ return Promise.resolve();
+ }
+ async getPendingEvents(roomId) {
+ return this.pendingEvents[roomId] ?? [];
+ }
+ async setPendingEvents(roomId, events) {
+ this.pendingEvents[roomId] = events;
+ }
+ saveToDeviceBatches(batches) {
+ for (const batch of batches) {
+ this.pendingToDeviceBatches.push({
+ id: this.nextToDeviceBatchId++,
+ eventType: batch.eventType,
+ txnId: batch.txnId,
+ batch: batch.batch
+ });
+ }
+ return Promise.resolve();
+ }
+ async getOldestToDeviceBatch() {
+ if (this.pendingToDeviceBatches.length === 0) return null;
+ return this.pendingToDeviceBatches[0];
+ }
+ removeToDeviceBatch(id) {
+ this.pendingToDeviceBatches = this.pendingToDeviceBatches.filter(batch => batch.id !== id);
+ return Promise.resolve();
+ }
+ async destroy() {
+ // Nothing to do
+ }
+}
+exports.MemoryStore = MemoryStore; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/store/stub.js b/comm/chat/protocols/matrix/lib/matrix-sdk/store/stub.js
new file mode 100644
index 0000000000..9bf1df937d
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/store/stub.js
@@ -0,0 +1,262 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.StubStore = void 0;
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
+/*
+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.
+ */
+
+/**
+ * Construct a stub store. This does no-ops on most store methods.
+ */
+class StubStore {
+ constructor() {
+ _defineProperty(this, "accountData", new Map());
+ // stub
+ _defineProperty(this, "fromToken", null);
+ }
+ /** @returns whether or not the database was newly created in this session. */
+ isNewlyCreated() {
+ return Promise.resolve(true);
+ }
+
+ /**
+ * Get the sync token.
+ */
+ getSyncToken() {
+ return this.fromToken;
+ }
+
+ /**
+ * Set the sync token.
+ */
+ setSyncToken(token) {
+ this.fromToken = token;
+ }
+
+ /**
+ * No-op.
+ */
+ storeRoom(room) {}
+
+ /**
+ * No-op.
+ */
+ getRoom(roomId) {
+ return null;
+ }
+
+ /**
+ * No-op.
+ * @returns An empty array.
+ */
+ getRooms() {
+ return [];
+ }
+
+ /**
+ * Permanently delete a room.
+ */
+ removeRoom(roomId) {
+ return;
+ }
+
+ /**
+ * No-op.
+ * @returns An empty array.
+ */
+ getRoomSummaries() {
+ return [];
+ }
+
+ /**
+ * No-op.
+ */
+ storeUser(user) {}
+
+ /**
+ * No-op.
+ */
+ getUser(userId) {
+ return null;
+ }
+
+ /**
+ * No-op.
+ */
+ getUsers() {
+ return [];
+ }
+
+ /**
+ * No-op.
+ */
+ scrollback(room, limit) {
+ 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.
+ */
+ storeEvents(room, events, token, toStart) {}
+
+ /**
+ * Store a filter.
+ */
+ storeFilter(filter) {}
+
+ /**
+ * Retrieve a filter.
+ * @returns A filter or null.
+ */
+ getFilter(userId, filterId) {
+ return null;
+ }
+
+ /**
+ * Retrieve a filter ID with the given name.
+ * @param filterName - The filter name.
+ * @returns The filter ID or null.
+ */
+ getFilterIdByName(filterName) {
+ return null;
+ }
+
+ /**
+ * Set a filter name to ID mapping.
+ */
+ setFilterIdByName(filterName, filterId) {}
+
+ /**
+ * Store user-scoped account data events
+ * @param events - The events to store.
+ */
+ storeAccountDataEvents(events) {}
+
+ /**
+ * Get account data event by event type
+ * @param eventType - The event type being queried
+ */
+ getAccountData(eventType) {
+ return undefined;
+ }
+
+ /**
+ * setSyncData does nothing as there is no backing data store.
+ *
+ * @param syncData - The sync data
+ * @returns An immediately resolved promise.
+ */
+ setSyncData(syncData) {
+ return Promise.resolve();
+ }
+
+ /**
+ * We never want to save because we have nothing to save to.
+ *
+ * @returns If the store wants to save
+ */
+ wantsSave() {
+ return false;
+ }
+
+ /**
+ * Save does nothing as there is no backing data store.
+ */
+ save() {
+ return Promise.resolve();
+ }
+
+ /**
+ * Startup does nothing.
+ * @returns An immediately resolved promise.
+ */
+ startup() {
+ 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.
+ */
+ getSavedSync() {
+ return Promise.resolve(null);
+ }
+
+ /**
+ * @returns If there is a saved sync, the nextBatch token
+ * for this sync, otherwise null.
+ */
+ getSavedSyncToken() {
+ return Promise.resolve(null);
+ }
+
+ /**
+ * Delete all data from this store. Does nothing since this store
+ * doesn't store anything.
+ * @returns An immediately resolved promise.
+ */
+ deleteAllData() {
+ return Promise.resolve();
+ }
+ getOutOfBandMembers() {
+ return Promise.resolve(null);
+ }
+ setOutOfBandMembers(roomId, membershipEvents) {
+ return Promise.resolve();
+ }
+ clearOutOfBandMembers() {
+ return Promise.resolve();
+ }
+ getClientOptions() {
+ return Promise.resolve(undefined);
+ }
+ storeClientOptions(options) {
+ return Promise.resolve();
+ }
+ async getPendingEvents(roomId) {
+ return [];
+ }
+ setPendingEvents(roomId, events) {
+ return Promise.resolve();
+ }
+ async saveToDeviceBatches(batch) {
+ return Promise.resolve();
+ }
+ getOldestToDeviceBatch() {
+ return Promise.resolve(null);
+ }
+ async removeToDeviceBatch(id) {
+ return Promise.resolve();
+ }
+ async destroy() {
+ // Nothing to do
+ }
+}
+exports.StubStore = StubStore; \ No newline at end of file