diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/chat/protocols/matrix/lib/matrix-sdk/store | |
parent | Initial commit. (diff) | |
download | thunderbird-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')
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 |