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);