diff --git a/lib/client.js b/lib/client.js index a5394e86a..cc2e5f321 100644 --- a/lib/client.js +++ b/lib/client.js @@ -151,7 +151,7 @@ function MatrixClient(opts) { setupCallEventHandler(this); this._supportsVoip = true; } - + this._syncState = null; } utils.inherits(MatrixClient, EventEmitter); @@ -179,6 +179,15 @@ MatrixClient.prototype.supportsVoip = function() { return this._supportsVoip; }; +/** + * Get the current sync state. + * @return {?string} the sync state, which may be null. + * @see module:client~MatrixClient#event:"sync" + */ +MatrixClient.prototype.getSyncState = function() { + return this._syncState; +}; + /** * Is end-to-end crypto enabled for this client. * @return {boolean} True if end-to-end is enabled. @@ -1979,16 +1988,17 @@ function doInitialSync(client, historyLen, includeArchived, attempt) { } client.clientRunning = true; - client.emit("syncComplete"); + updateSyncState(client, "PREPARED"); + // assume success until we fail which may be 30+ secs + updateSyncState(client, "SYNCING"); _pollForEvents(client); }, function(err) { console.error("/initialSync error (%s attempts): %s", attempt, err); - client.emit("syncError", err); + updateSyncState(client, "ERROR", { error: err }); attempt += 1; setTimeout(function() { doInitialSync(client, historyLen, includeArchived, attempt); - }, Math.pow(2, Math.min(attempt, 7)) * 1000); // max 2^7 secs = 2.1 mins - // TODO: Retries. + }, retryTimeMsForAttempt(attempt)); }); } @@ -2040,20 +2050,34 @@ MatrixClient.prototype.startClient = function(opts) { // periodically poll for turn servers if we support voip checkTurnServers(this); - var self = this; - this.pushRules().done(function(result) { - self.pushRules = result; - doInitialSync(self, opts.initialSyncLimit, opts.includeArchivedRooms); - }, function(err) { - self.emit("syncError", err); - }); + prepareForSync(this); }; +function prepareForSync(client, attempt) { + attempt = attempt || 1; + client.pushRules().done(function(result) { + client.pushRules = result; + doInitialSync( + client, + client._config.initialSyncLimit, + client._config.includeArchivedRooms + ); + }, function(err) { + updateSyncState(client, "ERROR", { error: err }); + attempt += 1; + setTimeout(function() { + prepareForSync(client, attempt); + }, retryTimeMsForAttempt(attempt)); + }); +} + /** * This is an internal method. * @param {MatrixClient} client + * @param {Number} attempt The attempt number */ -function _pollForEvents(client) { +function _pollForEvents(client, attempt) { + attempt = attempt || 1; var self = client; if (!client.clientRunning) { return; @@ -2075,6 +2099,11 @@ function _pollForEvents(client) { else { clearTimeout(timeoutObj); } + + if (self._syncState !== "SYNCING") { + updateSyncState(self, "SYNCING"); + } + try { var events = []; if (data) { @@ -2170,12 +2199,12 @@ function _pollForEvents(client) { else { clearTimeout(timeoutObj); } - self.emit("syncError", err); - // retry every few seconds - // FIXME: this should be exponential backoff with an option to nudge + + updateSyncState(self, "ERROR", { error: err }); + attempt += 1; setTimeout(function() { - _pollForEvents(self); - }, 2000); + _pollForEvents(self, attempt); + }, retryTimeMsForAttempt(attempt)); }); } @@ -2455,6 +2484,12 @@ function setupCallEventHandler(client) { }); } +function updateSyncState(client, newState, data) { + var old = client._syncState; + client._syncState = newState; + client.emit("sync", client._syncState, old, data); +} + function checkTurnServers(client) { if (!client._supportsVoip) { return; @@ -2511,6 +2546,12 @@ function createNewRoom(client, roomId) { return room; } +function retryTimeMsForAttempt(attempt) { + // 2,4,8,16,32,64,128,128,128,... seconds + // max 2^7 secs = 2.1 mins + return Math.pow(2, Math.min(attempt, 7)) * 1000; +} + function _reject(callback, defer, err) { if (callback) { callback(err); @@ -2604,18 +2645,20 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * ready for methods to be called on it. This will be immediately followed by * a state of SYNCING. This is the equivalent of "syncComplete" in the * previous API. - *
  • SYNCING : The client is currently polling for new events from the server.
  • + *
  • SYNCING : The client is currently polling for new events from the server. + * The client may fire this before or after processing latest events from a sync.
  • *
  • ERROR : The client has had a problem syncing with the server. If this is * called before PREPARED then there was a problem performing the initial * sync. If this is called after PREPARED then there was a problem polling - * the server for updates. This is the equivalent of "syncError" in the previous + * the server for updates. This may be called multiple times even if the state is + * already ERROR. This is the equivalent of "syncError" in the previous * API.
  • * * State transition diagram: *
      *              +----->PREPARED -------> SYNCING <--+
      *              |        ^                  |       |
    - *   null ------+        |  +----------------+      |
    + *   null ------+        |  +---------------+       |
      *              |        |  V                       |
      *              +------->ERROR ---------------------+
      *
    @@ -2634,8 +2677,10 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
      * live update.
      * 
  • ERROR -> SYNCING : Occurs when the client has performed a * live update after having previously failed. + *
  • ERROR -> ERROR : Occurs when the client has failed to sync + * for a second time or more.
  • * - * + * * @event module:client~MatrixClient#"sync" * @param {string} state An enum representing the syncing state. One of "PREPARED", * "SYNCING", "ERROR". diff --git a/spec/integ/matrix-client-room-timeline.spec.js b/spec/integ/matrix-client-room-timeline.spec.js index d56658f95..89bdd04ac 100644 --- a/spec/integ/matrix-client-room-timeline.spec.js +++ b/spec/integ/matrix-client-room-timeline.spec.js @@ -82,7 +82,8 @@ describe("MatrixClient room timelines", function() { it("should be added immediately after calling MatrixClient.sendEvent " + "with EventStatus.SENDING and the right event.sender", function(done) { - client.on("syncComplete", function() { + client.on("sync", function(state) { + if (state !== "PREPARED") { return; } var room = client.getRoom(roomId); expect(room.timeline.length).toEqual(1); @@ -116,7 +117,8 @@ describe("MatrixClient room timelines", function() { ]; eventData.chunk[0].event_id = eventId; - client.on("syncComplete", function() { + client.on("sync", function(state) { + if (state !== "PREPARED") { return; } var room = client.getRoom(roomId); client.sendTextMessage(roomId, "I am a fish", "txn1").done( function() { @@ -144,7 +146,8 @@ describe("MatrixClient room timelines", function() { ]; eventData.chunk[0].event_id = eventId; - client.on("syncComplete", function() { + client.on("sync", function(state) { + if (state !== "PREPARED") { return; } var room = client.getRoom(roomId); var promise = client.sendTextMessage(roomId, "I am a fish", "txn1"); httpBackend.flush("/events", 1).done(function() { @@ -180,7 +183,8 @@ describe("MatrixClient room timelines", function() { it("should set Room.oldState.paginationToken to null at the start" + " of the timeline.", function(done) { - client.on("syncComplete", function() { + client.on("sync", function(state) { + if (state !== "PREPARED") { return; } var room = client.getRoom(roomId); expect(room.timeline.length).toEqual(1); @@ -219,7 +223,8 @@ describe("MatrixClient room timelines", function() { }) ]; - client.on("syncComplete", function() { + client.on("sync", function(state) { + if (state !== "PREPARED") { return; } var room = client.getRoom(roomId); expect(room.timeline.length).toEqual(1); @@ -249,7 +254,8 @@ describe("MatrixClient room timelines", function() { }) ]; - client.on("syncComplete", function() { + client.on("sync", function(state) { + if (state !== "PREPARED") { return; } var room = client.getRoom(roomId); expect(room.timeline.length).toEqual(1); @@ -274,7 +280,8 @@ describe("MatrixClient room timelines", function() { }) ]; - client.on("syncComplete", function() { + client.on("sync", function(state) { + if (state !== "PREPARED") { return; } var room = client.getRoom(roomId); expect(room.oldState.paginationToken).toBeDefined(); @@ -297,7 +304,8 @@ describe("MatrixClient room timelines", function() { utils.mkMessage({user: userId, room: roomId}), utils.mkMessage({user: userId, room: roomId}) ]; - client.on("syncComplete", function() { + client.on("sync", function(state) { + if (state !== "PREPARED") { return; } var room = client.getRoom(roomId); var index = 0; @@ -331,7 +339,8 @@ describe("MatrixClient room timelines", function() { }), utils.mkMessage({user: userId, room: roomId}) ]; - client.on("syncComplete", function() { + client.on("sync", function(state) { + if (state !== "PREPARED") { return; } var room = client.getRoom(roomId); httpBackend.flush("/events", 1).done(function() { var preNameEvent = room.timeline[room.timeline.length - 3]; @@ -352,7 +361,8 @@ describe("MatrixClient room timelines", function() { } }) ]; - client.on("syncComplete", function() { + client.on("sync", function(state) { + if (state !== "PREPARED") { return; } var room = client.getRoom(roomId); var nameEmitCount = 0; client.on("Room.name", function(rm) { @@ -392,7 +402,8 @@ describe("MatrixClient room timelines", function() { user: userC, room: roomId, mship: "invite", skey: userD }) ]; - client.on("syncComplete", function() { + client.on("sync", function(state) { + if (state !== "PREPARED") { return; } var room = client.getRoom(roomId); httpBackend.flush("/events", 1).done(function() { expect(room.currentState.getMembers().length).toEqual(4);