diff --git a/src/models/room-state.js b/src/models/room-state.js index fb049daa9..bb8e93fb9 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -443,6 +443,22 @@ RoomState.prototype.markOutOfBandMembersFailed = function() { this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED; }; +/** + * Clears the loaded out-of-band members + */ +RoomState.prototype.clearOutOfBandMembers = function() { + let count = 0; + Object.keys(this.members).forEach((userId) => { + const member = this.members[userId]; + if (member.isOutOfBand()) { + ++count; + delete this.members[userId]; + } + }); + console.log(`LL: RoomState removed ${count} members...`); + this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED; +}; + /** * Sets the loaded out-of-band members. * @param {MatrixEvent[]} stateEvents array of membership state events diff --git a/src/models/room.js b/src/models/room.js index ce7054493..b4bd2c111 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -456,6 +456,30 @@ Room.prototype.loadMembersIfNeeded = function() { return this._membersPromise; }; +/** + * Removes the lazily loaded members from storage if needed + */ +Room.prototype.clearLoadedMembersIfNeeded = async function() { + if (this._opts.lazyLoadMembers && this._membersPromise) { + await this.loadMembersIfNeeded(); + await this._client.store.clearOutOfBandMembers(this.roomId); + this.currentState.clearOutOfBandMembers(); + this._membersPromise = null; + } +}; + +/** + * called when sync receives this room in the leave section + * to do cleanup after leaving a room. Possibly called multiple times. + */ +Room.prototype.onLeft = function() { + this.clearLoadedMembersIfNeeded().catch((err) => { + console.error(`error after clearing loaded members from ` + + `room ${this.roomId} after leaving`); + console.dir(err); + }); +}; + /** * Reset the live timeline of all timelineSets, and start new ones. * diff --git a/src/store/indexeddb-local-backend.js b/src/store/indexeddb-local-backend.js index b24bcbdbe..4bcbbe6e2 100644 --- a/src/store/indexeddb-local-backend.js +++ b/src/store/indexeddb-local-backend.js @@ -71,7 +71,7 @@ function selectQuery(store, keyRange, resultMapper) { }); } -function promiseifyTxn(txn) { +function txnAsPromise(txn) { return new Promise((resolve, reject) => { txn.oncomplete = function(event) { resolve(event); @@ -82,7 +82,7 @@ function promiseifyTxn(txn) { }); } -function promiseifyRequest(req) { +function reqAsEventPromise(req) { return new Promise((resolve, reject) => { req.onsuccess = function(event) { resolve(event); @@ -93,6 +93,17 @@ function promiseifyRequest(req) { }); } +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) => event.target.result); +} + /** * Does the actual reading from and writing to the indexeddb * @@ -159,7 +170,7 @@ LocalIndexedDBStoreBackend.prototype = { console.log( `LocalIndexedDBStoreBackend.connect: awaiting connection...`, ); - return promiseifyRequest(req).then((ev) => { + return reqAsEventPromise(req).then((ev) => { console.log( `LocalIndexedDBStoreBackend.connect: connected`, ); @@ -265,7 +276,7 @@ LocalIndexedDBStoreBackend.prototype = { const tx = this.db.transaction(["oob_membership_events"], "readwrite"); const store = tx.objectStore("oob_membership_events"); const eventPuts = membershipEvents.map((e) => { - const putPromise = promiseifyRequest(store.put(e)); + const putPromise = reqAsEventPromise(store.put(e)); // ignoring the result makes sure we discard the IDB success event // ASAP, and not create a potentially big array containing them // unneccesarily later on by calling Promise.all. @@ -281,7 +292,7 @@ LocalIndexedDBStoreBackend.prototype = { oob_written: true, state_key: 0, }; - const markerPut = promiseifyRequest(store.put(markerObject)); + const markerPut = reqAsEventPromise(store.put(markerObject)); const allPuts = eventPuts.concat(markerPut); // ignore the empty array Promise.all creates // as this method should just resolve @@ -292,6 +303,44 @@ LocalIndexedDBStoreBackend.prototype = { }); }, + clearOutOfBandMembers: async function(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 && cursor.primaryKey[1]); + const maxStateKeyProm = reqAsCursorPromise( + roomIndex.openKeyCursor(roomRange, "prev"), + ).then((cursor) => 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], + ); + + console.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. @@ -389,7 +438,7 @@ LocalIndexedDBStoreBackend.prototype = { roomsData: roomsData, groupsData: groupsData, }); // put == UPSERT - return promiseifyTxn(txn); + return txnAsPromise(txn); }); }, @@ -406,7 +455,7 @@ LocalIndexedDBStoreBackend.prototype = { for (let i = 0; i < accountData.length; i++) { store.put(accountData[i]); // put == UPSERT } - return promiseifyTxn(txn); + return txnAsPromise(txn); }); }, @@ -428,7 +477,7 @@ LocalIndexedDBStoreBackend.prototype = { event: tuple[1], }); // put == UPSERT } - return promiseifyTxn(txn); + return txnAsPromise(txn); }); }, diff --git a/src/store/indexeddb-remote-backend.js b/src/store/indexeddb-remote-backend.js index 7a58ded1b..85f07f86b 100644 --- a/src/store/indexeddb-remote-backend.js +++ b/src/store/indexeddb-remote-backend.js @@ -110,6 +110,10 @@ RemoteIndexedDBStoreBackend.prototype = { return this._doCmd('setOutOfBandMembers', [roomId, membershipEvents]); }, + clearOutOfBandMembers: function(roomId) { + return this._doCmd('clearOutOfBandMembers', [roomId]); + }, + /** * Load all user presence events from the database. This is not cached. * @return {Promise} A list of presence events in their raw form. diff --git a/src/store/indexeddb-store-worker.js b/src/store/indexeddb-store-worker.js index 021156afc..adfc3535b 100644 --- a/src/store/indexeddb-store-worker.js +++ b/src/store/indexeddb-store-worker.js @@ -95,6 +95,9 @@ class IndexedDBStoreWorker { 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; diff --git a/src/store/indexeddb.js b/src/store/indexeddb.js index 5e027537e..0de57c89a 100644 --- a/src/store/indexeddb.js +++ b/src/store/indexeddb.js @@ -242,4 +242,8 @@ IndexedDBStore.prototype.setOutOfBandMembers = function(roomId, membershipEvents return this.backend.setOutOfBandMembers(roomId, membershipEvents); }; +IndexedDBStore.prototype.clearOutOfBandMembers = function(roomId) { + return this.backend.clearOutOfBandMembers(roomId); +}; + module.exports.IndexedDBStore = IndexedDBStore; diff --git a/src/store/stub.js b/src/store/stub.js index 9c628b287..d0c2cabc5 100644 --- a/src/store/stub.js +++ b/src/store/stub.js @@ -272,6 +272,10 @@ StubStore.prototype = { setOutOfBandMembers: function() { return Promise.resolve(); }, + + clearOutOfBandMembers: function() { + return Promise.resolve(); + }, }; /** Stub Store class. */ diff --git a/src/sync.js b/src/sync.js index 1a4566db2..970a162c9 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1141,6 +1141,8 @@ SyncApi.prototype._processSyncResponse = async function( accountDataEvents.forEach(function(e) { client.emit("event", e); }); + + room.onLeft(); }); // update the notification timeline, if appropriate.