diff --git a/src/client.js b/src/client.js
index eaafeb90a11..383b7f23cf9 100644
--- a/src/client.js
+++ b/src/client.js
@@ -2733,10 +2733,10 @@ MatrixClient.prototype.getTurnServers = function() {
/**
- * High level helper method to call initialSync, emit the resulting events,
- * and then start polling the eventStream for new events. To listen for these
+ * High level helper method to begin syncing and poll for new events. To listen for these
* events, add a listener for {@link module:client~MatrixClient#event:"event"}
- * via {@link module:client~MatrixClient#on}.
+ * via {@link module:client~MatrixClient#on}. Alternatively, listen for specific
+ * state change events.
* @param {Object=} opts Options to apply when syncing.
* @param {Number=} opts.initialSyncLimit The event limit=
to apply
* to initial sync. Default: 8.
@@ -2753,7 +2753,7 @@ MatrixClient.prototype.getTurnServers = function() {
* accessbile via {@link module:models/room#getPendingEvents}. Default:
* "chronological".
*
- * @param {Number=} opts.pollTimeout The number of milliseconds to wait on /events.
+ * @param {Number=} opts.pollTimeout The number of milliseconds to wait on /sync.
* Default: 30000 (30 seconds).
*
* @param {Filter=} opts.filter The filter to apply to /sync calls. This will override
@@ -2790,6 +2790,12 @@ MatrixClient.prototype.startClient = function(opts) {
opts.crypto = this._crypto;
opts.syncAccumulator = this._syncAccumulator;
+ opts.canResetEntireTimeline = (roomId) => {
+ if (!this._canResetTimelineCallback) {
+ return false;
+ }
+ return this._canResetTimelineCallback(roomId);
+ };
this._clientOpts = opts;
this._syncApi = new SyncApi(this, opts);
@@ -2810,6 +2816,27 @@ MatrixClient.prototype.stopClient = function() {
global.clearTimeout(this._checkTurnServersTimeoutID);
};
+/*
+ * Set a function which is called when /sync returns a 'limited' response.
+ * It is called with a room ID and returns a boolean. It should return 'true' if the SDK
+ * can SAFELY remove events from this room. It may not be safe to remove events if there
+ * are other references to the timelines for this room, e.g because the client is
+ * actively viewing events in this room.
+ * Default: returns false.
+ * @param {Function} cb The callback which will be invoked.
+ */
+MatrixClient.prototype.setCanResetTimelineCallback = function(cb) {
+ this._canResetTimelineCallback = cb;
+};
+
+/**
+ * Get the callback set via `setCanResetTimelineCallback`.
+ * @return {?Function} The callback or null
+ */
+MatrixClient.prototype.getCanResetTimelineCallback = function() {
+ return this._canResetTimelineCallback;
+};
+
function setupCallEventHandler(client) {
const candidatesByCall = {
// callId: [Candidate]
diff --git a/src/models/event-timeline-set.js b/src/models/event-timeline-set.js
index 927049e1352..4b46eccf523 100644
--- a/src/models/event-timeline-set.js
+++ b/src/models/event-timeline-set.js
@@ -154,10 +154,11 @@ EventTimelineSet.prototype.replaceEventId = function(oldEventId, newEventId) {
* @fires module:client~MatrixClient#event:"Room.timelineReset"
*/
EventTimelineSet.prototype.resetLiveTimeline = function(backPaginationToken, flush) {
- let newTimeline;
+ // if timeline support is disabled, forget about the old timelines
+ const resetAllTimelines = !this._timelineSupport || flush;
- if (!this._timelineSupport || flush) {
- // if timeline support is disabled, forget about the old timelines
+ let newTimeline;
+ if (resetAllTimelines) {
newTimeline = new EventTimeline(this);
this._timelines = [newTimeline];
this._eventIdToTimeline = {};
@@ -187,7 +188,7 @@ EventTimelineSet.prototype.resetLiveTimeline = function(backPaginationToken, flu
newTimeline.setPaginationToken(backPaginationToken, EventTimeline.BACKWARDS);
this._liveTimeline = newTimeline;
- this.emit("Room.timelineReset", this.room, this);
+ this.emit("Room.timelineReset", this.room, this, resetAllTimelines);
};
/**
@@ -655,4 +656,5 @@ module.exports = EventTimelineSet;
* @event module:client~MatrixClient#"Room.timelineReset"
* @param {Room} room The room whose live timeline was reset, if any
* @param {EventTimelineSet} timelineSet timelineSet room whose live timeline was reset
+ * @param {boolean} resetAllTimelines True if all timelines were reset.
*/
diff --git a/src/models/event.js b/src/models/event.js
index 93729a31fa6..e91b57f95d2 100644
--- a/src/models/event.js
+++ b/src/models/event.js
@@ -49,6 +49,8 @@ module.exports.EventStatus = {
CANCELLED: "cancelled",
};
+const interns = {};
+
/**
* Construct a Matrix Event object
* @constructor
@@ -75,7 +77,31 @@ module.exports.EventStatus = {
module.exports.MatrixEvent = function MatrixEvent(
event,
) {
+ // intern the values of matrix events to force share strings and reduce the
+ // amount of needless string duplication. This can save moderate amounts of
+ // memory (~10% on a 350MB heap).
+ ["state_key", "type", "sender", "room_id"].forEach((prop) => {
+ if (!event[prop]) {
+ return;
+ }
+ if (!interns[event[prop]]) {
+ interns[event[prop]] = event[prop];
+ }
+ event[prop] = interns[event[prop]];
+ });
+
+ ["membership", "avatar_url", "displayname"].forEach((prop) => {
+ if (!event.content || !event.content[prop]) {
+ return;
+ }
+ if (!interns[event.content[prop]]) {
+ interns[event.content[prop]] = event.content[prop];
+ }
+ event.content[prop] = interns[event.content[prop]];
+ });
+
this.event = event || {};
+
this.sender = null;
this.target = null;
this.status = null;
diff --git a/src/models/room.js b/src/models/room.js
index 238d20c84ed..2447268a02b 100644
--- a/src/models/room.js
+++ b/src/models/room.js
@@ -204,10 +204,12 @@ Room.prototype.getLiveTimeline = function() {
*
This is used when /sync returns a 'limited' timeline. * * @param {string=} backPaginationToken token for back-paginating the new timeline + * @param {boolean=} flush True to remove all events in all timelines. If false, only + * the live timeline is reset. */ -Room.prototype.resetLiveTimeline = function(backPaginationToken) { +Room.prototype.resetLiveTimeline = function(backPaginationToken, flush) { for (let i = 0; i < this._timelineSets.length; i++) { - this._timelineSets[i].resetLiveTimeline(backPaginationToken); + this._timelineSets[i].resetLiveTimeline(backPaginationToken, flush); } this._fixUpLegacyTimelineFields(); diff --git a/src/sync.js b/src/sync.js index 8eef2fbf860..c9573a418c8 100644 --- a/src/sync.js +++ b/src/sync.js @@ -63,6 +63,11 @@ function debuglog() { * @param {SyncAccumulator=} opts.syncAccumulator An accumulator which will be * kept up-to-date. If one is supplied, the response to getJSON() will be used * initially. + * @param {Function=} opts.canResetEntireTimeline A function which is called + * with a room ID and returns a boolean. It should return 'true' if the SDK can + * SAFELY remove events from this room. It may not be safe to remove events if + * there are other references to the timelines for this room. + * Default: returns false. */ function SyncApi(client, opts) { this.client = client; @@ -73,6 +78,11 @@ function SyncApi(client, opts) { opts.resolveInvitesToProfiles = opts.resolveInvitesToProfiles || false; opts.pollTimeout = opts.pollTimeout || (30 * 1000); opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological"; + if (!opts.canResetEntireTimeline) { + opts.canResetEntireTimeline = function(roomId) { + return false; + }; + } this.opts = opts; this._peekRoomId = null; this._currentSyncRequest = null; @@ -848,7 +858,10 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) { // timeline. room.currentState.paginationToken = syncToken; self._deregisterStateListeners(room); - room.resetLiveTimeline(joinObj.timeline.prev_batch); + room.resetLiveTimeline( + joinObj.timeline.prev_batch, + self.opts.canResetEntireTimeline(room.roomId), + ); // We have to assume any gap in any timeline is // reason to stop incrementally tracking notifications and