diff --git a/lib/client.js b/lib/client.js index 371d3c8de..20d9e9581 100644 --- a/lib/client.js +++ b/lib/client.js @@ -2117,8 +2117,30 @@ MatrixClient.prototype.search = function(opts, callback) { * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixClient.prototype.syncLeftRooms = function() { + // Guard against multiple calls whilst ongoing and multiple calls post success + if (this._syncedLeftRooms) { + console.log("Already synced left rooms"); + return q([]); // don't call syncRooms again if it succeeded. + } + if (this._syncLeftRoomsPromise) { + console.log("Returning ongoing request promise"); + return this._syncLeftRoomsPromise; // return the ongoing request + } + console.log("Making sync left rooms request"); + var self = this; var syncApi = new SyncApi(this); - return syncApi.syncLeftRooms(); + this._syncLeftRoomsPromise = syncApi.syncLeftRooms(); + + // cleanup locks + this._syncLeftRoomsPromise.then(function(res) { + console.log("Marking success of sync left room request"); + self._syncedLeftRooms = true; // flip the bit on success + }).finally(function() { + console.log("Cleaning up request state"); + self._syncLeftRoomsPromise = null; // cleanup ongoing request state + }); + + return this._syncLeftRoomsPromise; }; diff --git a/lib/sync.js b/lib/sync.js index c2fa37a32..8c451f880 100644 --- a/lib/sync.js +++ b/lib/sync.js @@ -21,12 +21,14 @@ var Filter = require("./filter"); // to determine the max time we're willing to wait. var BUFFER_PERIOD_MS = 20 * 1000; -function getFilterName(userId) { +function getFilterName(userId, suffix) { // scope this on the user ID because people may login on many accounts // and they all need to be stored! - return "FILTER_SYNC_" + userId; + return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : ""); } + + /** * Internal class - unstable. * Construct an entity which is able to sync with a homeserver. @@ -79,8 +81,63 @@ SyncApi.prototype.createRoom = function(roomId) { * @return {Promise} Resolved when they've been added to the store. */ SyncApi.prototype.syncLeftRooms = function() { - // TODO - return null; + var client = this.client; + var self = this; + + // grab a filter with limit=0 and include_leave=true + var filter = new Filter(this.client.credentials.userId); + filter.setTimelineLimit(1); + filter.setIncludeLeaveRooms(true); + + var localTimeoutMs = this.opts.pollTimeout + BUFFER_PERIOD_MS; + var qps = { + timeout: 1 // don't want to block since this is a single isolated req + }; + + return this._getOrCreateFilter( + getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter + ).then(function(filterId) { + qps.filter = filterId; + return client._http.authedRequestWithPrefix( + undefined, "GET", "/sync", qps, undefined, httpApi.PREFIX_V2_ALPHA, + localTimeoutMs + ); + }).then(function(data) { + var leaveRooms = []; + if (data.rooms && data.rooms.leave) { + leaveRooms = self._mapSyncResponseToRoomArray(data.rooms.leave); + } + var rooms = []; + leaveRooms.forEach(function(leaveObj) { + var room = leaveObj.room; + rooms.push(room); + if (!leaveObj.isBrandNewRoom) { + // the intention behind syncLeftRooms is to add in rooms which were + // *omitted* from the initial /sync. Rooms the user were joined to + // but then left whilst the app is running will appear in this list + // and we do not want to bother with them since they will have the + // current state already (and may get dupe messages if we add + // yet more timeline events!), so skip them. + // NB: When we persist rooms to localStorage this will be more + // complicated... + return; + } + leaveObj.timeline = leaveObj.timeline || {}; + var timelineEvents = self._mapSyncEventsFormat(leaveObj.timeline, room); + var stateEvents = self._mapSyncEventsFormat(leaveObj.state, room); + var paginationToken = ( + leaveObj.timeline.limited ? leaveObj.timeline.prev_batch : null + ); + self._processRoomEvents( + room, stateEvents, timelineEvents, paginationToken + ); + room.recalculate(client.credentials.userId); + client.store.storeRoom(room); + client.emit("Room", room); + }); + client._syncedLeftRooms = true; + return rooms; + }); }; /** @@ -112,27 +169,14 @@ SyncApi.prototype.sync = function() { attempt = attempt || 0; attempt += 1; - - // Get or create filter - var filterId = client.store.getFilterIdByName( - getFilterName(client.credentials.userId) - ); - if (filterId) { - // super, just use that. - console.log("Using existing filter ID %s", filterId); - self._sync({ filterId: filterId }); - return; - } - - // create a filter var filter = new Filter(client.credentials.userId); filter.setTimelineLimit(self.opts.initialSyncLimit); - client.createFilter(filter.getDefinition()).done(function(filter) { - client.store.setFilterIdByName( - getFilterName(client.credentials.userId), filter.filterId - ); - console.log("Created filter ", filter.filterId); - self._sync({ filterId: filter.filterId }); // Now start the /sync loop + + self._getOrCreateFilter( + getFilterName(client.credentials.userId), filter + ).done(function(filterId) { + console.log("Using existing filter ID %s", filterId); + self._sync({ filterId: filterId }); }, retryHandler(attempt, getFilter)); } @@ -348,6 +392,27 @@ SyncApi.prototype._sync = function(syncOptions, attempt) { }); }; +/** + * @param {string} filterName + * @param {Filter} filter + * @return {Promise} Filter ID + */ +SyncApi.prototype._getOrCreateFilter = function(filterName, filter) { + var client = this.client; + var filterId = client.store.getFilterIdByName(filterName); + if (filterId) { + // super, just use that. + console.log("Using existing filter ID %s", filterId); + return q(filterId); + } + + // create a filter + return client.createFilter(filter.getDefinition()).then(function(createdFilter) { + client.store.setFilterIdByName(filterName, createdFilter.filterId); + return createdFilter.filterId; + }); +}; + /** * @param {Object} obj * @return {Object[]}