Implement the new sync state API

Also have retry schemes for the rest of the syncing ops (/events, /pushrules)
This commit is contained in:
Kegan Dougal
2015-11-03 16:44:19 +00:00
parent 27ce0970c5
commit 4b93d801ae
2 changed files with 89 additions and 33 deletions
+67 -22
View File
@@ -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. <i>This is the equivalent of "syncComplete" in the
* previous API.</i></li>
* <li>SYNCING : The client is currently polling for new events from the server.</li>
* <li>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.</li>
* <li>ERROR : The client has had a problem syncing with the server. If this is
* called <i>before</i> PREPARED then there was a problem performing the initial
* sync. If this is called <i>after</i> PREPARED then there was a problem polling
* the server for updates. <i>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. <i>This is the equivalent of "syncError" in the previous
* API.</i></li>
* </ul>
* State transition diagram:
* <pre>
* +----->PREPARED -------> SYNCING <--+
* | ^ | |
* null ------+ | +----------------+ |
* null ------+ | +---------------+ |
* | | V |
* +------->ERROR ---------------------+
*
@@ -2634,8 +2677,10 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
* live update.
* <li><code>ERROR -> SYNCING</code> : Occurs when the client has performed a
* live update after having previously failed.
* <li><code>ERROR -> ERROR</code> : Occurs when the client has failed to sync
* for a second time or more.</li>
* </ul>
*
*
* @event module:client~MatrixClient#"sync"
* @param {string} state An enum representing the syncing state. One of "PREPARED",
* "SYNCING", "ERROR".
+22 -11
View File
@@ -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);