diff --git a/lib/client.js b/lib/client.js index 1ec64be36..5091a5828 100644 --- a/lib/client.js +++ b/lib/client.js @@ -13,12 +13,11 @@ var httpApi = require("./http-api"); var MatrixEvent = require("./models/event").MatrixEvent; var EventStatus = require("./models/event").EventStatus; var StubStore = require("./store/stub"); -var Room = require("./models/room"); -var User = require("./models/user"); var webRtcCall = require("./webrtc/call"); var utils = require("./utils"); var contentRepo = require("./content-repo"); var Filter = require("./filter"); +var SyncApi = require("./sync"); var SCROLLBACK_DELAY_MS = 3000; var CRYPTO_ENABLED = false; @@ -32,9 +31,6 @@ try { // Olm not installed. } -// TODO: -// Internal: rate limiting - var OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2"; /** @@ -142,13 +138,9 @@ function MatrixClient(opts) { userId: (opts.userId || null) }; this._http = new httpApi.MatrixHttpApi(httpOpts); - this._syncingRooms = { - // room_id: Promise - }; this.callList = { // callId: MatrixCall }; - this._config = {}; // see startClient() // try constructing a MatrixCall to see if we are running in an environment // which has WebRTC. If we are, listen for and handle m.call.* events. @@ -622,9 +614,11 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) { this._http.authedRequest(undefined, "POST", path, undefined, {}).then( function(res) { var roomId = res.room_id; - var room = createNewRoom(self, roomId); + var syncApi = new SyncApi(self); + var room = syncApi.createRoom(roomId); if (opts.syncRoom) { - return _syncRoom(self, room); + // v2 will do this for us + // return syncApi.syncRoom(room); } return q(room); }, function(err) { @@ -2171,142 +2165,6 @@ MatrixClient.prototype.isLoggedIn = function() { // reconnect after connectivity outages -/** - * This is an internal method. - * @param {MatrixClient} client - * @param {integer} historyLen - * @param {integer} includeArchived - * @param {integer} attempt - */ -function doInitialSync(client, historyLen, includeArchived, attempt) { - attempt = attempt || 1; - var qps = { limit: historyLen }; - if (includeArchived) { - qps.archived = true; - } - if (client._guestRooms && client._isGuest) { - console.log(client._guestRooms); - qps.room_id = JSON.stringify(client._guestRooms); - } - client._http.authedRequest( - undefined, "GET", "/initialSync", qps - ).done(function(data) { - var i, j; - // intercept the results and put them into our store - if (!(client.store instanceof StubStore)) { - utils.forEach( - utils.map(data.presence, _PojoToMatrixEventMapper(client)), - function(e) { - var user = createNewUser(client, e.getContent().user_id); - user.setPresenceEvent(e); - client.store.storeUser(user); - }); - - // group receipts by room ID. - var receiptsByRoom = {}; - data.receipts = data.receipts || []; - utils.forEach(data.receipts.map(_PojoToMatrixEventMapper(client)), - function(receiptEvent) { - if (!receiptsByRoom[receiptEvent.getRoomId()]) { - receiptsByRoom[receiptEvent.getRoomId()] = []; - } - receiptsByRoom[receiptEvent.getRoomId()].push(receiptEvent); - } - ); - - for (i = 0; i < data.rooms.length; i++) { - var room = createNewRoom(client, data.rooms[i].room_id); - if (!data.rooms[i].state) { - data.rooms[i].state = []; - } - if (data.rooms[i].membership === "invite") { - var inviteEvent = data.rooms[i].invite; - if (!inviteEvent) { - // fallback for servers which don't serve the invite key yet - inviteEvent = { - event_id: "$fake_" + room.roomId, - content: { - membership: "invite" - }, - state_key: client.credentials.userId, - user_id: data.rooms[i].inviter, - room_id: room.roomId, - type: "m.room.member" - }; - } - data.rooms[i].state.push(inviteEvent); - } - - _processRoomEvents( - client, room, data.rooms[i].state, data.rooms[i].messages - ); - - var receipts = receiptsByRoom[room.roomId] || []; - for (j = 0; j < receipts.length; j++) { - room.addReceipt(receipts[j]); - } - - var privateUserData = data.rooms[i].account_data || []; - var privateUserDataEvents = - utils.map(privateUserData, _PojoToMatrixEventMapper(client)); - for (j = 0; j < privateUserDataEvents.length; j++) { - var event = privateUserDataEvents[j]; - if (event.getType() === "m.tag") { - room.addTags(event); - } - // XXX: unhandled private user data event - we should probably - // put it somewhere useful once the API has settled - } - - // cache the name/summary/etc prior to storage since we don't - // know how the store will serialise the Room. - room.recalculate(client.credentials.userId); - - client.store.storeRoom(room); - client.emit("Room", room); - } - } - - if (data) { - client.store.setSyncToken(data.end); - var events = []; - for (i = 0; i < data.presence.length; i++) { - events.push(new MatrixEvent(data.presence[i])); - } - for (i = 0; i < data.rooms.length; i++) { - if (data.rooms[i].state) { - for (j = 0; j < data.rooms[i].state.length; j++) { - events.push(new MatrixEvent(data.rooms[i].state[j])); - } - } - if (data.rooms[i].messages) { - for (j = 0; j < data.rooms[i].messages.chunk.length; j++) { - events.push( - new MatrixEvent(data.rooms[i].messages.chunk[j]) - ); - } - } - } - utils.forEach(events, function(e) { - client.emit("event", e); - }); - } - - client.clientRunning = true; - 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); - attempt += 1; - startSyncingRetryTimer(client, attempt, function() { - doInitialSync(client, historyLen, includeArchived, attempt); - }); - updateSyncState(client, "ERROR", { error: err }); - }); -} - /** * High level helper method to call initialSync, emit the resulting events, * and then start polling the eventStream for new events. To listen for these @@ -2333,6 +2191,7 @@ MatrixClient.prototype.startClient = function(opts) { // client is already running. return; } + this.clientRunning = true; // backwards compat for when 'opts' was 'historyLen'. if (typeof opts === "number") { opts = { @@ -2340,262 +2199,17 @@ MatrixClient.prototype.startClient = function(opts) { }; } - opts = opts || {}; - opts.initialSyncLimit = opts.initialSyncLimit || 8; - opts.includeArchivedRooms = opts.includeArchivedRooms || false; - opts.resolveInvitesToProfiles = opts.resolveInvitesToProfiles || false; - opts.pollTimeout = opts.pollTimeout || (30 * 1000); - opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological"; - this._config = opts; - if (CRYPTO_ENABLED && this.sessionStore !== null) { this.uploadKeys(5); } - if (this.store.getSyncToken()) { - // resume from where we left off. - _pollForEvents(this); - return; - } - // periodically poll for turn servers if we support voip checkTurnServers(this); - prepareForSync(this); + var syncApi = new SyncApi(this, opts); + syncApi.sync(); }; -function prepareForSync(client, attempt) { - if (client.isGuest()) { - // no push rules for guests - doInitialSync( - client, - client._config.initialSyncLimit, - client._config.includeArchivedRooms - ); - return; - } - - attempt = attempt || 1; - // we do push rules before syncing so when we gets events down we know immediately - // whether they are bing-worthy. - client.pushRules().done(function(result) { - client.pushRules = result; - doInitialSync( - client, - client._config.initialSyncLimit, - client._config.includeArchivedRooms - ); - }, function(err) { - attempt += 1; - startSyncingRetryTimer(client, attempt, function() { - prepareForSync(client, attempt); - }); - updateSyncState(client, "ERROR", { error: err }); - }); -} - -/** - * This is an internal method. - * @param {MatrixClient} client - * @param {Number} attempt The attempt number - */ -function _pollForEvents(client, attempt) { - attempt = attempt || 1; - var self = client; - if (!client.clientRunning) { - return; - } - var timeoutMs = client._config.pollTimeout; - if (attempt > 1) { - // we think the connection is dead. If it comes back up, we won't know - // about it till /events returns. If the timeout= is high, this could - // be a long time. Set it to 1 when doing retries. - timeoutMs = 1; - } - var discardResult = false; - var timeoutObj = setTimeout(function() { - discardResult = true; - console.error("/events request timed out."); - _pollForEvents(client); - }, timeoutMs + (20 * 1000)); // 20s buffer - - var queryParams = { - from: client.store.getSyncToken(), - timeout: timeoutMs - }; - if (client._guestRooms && client._isGuest) { - queryParams.room_id = client._guestRooms; - } - - client._http.authedRequest(undefined, "GET", "/events", queryParams).done( - function(data) { - if (discardResult) { - return; - } - else { - clearTimeout(timeoutObj); - } - - if (self._syncState !== "SYNCING") { - updateSyncState(self, "SYNCING"); - } - - try { - var events = []; - if (data) { - events = utils.map(data.chunk, _PojoToMatrixEventMapper(self)); - } - if (!(self.store instanceof StubStore)) { - var roomIdsWithNewInvites = {}; - // bucket events based on room. - var i = 0; - var roomIdToEvents = {}; - for (i = 0; i < events.length; i++) { - var roomId = events[i].getRoomId(); - // possible to have no room ID e.g. for presence events. - if (roomId) { - if (!roomIdToEvents[roomId]) { - roomIdToEvents[roomId] = []; - } - roomIdToEvents[roomId].push(events[i]); - if (events[i].getType() === "m.room.member" && - events[i].getContent().membership === "invite") { - roomIdsWithNewInvites[roomId] = true; - } - } - else if (events[i].getType() === "m.presence") { - var usr = self.store.getUser(events[i].getContent().user_id); - if (usr) { - usr.setPresenceEvent(events[i]); - } - else { - usr = createNewUser(self, events[i].getContent().user_id); - usr.setPresenceEvent(events[i]); - self.store.storeUser(usr); - } - } - } - - // add events to room - var roomIds = utils.keys(roomIdToEvents); - utils.forEach(roomIds, function(roomId) { - var room = self.store.getRoom(roomId); - var isBrandNewRoom = false; - if (!room) { - room = createNewRoom(self, roomId); - isBrandNewRoom = true; - } - - var wasJoined = room.hasMembershipState( - self.credentials.userId, "join" - ); - - room.addEvents(roomIdToEvents[roomId], "replace"); - room.recalculate(self.credentials.userId); - - // store the Room for things like invite events so developers - // can update the UI - if (isBrandNewRoom) { - self.store.storeRoom(room); - self.emit("Room", room); - } - - var justJoined = room.hasMembershipState( - self.credentials.userId, "join" - ); - - if (!wasJoined && justJoined) { - // we've just transitioned into a join state for this room, - // so sync state. - _syncRoom(self, room); - } - }); - - Object.keys(roomIdsWithNewInvites).forEach(function(inviteRoomId) { - _resolveInvites(self, self.store.getRoom(inviteRoomId)); - }); - } - if (data) { - self.store.setSyncToken(data.end); - utils.forEach(events, function(e) { - self.emit("event", e); - }); - } - } - catch (e) { - console.error("Event stream error:"); - console.error(e); - } - _pollForEvents(self); - }, function(err) { - console.error("/events error: %s", JSON.stringify(err)); - if (discardResult) { - return; - } - else { - clearTimeout(timeoutObj); - } - - attempt += 1; - startSyncingRetryTimer(self, attempt, function() { - _pollForEvents(self, attempt); - }); - updateSyncState(self, "ERROR", { error: err }); - }); -} - -function _syncRoom(client, room) { - if (client._syncingRooms[room.roomId]) { - return client._syncingRooms[room.roomId]; - } - var defer = q.defer(); - client._syncingRooms[room.roomId] = defer.promise; - client.roomInitialSync(room.roomId, client._config.initialSyncLimit).done( - function(res) { - room.timeline = []; // blow away any previous messages. - _processRoomEvents(client, room, res.state, res.messages); - room.recalculate(client.credentials.userId); - client.store.storeRoom(room); - client.emit("Room", room); - defer.resolve(room); - client._syncingRooms[room.roomId] = undefined; - }, function(err) { - defer.reject(err); - client._syncingRooms[room.roomId] = undefined; - }); - return defer.promise; -} - -function _processRoomEvents(client, room, stateEventList, messageChunk) { - // "old" and "current" state are the same initially; they - // start diverging if the user paginates. - // We must deep copy otherwise membership changes in old state - // will leak through to current state! - var oldStateEvents = utils.map( - utils.deepCopy(stateEventList), _PojoToMatrixEventMapper(client) - ); - var stateEvents = utils.map(stateEventList, _PojoToMatrixEventMapper(client)); - room.oldState.setStateEvents(oldStateEvents); - room.currentState.setStateEvents(stateEvents); - - _resolveInvites(client, room); - - // add events to the timeline *after* setting the state - // events so messages use the right display names. Initial sync - // returns messages in chronological order, so we need to reverse - // it to get most recent -> oldest. We need it in that order in - // order to diverge old/current state correctly. - room.addEventsToTimeline( - utils.map( - messageChunk ? messageChunk.chunk : [], - _PojoToMatrixEventMapper(client) - ).reverse(), true - ); - if (messageChunk) { - room.oldState.paginationToken = messageChunk.start; - } -} - /** * High level helper method to stop the client from polling and allow a * clean shutdown. @@ -2603,68 +2217,9 @@ function _processRoomEvents(client, room, stateEventList, messageChunk) { MatrixClient.prototype.stopClient = function() { this.clientRunning = false; // TODO: f.e. Room => self.store.storeRoom(room) ? + // TODO: Actually stop the SyncApi }; - -function reEmit(reEmitEntity, emittableEntity, eventNames) { - utils.forEach(eventNames, function(eventName) { - // setup a listener on the entity (the Room, User, etc) for this event - emittableEntity.on(eventName, function() { - // take the args from the listener and reuse them, adding the - // event name to the arg list so it works with .emit() - // Transformation Example: - // listener on "foo" => function(a,b) { ... } - // Re-emit on "thing" => thing.emit("foo", a, b) - var newArgs = [eventName]; - for (var i = 0; i < arguments.length; i++) { - newArgs.push(arguments[i]); - } - reEmitEntity.emit.apply(reEmitEntity, newArgs); - }); - }); -} - -function _resolveInvites(client, room) { - if (!room || !client._config.resolveInvitesToProfiles) { - return; - } - // For each invited room member we want to give them a displayname/avatar url - // if they have one (the m.room.member invites don't contain this). - room.getMembersWithMembership("invite").forEach(function(member) { - if (member._requestedProfileInfo) { - return; - } - member._requestedProfileInfo = true; - // try to get a cached copy first. - var user = client.getUser(member.userId); - var promise; - if (user) { - promise = q({ - avatar_url: user.avatarUrl, - displayname: user.displayName - }); - } - else { - promise = client.getProfileInfo(member.userId); - } - promise.done(function(info) { - // slightly naughty by doctoring the invite event but this means all - // the code paths remain the same between invite/join display name stuff - // which is a worthy trade-off for some minor pollution. - var inviteEvent = member.events.member; - if (inviteEvent.getContent().membership !== "invite") { - // between resolving and now they have since joined, so don't clobber - return; - } - inviteEvent.getContent().avatar_url = info.avatar_url; - inviteEvent.getContent().displayname = info.displayname; - member.setMembershipEvent(inviteEvent, room.currentState); // fire listeners - }, function(err) { - // OH WELL. - }); - }); -} - function setupCallEventHandler(client) { var candidatesByCall = { // callId: [Candidate] @@ -2820,20 +2375,6 @@ function setupCallEventHandler(client) { }); } -function startSyncingRetryTimer(client, attempt, fn) { - client._syncingRetry = {}; - client._syncingRetry.fn = fn; - client._syncingRetry.timeoutId = setTimeout(function() { - fn(); - }, retryTimeMsForAttempt(attempt)); -} - -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; @@ -2861,43 +2402,6 @@ function checkTurnServers(client) { }); } -function createNewUser(client, userId) { - var user = new User(userId); - reEmit(client, user, ["User.avatarUrl", "User.displayName", "User.presence"]); - return user; -} - -function createNewRoom(client, roomId) { - var room = new Room(roomId, { - pendingEventOrdering: client._config.pendingEventOrdering - }); - reEmit(client, room, ["Room.name", "Room.timeline", "Room.receipt", "Room.tags"]); - - // we need to also re-emit room state and room member events, so hook it up - // to the client now. We need to add a listener for RoomState.members in - // order to hook them correctly. (TODO: find a better way?) - reEmit(client, room.currentState, [ - "RoomState.events", "RoomState.members", "RoomState.newMember" - ]); - room.currentState.on("RoomState.newMember", function(event, state, member) { - member.user = client.getUser(member.userId); - reEmit( - client, member, - [ - "RoomMember.name", "RoomMember.typing", "RoomMember.powerLevel", - "RoomMember.membership" - ] - ); - }); - 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); @@ -2924,6 +2428,13 @@ function _PojoToMatrixEventMapper(client) { return mapper; } +/** + * @return {Function} + */ +MatrixClient.prototype.getEventMapper = function() { + return _PojoToMatrixEventMapper(this); +}; + // Identity Server Operations // ========================== @@ -2971,7 +2482,6 @@ module.exports.MatrixClient = MatrixClient; /** */ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; - // MatrixClient Event JSDocs /** @@ -2992,7 +2502,7 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * a state of SYNCING. This is the equivalent of "syncComplete" in the * previous API. *
null -> PREPARED : Occurs when the initial sync is completed
- * first time.
+ * first time. This involves setting up filters and obtaining push rules.
* null -> ERROR : Occurs when the initial sync failed first time.
* ERROR -> PREPARED : Occurs when the initial sync succeeds
* after previously failing.
@@ -3025,6 +2535,8 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
* live update after having previously failed.
* ERROR -> ERROR : Occurs when the client has failed to sync
* for a second time or more.SYNCING -> SYNCING : Occurs when the client has performed a live
+ * update. This is called after processing.{data: {Object},
* headers: {Object}, code: {Number}}.
* If onlyData is set, this will resolve to the data
@@ -249,10 +251,10 @@ module.exports.MatrixHttpApi.prototype = {
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
- authedRequest: function(callback, method, path, queryParams, data) {
+ authedRequest: function(callback, method, path, queryParams, data, localTimeoutMs) {
if (!queryParams) { queryParams = {}; }
queryParams.access_token = this.opts.accessToken;
- return this.request(callback, method, path, queryParams, data);
+ return this.request(callback, method, path, queryParams, data, localTimeoutMs);
},
/**
@@ -265,6 +267,8 @@ module.exports.MatrixHttpApi.prototype = {
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
+ * @param {Number=} localTimeoutMs The maximum amount of time to wait before
+ * timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to {data: {Object},
* headers: {Object}, code: {Number}}.
* If onlyData is set, this will resolve to the data
@@ -272,9 +276,9 @@ module.exports.MatrixHttpApi.prototype = {
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
- request: function(callback, method, path, queryParams, data) {
+ request: function(callback, method, path, queryParams, data, localTimeoutMs) {
return this.requestWithPrefix(
- callback, method, path, queryParams, data, this.opts.prefix
+ callback, method, path, queryParams, data, this.opts.prefix, localTimeoutMs
);
},
@@ -292,6 +296,8 @@ module.exports.MatrixHttpApi.prototype = {
* @param {Object} data The HTTP JSON body.
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
+ * @param {Number=} localTimeoutMs The maximum amount of time to wait before
+ * timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to {data: {Object},
* headers: {Object}, code: {Number}}.
* If onlyData is set, this will resolve to the data
@@ -300,13 +306,15 @@ module.exports.MatrixHttpApi.prototype = {
* occurred. This includes network problems and Matrix-specific error JSON.
*/
authedRequestWithPrefix: function(callback, method, path, queryParams, data,
- prefix) {
+ prefix, localTimeoutMs) {
var fullUri = this.opts.baseUrl + prefix + path;
if (!queryParams) {
queryParams = {};
}
queryParams.access_token = this.opts.accessToken;
- return this._request(callback, method, fullUri, queryParams, data);
+ return this._request(
+ callback, method, fullUri, queryParams, data, localTimeoutMs
+ );
},
/**
@@ -323,6 +331,8 @@ module.exports.MatrixHttpApi.prototype = {
* @param {Object} data The HTTP JSON body.
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
+ * @param {Number=} localTimeoutMs The maximum amount of time to wait before
+ * timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to {data: {Object},
* headers: {Object}, code: {Number}}.
* If onlyData is set, this will resolve to the data
@@ -330,12 +340,15 @@ module.exports.MatrixHttpApi.prototype = {
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
- requestWithPrefix: function(callback, method, path, queryParams, data, prefix) {
+ requestWithPrefix: function(callback, method, path, queryParams, data, prefix,
+ localTimeoutMs) {
var fullUri = this.opts.baseUrl + prefix + path;
if (!queryParams) {
queryParams = {};
}
- return this._request(callback, method, fullUri, queryParams, data);
+ return this._request(
+ callback, method, fullUri, queryParams, data, localTimeoutMs
+ );
},
/**
@@ -357,12 +370,13 @@ module.exports.MatrixHttpApi.prototype = {
return this.opts.baseUrl + prefix + path + queryString;
},
- _request: function(callback, method, uri, queryParams, data) {
+ _request: function(callback, method, uri, queryParams, data, localTimeoutMs) {
if (callback !== undefined && !utils.isFunction(callback)) {
throw Error(
"Expected callback to be a function but got " + typeof callback
);
}
+ var self = this;
if (!queryParams) {
queryParams = {};
}
@@ -373,6 +387,20 @@ module.exports.MatrixHttpApi.prototype = {
}
}
var defer = q.defer();
+
+ var timeoutId;
+ var timedOut = false;
+ if (localTimeoutMs) {
+ timeoutId = setTimeout(function() {
+ timedOut = true;
+ defer.reject(new module.exports.MatrixError({
+ error: "Locally timed out waiting for a response",
+ errcode: "ORG_MATRIX_JSSDK_TIMEOUT",
+ timeout: localTimeoutMs
+ }));
+ }, localTimeoutMs);
+ }
+
try {
this.opts.request(
{
@@ -384,7 +412,16 @@ module.exports.MatrixHttpApi.prototype = {
json: true,
_matrix_opts: this.opts
},
- requestCallback(defer, callback, this.opts.onlyData)
+ function(err, response, body) {
+ if (localTimeoutMs) {
+ clearTimeout(timeoutId);
+ if (timedOut) {
+ return; // already rejected promise
+ }
+ }
+ var handlerFn = requestCallback(defer, callback, self.opts.onlyData);
+ handlerFn(err, response, body);
+ }
);
}
catch (ex) {
diff --git a/lib/matrix.js b/lib/matrix.js
index f150f7e76..a68350488 100644
--- a/lib/matrix.js
+++ b/lib/matrix.js
@@ -81,7 +81,9 @@ module.exports.createClient = function(opts) {
};
}
opts.request = opts.request || request;
- opts.store = opts.store || new module.exports.MatrixInMemoryStore();
+ opts.store = opts.store || new module.exports.MatrixInMemoryStore({
+ localStorage: global.localStorage
+ });
opts.scheduler = opts.scheduler || new module.exports.MatrixScheduler();
return new module.exports.MatrixClient(opts);
};
diff --git a/lib/models/event.js b/lib/models/event.js
index 18c2fe068..e8db3dbf2 100644
--- a/lib/models/event.js
+++ b/lib/models/event.js
@@ -63,7 +63,7 @@ module.exports.MatrixEvent.prototype = {
* @return {string} The user ID, e.g. @alice:matrix.org
*/
getSender: function() {
- return this.event.user_id;
+ return this.event.sender || this.event.user_id; // v2 / v1
},
/**
@@ -143,7 +143,7 @@ module.exports.MatrixEvent.prototype = {
* @return {Number} The age of this event in milliseconds.
*/
getAge: function() {
- return this.event.age;
+ return this.getUnsigned().age || this.event.age; // v2 / v1
},
/**
@@ -169,5 +169,9 @@ module.exports.MatrixEvent.prototype = {
*/
isEncrypted: function() {
return this.encrypted;
+ },
+
+ getUnsigned: function() {
+ return this.event.unsigned || {};
}
};
diff --git a/lib/models/room.js b/lib/models/room.js
index a2f8a1e0f..7bb9d6a33 100644
--- a/lib/models/room.js
+++ b/lib/models/room.js
@@ -79,6 +79,7 @@ function Room(roomId, opts) {
this.storageToken = opts.storageToken;
this._opts = opts;
this._redactions = [];
+ this._txnToEvent = {}; // Pending in-flight requests { string: MatrixEvent }
// receipts should clobber based on receipt_type and user_id pairs hence
// the form of this structure. This is sub-optimal for the exposed APIs
// which pass in an event ID and get back some receipts, so we also store
@@ -211,6 +212,23 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline) {
)
);
+ // FIXME: HORRIBLE ASSUMPTION THAT THIS PROP EXISTS
+ // Exists due to client.js:815 (MatrixClient.sendEvent)
+ // We should make txnId a first class citizen.
+ if (!toStartOfTimeline && events[i]._txnId) {
+ this._txnToEvent[events[i]._txnId] = events[i];
+ }
+ else if (!toStartOfTimeline && events[i].getUnsigned().transaction_id) {
+ var existingEvent = this._txnToEvent[events[i].getUnsigned().transaction_id];
+ if (existingEvent) {
+ // no longer pending
+ delete this._txnToEvent[events[i].getUnsigned().transaction_id];
+ // replace the event source
+ existingEvent.event = events[i].event;
+ continue;
+ }
+ }
+
setEventMetadata(events[i], stateContext, toStartOfTimeline);
// modify state
if (events[i].isState()) {
diff --git a/lib/pushprocessor.js b/lib/pushprocessor.js
index 8ea138afa..e7c71029b 100644
--- a/lib/pushprocessor.js
+++ b/lib/pushprocessor.js
@@ -195,7 +195,7 @@ function PushProcessor(client) {
};
var matchingRuleForEventWithRulesets = function(ev, rulesets) {
- if (!rulesets) { return null; }
+ if (!rulesets || !rulesets.device) { return null; }
if (ev.user_id == client.credentials.userId) { return null; }
var allDevNames = Object.keys(rulesets.device);
diff --git a/lib/store/memory.js b/lib/store/memory.js
index ae8257f03..b7af1bfd8 100644
--- a/lib/store/memory.js
+++ b/lib/store/memory.js
@@ -8,8 +8,13 @@
/**
* Construct a new in-memory data store for the Matrix Client.
* @constructor
+ * @param {Object=} opts Config options
+ * @param {LocalStorage} opts.localStorage The local storage instance to persist
+ * some forms of data such as tokens. Rooms will NOT be stored. See
+ * {@link WebStorageStore} to persist rooms.
*/
-module.exports.MatrixInMemoryStore = function MatrixInMemoryStore() {
+module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
+ opts = opts || {};
this.rooms = {
// roomId: Room
};
@@ -22,6 +27,7 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore() {
// filterId: Filter
// }
};
+ this.localStorage = opts.localStorage;
};
module.exports.MatrixInMemoryStore.prototype = {
@@ -139,6 +145,37 @@ module.exports.MatrixInMemoryStore.prototype = {
return null;
}
return this.filters[userId][filterId];
+ },
+
+ /**
+ * Retrieve a filter ID with the given name.
+ * @param {string} filterName The filter name.
+ * @return {?string} The filter ID or null.
+ */
+ getFilterIdByName: function(filterName) {
+ if (!this.localStorage) {
+ return null;
+ }
+ try {
+ return this.localStorage.getItem("mxjssdk_memory_filter_" + filterName);
+ }
+ catch (e) {}
+ return null;
+ },
+
+ /**
+ * Set a filter name to ID mapping.
+ * @param {string} filterName
+ * @param {string} filterId
+ */
+ setFilterIdByName: function(filterName, filterId) {
+ if (!this.localStorage) {
+ return;
+ }
+ try {
+ this.localStorage.setItem("mxjssdk_memory_filter_" + filterName, filterId);
+ }
+ catch (e) {}
}
// TODO
diff --git a/lib/store/stub.js b/lib/store/stub.js
index a8f0aa9bc..4cdc1a93e 100644
--- a/lib/store/stub.js
+++ b/lib/store/stub.js
@@ -113,6 +113,24 @@ StubStore.prototype = {
*/
getFilter: function(userId, filterId) {
return null;
+ },
+
+ /**
+ * Retrieve a filter ID with the given name.
+ * @param {string} filterName The filter name.
+ * @return {?string} The filter ID or null.
+ */
+ getFilterIdByName: function(filterName) {
+ return null;
+ },
+
+ /**
+ * Set a filter name to ID mapping.
+ * @param {string} filterName
+ * @param {string} filterId
+ */
+ setFilterIdByName: function(filterName, filterId) {
+
}
// TODO
diff --git a/lib/sync.js b/lib/sync.js
new file mode 100644
index 000000000..c884bc6d4
--- /dev/null
+++ b/lib/sync.js
@@ -0,0 +1,514 @@
+"use strict";
+
+/*
+ * TODO:
+ * This class mainly serves to take all the syncing logic out of client.js and
+ * into a separate file. It's all very fluid, and this class gut wrenches a lot
+ * of MatrixClient props (e.g. _http). Given we want to support WebSockets as
+ * an alternative syncing API, we may want to have a proper syncing interface
+ * for HTTP and WS at some point.
+ */
+var q = require("q");
+var User = require("./models/user");
+var Room = require("./models/room");
+var utils = require("./utils");
+var httpApi = require("./http-api");
+var Filter = require("./filter");
+
+// /sync requests allow you to set a timeout= but the request may continue
+// beyond that and wedge forever, so we need to track how long we are willing
+// to keep open the connection. This constant is *ADDED* to the timeout= value
+// to determine the max time we're willing to wait.
+var BUFFER_PERIOD_MS = 20 * 1000;
+
+function getFilterName(userId) {
+ // scope this on the user ID because people may login on many accounts
+ // and they all need to be stored!
+ return "FILTER_SYNC_" + userId;
+}
+
+/**
+ * Internal class - unstable.
+ * Construct an entity which is able to sync with a homeserver.
+ * @constructor
+ * @param {MatrixClient} client The matrix client instance to use.
+ * @param {Object} opts Config options
+ */
+function SyncApi(client, opts) {
+ this.client = client;
+ opts = opts || {};
+ opts.initialSyncLimit = opts.initialSyncLimit || 8;
+ opts.includeArchivedRooms = opts.includeArchivedRooms || false;
+ opts.resolveInvitesToProfiles = opts.resolveInvitesToProfiles || false;
+ opts.pollTimeout = opts.pollTimeout || (30 * 1000);
+ opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
+ this.opts = opts;
+}
+
+/**
+ * @param {string} roomId
+ * @return {Room}
+ */
+SyncApi.prototype.createRoom = function(roomId) {
+ var client = this.client;
+ var room = new Room(roomId, {
+ pendingEventOrdering: this.opts.pendingEventOrdering
+ });
+ reEmit(client, room, ["Room.name", "Room.timeline", "Room.receipt", "Room.tags"]);
+
+ // we need to also re-emit room state and room member events, so hook it up
+ // to the client now. We need to add a listener for RoomState.members in
+ // order to hook them correctly. (TODO: find a better way?)
+ reEmit(client, room.currentState, [
+ "RoomState.events", "RoomState.members", "RoomState.newMember"
+ ]);
+ room.currentState.on("RoomState.newMember", function(event, state, member) {
+ member.user = client.getUser(member.userId);
+ reEmit(
+ client, member,
+ [
+ "RoomMember.name", "RoomMember.typing", "RoomMember.powerLevel",
+ "RoomMember.membership"
+ ]
+ );
+ });
+ return room;
+};
+
+/**
+ * Main entry point
+ */
+SyncApi.prototype.sync = function() {
+ console.log("SyncApi.sync");
+ var client = this.client;
+ var self = this;
+
+ // We need to do one-off checks before we can begin the /sync loop.
+ // These are:
+ // 1) We need to get push rules so we can check if events should bing as we get
+ // them from /sync.
+ // 2) We need to get/create a filter which we can use for /sync.
+
+ function getPushRules(attempt) {
+ attempt = attempt || 0;
+ attempt += 1;
+
+ client.pushRules().done(function(result) {
+ console.log("Got push rules");
+ client.pushRules = result;
+ getFilter(); // Now get the filter
+ }, retryHandler(attempt, getPushRules));
+ }
+
+ function getFilter(attempt) {
+ 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
+ }, retryHandler(attempt, getFilter));
+ }
+
+ // sets the sync state to error and waits a bit before re-invoking the function.
+ function retryHandler(attempt, fnToRun) {
+ return function(err) {
+ startSyncingRetryTimer(client, attempt, function() {
+ fnToRun(attempt);
+ });
+ updateSyncState(client, "ERROR", { error: err });
+ };
+ }
+
+ if (client.isGuest()) {
+ // no push rules for guests
+ getFilter();
+ }
+ else {
+ getPushRules();
+ }
+};
+
+/**
+ * Invoke me to do /sync calls
+ * @param {Object} syncOptions
+ * @param {string} syncOptions.filterId
+ * @param {boolean} syncOptions.hasSyncedBefore
+ * @param {Number=} attempt
+ */
+SyncApi.prototype._sync = function(syncOptions, attempt) {
+ var client = this.client;
+ var self = this;
+ attempt = attempt || 1;
+
+ // TODO include archived rooms flag.
+
+ var qps = {
+ filter: syncOptions.filterId,
+ timeout: this.opts.pollTimeout,
+ since: client.store.getSyncToken() || undefined // do not send 'null'
+ };
+
+ if (attempt > 1) {
+ // we think the connection is dead. If it comes back up, we won't know
+ // about it till /sync returns. If the timeout= is high, this could
+ // be a long time. Set it to 1 when doing retries.
+ qps.timeout = 1;
+ }
+
+ if (client._guestRooms && client._isGuest) {
+ qps.room_id = client._guestRooms;
+ }
+
+ client._http.authedRequestWithPrefix(
+ undefined, "GET", "/sync", qps, undefined, httpApi.PREFIX_V2_ALPHA,
+ this.opts.pollTimeout + BUFFER_PERIOD_MS // normal timeout= plus buffer time
+ ).done(function(data) {
+ // data looks like:
+ // {
+ // next_batch: $token,
+ // presence: { events: [] },
+ // rooms: {
+ // invite: {
+ // $roomid: {
+ // invite_state: { events: [] }
+ // }
+ // },
+ // join: {
+ // $roomid: {
+ // state: { events: [] },
+ // timeline: { events: [], prev_batch: $token, limited: true },
+ // ephemeral: { events: [] },
+ // account_data: { events: [] }
+ // }
+ // },
+ // leave: {
+ // $roomid: {
+ // state: { events: [] },
+ // timeline: { events: [], prev_batch: $token }
+ // }
+ // }
+ // }
+ // }
+
+ // set the sync token NOW *before* processing the events. We do this so
+ // if something barfs on an event we can skip it rather than constantly
+ // polling with the same token.
+ client.store.setSyncToken(data.next_batch);
+
+ // TODO-arch:
+ // - Each event we pass through needs to be emitted via 'event', can we
+ // do this in one place?
+ // - The isBrandNewRoom boilerplate is boilerplatey.
+
+ try {
+ // handle presence events (User objects)
+ if (data.presence && utils.isArray(data.presence.events)) {
+ data.presence.events.map(client.getEventMapper()).forEach(
+ function(presenceEvent) {
+ var user = client.store.getUser(presenceEvent.getSender());
+ if (user) {
+ user.setPresenceEvent(presenceEvent);
+ }
+ else {
+ user = createNewUser(client, presenceEvent.getSender());
+ user.setPresenceEvent(presenceEvent);
+ client.store.storeUser(user);
+ }
+ client.emit("event", presenceEvent);
+ });
+ }
+
+ // the returned json structure is abit crap, so make it into a
+ // nicer form (array) after applying sanity to make sure we don't fail
+ // on missing keys (on the off chance)
+ var inviteRooms = [];
+ var joinRooms = [];
+ var leaveRooms = [];
+
+ if (data.rooms) {
+ if (data.rooms.invite) {
+ inviteRooms = self._mapSyncResponseToRoomArray(data.rooms.invite);
+ }
+ if (data.rooms.join) {
+ joinRooms = self._mapSyncResponseToRoomArray(data.rooms.join);
+ }
+ if (data.rooms.leave) {
+ leaveRooms = self._mapSyncResponseToRoomArray(data.rooms.leave);
+ }
+ }
+
+ // Handle invites
+ inviteRooms.forEach(function(inviteObj) {
+ var room = inviteObj.room;
+ var stateEvents = self._mapSyncEventsFormat(inviteObj.invite_state, room);
+ self._processRoomEvents(room, stateEvents);
+ if (inviteObj.isBrandNewRoom) {
+ room.recalculate(client.credentials.userId);
+ client.store.storeRoom(room);
+ client.emit("Room", room);
+ }
+ stateEvents.forEach(function(e) { client.emit("event", e); });
+ });
+
+ // Handle joins
+ joinRooms.forEach(function(joinObj) {
+ var room = joinObj.room;
+ var stateEvents = self._mapSyncEventsFormat(joinObj.state, room);
+ var timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room);
+ var ephemeralEvents = self._mapSyncEventsFormat(joinObj.ephemeral);
+ var accountDataEvents = self._mapSyncEventsFormat(joinObj.account_data);
+
+ joinObj.timeline = joinObj.timeline || {};
+
+ if (joinObj.timeline.limited) {
+ // nuke the timeline so we don't get holes
+ room.timeline = [];
+ }
+
+ // we want to set a new pagination token if this is the first time
+ // we've made this room or if we're nuking the timeline
+ var paginationToken = null;
+ if (joinObj.isBrandNewRoom || joinObj.timeline.limited) {
+ paginationToken = joinObj.timeline.prev_batch;
+ }
+
+ self._processRoomEvents(
+ room, stateEvents, timelineEvents, paginationToken
+ );
+ room.addEvents(ephemeralEvents);
+ room.addEvents(accountDataEvents);
+ room.recalculate(client.credentials.userId);
+ if (joinObj.isBrandNewRoom) {
+ client.store.storeRoom(room);
+ client.emit("Room", room);
+ }
+ stateEvents.forEach(function(e) { client.emit("event", e); });
+ timelineEvents.forEach(function(e) { client.emit("event", e); });
+ ephemeralEvents.forEach(function(e) { client.emit("event", e); });
+ accountDataEvents.forEach(function(e) { client.emit("event", e); });
+ });
+
+ // Ignore leave rooms for now (TODO: Honour includeArchived opt)
+ }
+ catch (e) {
+ console.error("Caught /sync error:");
+ console.error(e);
+ }
+
+ // emit synced events
+ if (!syncOptions.hasSyncedBefore) {
+ updateSyncState(client, "PREPARED");
+ syncOptions.hasSyncedBefore = true;
+ }
+
+ // keep emitting SYNCING -> SYNCING for clients who want to do bulk updates
+ updateSyncState(client, "SYNCING");
+
+ self._sync(syncOptions);
+ }, function(err) {
+ console.error("/sync error (%s attempts): %s", attempt, err);
+ console.error(err);
+ attempt += 1;
+ startSyncingRetryTimer(client, attempt, function() {
+ self._sync(syncOptions, attempt);
+ });
+ updateSyncState(client, "ERROR", { error: err });
+ });
+};
+
+/**
+ * @param {Object} obj
+ * @return {Object[]}
+ */
+SyncApi.prototype._mapSyncResponseToRoomArray = function(obj) {
+ // Maps { roomid: {stuff}, roomid: {stuff} }
+ // to
+ // [{stuff+Room+isBrandNewRoom}, {stuff+Room+isBrandNewRoom}]
+ var client = this.client;
+ var self = this;
+ return utils.keys(obj).map(function(roomId) {
+ var arrObj = obj[roomId];
+ var room = client.store.getRoom(roomId);
+ var isBrandNewRoom = false;
+ if (!room) {
+ room = self.createRoom(roomId);
+ isBrandNewRoom = true;
+ }
+ arrObj.room = room;
+ arrObj.isBrandNewRoom = isBrandNewRoom;
+ return arrObj;
+ });
+};
+
+/**
+ * @param {Object} obj
+ * @param {Room} room
+ * @return {MatrixEvent[]}
+ */
+SyncApi.prototype._mapSyncEventsFormat = function(obj, room) {
+ if (!obj || !utils.isArray(obj.events)) {
+ return [];
+ }
+ var mapper = this.client.getEventMapper();
+ return obj.events.map(function(e) {
+ if (room) {
+ e.room_id = room.roomId;
+ }
+ return mapper(e);
+ });
+};
+
+/**
+ * @param {Room} room
+ */
+SyncApi.prototype._resolveInvites = function(room) {
+ if (!room || !this.opts.resolveInvitesToProfiles) {
+ return;
+ }
+ var client = this.client;
+ // For each invited room member we want to give them a displayname/avatar url
+ // if they have one (the m.room.member invites don't contain this).
+ room.getMembersWithMembership("invite").forEach(function(member) {
+ if (member._requestedProfileInfo) {
+ return;
+ }
+ member._requestedProfileInfo = true;
+ // try to get a cached copy first.
+ var user = client.getUser(member.userId);
+ var promise;
+ if (user) {
+ promise = q({
+ avatar_url: user.avatarUrl,
+ displayname: user.displayName
+ });
+ }
+ else {
+ promise = client.getProfileInfo(member.userId);
+ }
+ promise.done(function(info) {
+ // slightly naughty by doctoring the invite event but this means all
+ // the code paths remain the same between invite/join display name stuff
+ // which is a worthy trade-off for some minor pollution.
+ var inviteEvent = member.events.member;
+ if (inviteEvent.getContent().membership !== "invite") {
+ // between resolving and now they have since joined, so don't clobber
+ return;
+ }
+ inviteEvent.getContent().avatar_url = info.avatar_url;
+ inviteEvent.getContent().displayname = info.displayname;
+ // fire listeners
+ member.setMembershipEvent(inviteEvent, room.currentState);
+ }, function(err) {
+ // OH WELL.
+ });
+ });
+};
+
+/**
+ * @param {Room} room
+ * @param {MatrixEvent[]} stateEventList A list of state events. This is the state
+ * at the *START* of the timeline list if it is supplied.
+ * @param {MatrixEvent[]=} timelineEventList A list of timeline events. Lower index
+ * is earlier in time. Higher index is later.
+ * @param {string=} paginationToken
+ */
+SyncApi.prototype._processRoomEvents = function(room, stateEventList,
+ timelineEventList, paginationToken) {
+ timelineEventList = timelineEventList || [];
+ var client = this.client;
+ // "old" and "current" state are the same initially; they
+ // start diverging if the user paginates.
+ // We must deep copy otherwise membership changes in old state
+ // will leak through to current state!
+ var oldStateEvents = utils.map(
+ utils.deepCopy(
+ stateEventList.map(function(mxEvent) { return mxEvent.event; })
+ ), client.getEventMapper()
+ );
+ var stateEvents = stateEventList;
+
+ // Set the pagination token BEFORE adding events to the timeline: it's not
+ // unreasonable for clients to call scrollback() in response to Room.timeline
+ // events which addEventsToTimeline will emit-- we want to make sure they use
+ // the right token if and when they do.
+ if (paginationToken) {
+ room.oldState.paginationToken = paginationToken;
+ }
+
+ // set the state of the room to as it was before the timeline executes
+ room.oldState.setStateEvents(oldStateEvents);
+ room.currentState.setStateEvents(stateEvents);
+
+ this._resolveInvites(room);
+
+ // execute the timeline events, this will begin to diverge the current state
+ // if the timeline has any state events in it.
+ room.addEventsToTimeline(timelineEventList);
+
+};
+
+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 startSyncingRetryTimer(client, attempt, fn) {
+ client._syncingRetry = {};
+ client._syncingRetry.fn = fn;
+ client._syncingRetry.timeoutId = setTimeout(function() {
+ fn();
+ }, retryTimeMsForAttempt(attempt));
+}
+
+function updateSyncState(client, newState, data) {
+ var old = client._syncState;
+ client._syncState = newState;
+ client.emit("sync", client._syncState, old, data);
+}
+
+function createNewUser(client, userId) {
+ var user = new User(userId);
+ reEmit(client, user, ["User.avatarUrl", "User.displayName", "User.presence"]);
+ return user;
+}
+
+function reEmit(reEmitEntity, emittableEntity, eventNames) {
+ utils.forEach(eventNames, function(eventName) {
+ // setup a listener on the entity (the Room, User, etc) for this event
+ emittableEntity.on(eventName, function() {
+ // take the args from the listener and reuse them, adding the
+ // event name to the arg list so it works with .emit()
+ // Transformation Example:
+ // listener on "foo" => function(a,b) { ... }
+ // Re-emit on "thing" => thing.emit("foo", a, b)
+ var newArgs = [eventName];
+ for (var i = 0; i < arguments.length; i++) {
+ newArgs.push(arguments[i]);
+ }
+ reEmitEntity.emit.apply(reEmitEntity, newArgs);
+ });
+ });
+}
+
+/** */
+module.exports = SyncApi;
diff --git a/lib/utils.js b/lib/utils.js
index 955903fff..f58a6533b 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -367,6 +367,74 @@ module.exports.runPolyfills = function() {
return A;
};
}
+
+ // Array.prototype.forEach
+ // ========================================================
+ // SOURCE:
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
+ // Production steps of ECMA-262, Edition 5, 15.4.4.18
+ // Reference: http://es5.github.io/#x15.4.4.18
+ if (!Array.prototype.forEach) {
+
+ Array.prototype.forEach = function(callback, thisArg) {
+
+ var T, k;
+
+ if (this === null || this === undefined) {
+ throw new TypeError(' this is null or not defined');
+ }
+
+ // 1. Let O be the result of calling ToObject passing the |this| value as the
+ // argument.
+ var O = Object(this);
+
+ // 2. Let lenValue be the result of calling the Get internal method of O with the
+ // argument "length".
+ // 3. Let len be ToUint32(lenValue).
+ var len = O.length >>> 0;
+
+ // 4. If IsCallable(callback) is false, throw a TypeError exception.
+ // See: http://es5.github.com/#x9.11
+ if (typeof callback !== "function") {
+ throw new TypeError(callback + ' is not a function');
+ }
+
+ // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
+ if (arguments.length > 1) {
+ T = thisArg;
+ }
+
+ // 6. Let k be 0
+ k = 0;
+
+ // 7. Repeat, while k < len
+ while (k < len) {
+
+ var kValue;
+
+ // a. Let Pk be ToString(k).
+ // This is implicit for LHS operands of the in operator
+ // b. Let kPresent be the result of calling the HasProperty internal
+ // method of O with
+ // argument Pk.
+ // This step can be combined with c
+ // c. If kPresent is true, then
+ if (k in O) {
+
+ // i. Let kValue be the result of calling the Get internal method of O with
+ // argument Pk
+ kValue = O[k];
+
+ // ii. Call the Call internal method of callback with T as the this value and
+ // argument list containing kValue, k, and O.
+ callback.call(T, kValue, k, O);
+ }
+ // d. Increase k by 1.
+ k++;
+ }
+ // 8. return undefined
+ };
+ }
};
/**
diff --git a/spec/integ/matrix-client-crypto.spec.js b/spec/integ/matrix-client-crypto.spec.js
index 75b559c36..d40658b5a 100644
--- a/spec/integ/matrix-client-crypto.spec.js
+++ b/spec/integ/matrix-client-crypto.spec.js
@@ -67,6 +67,7 @@ describe("MatrixClient crypto", function() {
});
httpBackend.when("GET", "/pushrules").respond(200, {});
+ httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
});
describe("Ali account setup", function() {
@@ -200,22 +201,26 @@ describe("MatrixClient crypto", function() {
});
function bobRecvMessage(done) {
- var initialSync = {
- end: "alpha",
- presence: [],
- rooms: []
+ var syncData = {
+ next_batch: "x",
+ rooms: {
+ join: {
+
+ }
+ }
};
- var events = {
- start: "alpha",
- end: "beta",
- chunk: [utils.mkEvent({
- type: "m.room.encrypted",
- room: roomId,
- content: aliMessage
- })]
+ syncData.rooms.join[roomId] = {
+ timeline: {
+ events: [
+ utils.mkEvent({
+ type: "m.room.encrypted",
+ room: roomId,
+ content: aliMessage
+ })
+ ]
+ }
};
- httpBackend.when("GET", "initialSync").respond(200, initialSync);
- httpBackend.when("GET", "events").respond(200, events);
+ httpBackend.when("GET", "/sync").respond(200, syncData);
bobClient.on("event", function(event) {
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toEqual({
diff --git a/spec/integ/matrix-client-event-emitter.spec.js b/spec/integ/matrix-client-event-emitter.spec.js
index a6c04f36f..e5191d97c 100644
--- a/spec/integ/matrix-client-event-emitter.spec.js
+++ b/spec/integ/matrix-client-event-emitter.spec.js
@@ -19,6 +19,7 @@ describe("MatrixClient events", function() {
accessToken: selfAccessToken
});
httpBackend.when("GET", "/pushrules").respond(200, {});
+ httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
});
afterEach(function() {
@@ -26,108 +27,114 @@ describe("MatrixClient events", function() {
});
describe("emissions", function() {
- var initialSync = {
- end: "s_5_3",
- presence: [{
- event_id: "$wefiuewh:bar",
- type: "m.presence",
- content: {
- user_id: "@foo:bar",
- displayname: "Foo Bar",
- presence: "online"
- }
- }],
- rooms: [{
- room_id: "!erufh:bar",
- membership: "join",
- messages: {
- start: "s",
- end: "t",
- chunk: [
- utils.mkMessage({
- room: "!erufh:bar", user: "@foo:bar", msg: "hmmm"
- })
- ]
- },
- state: [
- utils.mkMembership({
- room: "!erufh:bar", mship: "join", user: "@foo:bar"
- }),
- utils.mkEvent({
- type: "m.room.create", room: "!erufh:bar", user: "@foo:bar",
- content: {
- creator: "@foo:bar"
- }
+ var SYNC_DATA = {
+ next_batch: "s_5_3",
+ presence: {
+ events: [
+ utils.mkPresence({
+ user: "@foo:bar", name: "Foo Bar", presence: "online"
})
]
- }]
- };
- var eventData = {
- start: "s_5_3",
- end: "e_6_7",
- chunk: [
- utils.mkMessage({
- room: "!erufh:bar", user: "@foo:bar", msg: "ello ello"
- }),
- utils.mkMessage({
- room: "!erufh:bar", user: "@foo:bar", msg: ":D"
- }),
- utils.mkEvent({
- type: "m.typing", room: "!erufh:bar", content: {
- user_ids: ["@foo:bar"]
+ },
+ rooms: {
+ join: {
+ "!erufh:bar": {
+ timeline: {
+ events: [
+ utils.mkMessage({
+ room: "!erufh:bar", user: "@foo:bar", msg: "hmmm"
+ })
+ ],
+ prev_batch: "s"
+ },
+ state: {
+ events: [
+ utils.mkMembership({
+ room: "!erufh:bar", mship: "join", user: "@foo:bar"
+ }),
+ utils.mkEvent({
+ type: "m.room.create", room: "!erufh:bar",
+ user: "@foo:bar",
+ content: {
+ creator: "@foo:bar"
+ }
+ })
+ ]
+ }
}
- })
- ]
+ }
+ }
+ };
+ var NEXT_SYNC_DATA = {
+ next_batch: "e_6_7",
+ rooms: {
+ join: {
+ "!erufh:bar": {
+ timeline: {
+ events: [
+ utils.mkMessage({
+ room: "!erufh:bar", user: "@foo:bar", msg: "ello ello"
+ }),
+ utils.mkMessage({
+ room: "!erufh:bar", user: "@foo:bar", msg: ":D"
+ }),
+ ]
+ },
+ ephemeral: {
+ events: [
+ utils.mkEvent({
+ type: "m.typing", room: "!erufh:bar", content: {
+ user_ids: ["@foo:bar"]
+ }
+ })
+ ]
+ }
+ }
+ }
+ }
};
- it("should emit events from both /initialSync and /events", function(done) {
- httpBackend.when("GET", "/initialSync").respond(200, initialSync);
- httpBackend.when("GET", "/events").respond(200, eventData);
+ it("should emit events from both the first and subsequent /sync calls",
+ function(done) {
+ httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
+ httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
+
+ var expectedEvents = [];
+ expectedEvents = expectedEvents.concat(
+ SYNC_DATA.presence.events,
+ SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
+ SYNC_DATA.rooms.join["!erufh:bar"].state.events,
+ NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
+ NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events
+ );
- // initial sync events are unordered, so make an array of the types
- // that should be emitted and we'll just pick them off one by one,
- // so long as this is emptied we're good.
- var initialSyncEventTypes = [
- "m.presence", "m.room.member", "m.room.message", "m.room.create"
- ];
- var chunkIndex = 0;
client.on("event", function(event) {
- if (initialSyncEventTypes.length === 0) {
- if (chunkIndex + 1 >= eventData.chunk.length) {
- return;
+ var found = false;
+ for (var i = 0; i < expectedEvents.length; i++) {
+ if (expectedEvents[i].event_id === event.getId()) {
+ expectedEvents.splice(i, 1);
+ found = true;
+ break;
}
- // this should be /events now
- expect(eventData.chunk[chunkIndex].event_id).toEqual(
- event.getId()
- );
- chunkIndex++;
- return;
}
- var index = initialSyncEventTypes.indexOf(event.getType());
- expect(index).not.toEqual(
- -1, "Unexpected event type: " + event.getType()
+ expect(found).toBe(
+ true, "Unexpected 'event' emitted: " + event.getType()
);
- if (index >= 0) {
- initialSyncEventTypes.splice(index, 1);
- }
});
client.startClient();
httpBackend.flush().done(function() {
- expect(initialSyncEventTypes.length).toEqual(
- 0, "Failed to see all events from /initialSync"
- );
- expect(chunkIndex + 1).toEqual(
- eventData.chunk.length, "Failed to see all events from /events"
+ expect(expectedEvents.length).toEqual(
+ 0, "Failed to see all events from /sync calls"
);
done();
});
});
it("should emit User events", function(done) {
- httpBackend.when("GET", "/initialSync").respond(200, initialSync);
- httpBackend.when("GET", "/events").respond(200, eventData);
+ httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
+ httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var fired = false;
client.on("User.presence", function(event, user) {
fired = true;
@@ -135,9 +142,9 @@ describe("MatrixClient events", function() {
expect(event).toBeDefined();
if (!user || !event) { return; }
- expect(event.event).toEqual(initialSync.presence[0]);
+ expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
expect(user.presence).toEqual(
- initialSync.presence[0].content.presence
+ SYNC_DATA.presence.events[0].content.presence
);
});
client.startClient();
@@ -149,8 +156,8 @@ describe("MatrixClient events", function() {
});
it("should emit Room events", function(done) {
- httpBackend.when("GET", "/initialSync").respond(200, initialSync);
- httpBackend.when("GET", "/events").respond(200, eventData);
+ httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
+ httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var roomInvokeCount = 0;
var roomNameInvokeCount = 0;
var timelineFireCount = 0;
@@ -183,8 +190,8 @@ describe("MatrixClient events", function() {
});
it("should emit RoomState events", function(done) {
- httpBackend.when("GET", "/initialSync").respond(200, initialSync);
- httpBackend.when("GET", "/events").respond(200, eventData);
+ httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
+ httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var roomStateEventTypes = [
"m.room.member", "m.room.create"
@@ -232,8 +239,8 @@ describe("MatrixClient events", function() {
});
it("should emit RoomMember events", function(done) {
- httpBackend.when("GET", "/initialSync").respond(200, initialSync);
- httpBackend.when("GET", "/events").respond(200, eventData);
+ httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
+ httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var typingInvokeCount = 0;
var powerLevelInvokeCount = 0;
diff --git a/spec/integ/matrix-client-opts.spec.js b/spec/integ/matrix-client-opts.spec.js
index d8c010c78..347e3acd3 100644
--- a/spec/integ/matrix-client-opts.spec.js
+++ b/spec/integ/matrix-client-opts.spec.js
@@ -11,47 +11,45 @@ describe("MatrixClient opts", function() {
var userB = "@bob:localhost";
var accessToken = "aseukfgwef";
var roomId = "!foo:bar";
- var eventData = {
- chunk: [],
- start: "s",
- end: "e"
- };
- var initialSync = {
- end: "s_5_3",
- presence: [],
- rooms: [{
- membership: "join",
- room_id: roomId,
- messages: {
- start: "f_1_1",
- end: "f_2_2",
- chunk: [
- utils.mkMessage({
- room: roomId, user: userB, msg: "hello"
- })
- ]
- },
- state: [
- utils.mkEvent({
- type: "m.room.name", room: roomId, user: userB,
- content: {
- name: "Old room name"
+ var syncData = {
+ next_batch: "s_5_3",
+ presence: {},
+ rooms: {
+ join: {
+ "!foo:bar": { // roomId
+ timeline: {
+ events: [
+ utils.mkMessage({
+ room: roomId, user: userB, msg: "hello"
+ })
+ ],
+ prev_batch: "f_1_1"
+ },
+ state: {
+ events: [
+ utils.mkEvent({
+ type: "m.room.name", room: roomId, user: userB,
+ content: {
+ name: "Old room name"
+ }
+ }),
+ utils.mkMembership({
+ room: roomId, mship: "join", user: userB, name: "Bob"
+ }),
+ utils.mkMembership({
+ room: roomId, mship: "join", user: userId, name: "Alice"
+ }),
+ utils.mkEvent({
+ type: "m.room.create", room: roomId, user: userId,
+ content: {
+ creator: userId
+ }
+ })
+ ]
}
- }),
- utils.mkMembership({
- room: roomId, mship: "join", user: userB, name: "Bob"
- }),
- utils.mkMembership({
- room: roomId, mship: "join", user: userId, name: "Alice"
- }),
- utils.mkEvent({
- type: "m.room.create", room: roomId, user: userId,
- content: {
- creator: userId
- }
- })
- ]
- }]
+ }
+ }
+ }
};
beforeEach(function() {
@@ -101,13 +99,13 @@ describe("MatrixClient opts", function() {
);
});
httpBackend.when("GET", "/pushrules").respond(200, {});
- httpBackend.when("GET", "/initialSync").respond(200, initialSync);
- httpBackend.when("GET", "/events").respond(200, eventData);
+ httpBackend.when("POST", "/filter").respond(200, { filter_id: "foo" });
+ httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush("/pushrules", 1).then(function() {
- return httpBackend.flush("/initialSync", 1);
+ return httpBackend.flush("/filter", 1);
}).then(function() {
- return httpBackend.flush("/events", 1);
+ return httpBackend.flush("/sync", 1);
}).done(function() {
expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes
diff --git a/spec/integ/matrix-client-room-timeline.spec.js b/spec/integ/matrix-client-room-timeline.spec.js
index 58d32c011..a8cf97f70 100644
--- a/spec/integ/matrix-client-room-timeline.spec.js
+++ b/spec/integ/matrix-client-room-timeline.spec.js
@@ -12,45 +12,94 @@ describe("MatrixClient room timelines", function() {
var accessToken = "aseukfgwef";
var roomId = "!foo:bar";
var otherUserId = "@bob:localhost";
- var eventData;
- var initialSync = {
- end: "s_5_3",
- presence: [],
- rooms: [{
- membership: "join",
- room_id: roomId,
- messages: {
- start: "f_1_1",
- end: "f_2_2",
- chunk: [
- utils.mkMessage({
- room: roomId, user: otherUserId, msg: "hello"
- })
- ]
- },
- state: [
- utils.mkEvent({
- type: "m.room.name", room: roomId, user: otherUserId,
- content: {
- name: "Old room name"
+ var USER_MEMBERSHIP_EVENT = utils.mkMembership({
+ room: roomId, mship: "join", user: userId, name: userName
+ });
+ var ROOM_NAME_EVENT = utils.mkEvent({
+ type: "m.room.name", room: roomId, user: otherUserId,
+ content: {
+ name: "Old room name"
+ }
+ });
+ var NEXT_SYNC_DATA;
+ var SYNC_DATA = {
+ next_batch: "s_5_3",
+ rooms: {
+ join: {
+ "!foo:bar": { // roomId
+ timeline: {
+ events: [
+ utils.mkMessage({
+ room: roomId, user: otherUserId, msg: "hello"
+ })
+ ],
+ prev_batch: "f_1_1"
+ },
+ state: {
+ events: [
+ ROOM_NAME_EVENT,
+ utils.mkMembership({
+ room: roomId, mship: "join",
+ user: otherUserId, name: "Bob"
+ }),
+ USER_MEMBERSHIP_EVENT,
+ utils.mkEvent({
+ type: "m.room.create", room: roomId, user: userId,
+ content: {
+ creator: userId
+ }
+ })
+ ]
}
- }),
- utils.mkMembership({
- room: roomId, mship: "join", user: otherUserId, name: "Bob"
- }),
- utils.mkMembership({
- room: roomId, mship: "join", user: userId, name: userName
- }),
- utils.mkEvent({
- type: "m.room.create", room: roomId, user: userId,
- content: {
- creator: userId
- }
- })
- ]
- }]
+ }
+ }
+ }
};
+ function setNextSyncData(events) {
+ events = events || [];
+ NEXT_SYNC_DATA = {
+ next_batch: "n",
+ presence: { events: [] },
+ rooms: {
+ invite: {},
+ join: {
+ "!foo:bar": {
+ timeline: { events: [] },
+ state: { events: [] },
+ ephemeral: { events: [] }
+ }
+ },
+ leave: {}
+ }
+ };
+ events.forEach(function(e) {
+ if (e.room_id !== roomId) {
+ throw new Error("setNextSyncData only works with one room id");
+ }
+ if (e.state_key) {
+ if (e.__prev_event === undefined) {
+ throw new Error(
+ "setNextSyncData needs the prev state set to '__prev_event' " +
+ "for " + e.type
+ );
+ }
+ if (e.__prev_event !== null) {
+ // push the previous state for this event type
+ NEXT_SYNC_DATA.rooms.join[roomId].state.events.push(e.__prev_event);
+ }
+ // push the current
+ NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
+ }
+ else if (["m.typing", "m.receipt"].indexOf(e.type) !== -1) {
+ NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e);
+ }
+ else {
+ NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
+ }
+ });
+ }
+
beforeEach(function(done) {
utils.beforeEach(this);
httpBackend = new HttpBackend();
@@ -60,18 +109,17 @@ describe("MatrixClient room timelines", function() {
userId: userId,
accessToken: accessToken
});
- eventData = {
- chunk: [],
- end: "end_",
- start: "start_"
- };
+ setNextSyncData();
httpBackend.when("GET", "/pushrules").respond(200, {});
- httpBackend.when("GET", "/initialSync").respond(200, initialSync);
- httpBackend.when("GET", "/events").respond(200, function() {
- return eventData;
+ httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
+ httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
+ httpBackend.when("GET", "/sync").respond(200, function() {
+ return NEXT_SYNC_DATA;
});
client.startClient();
- httpBackend.flush("/pushrules").done(done);
+ httpBackend.flush("/pushrules").then(function() {
+ return httpBackend.flush("/filter");
+ }).done(done);
});
afterEach(function() {
@@ -97,11 +145,11 @@ describe("MatrixClient room timelines", function() {
expect(member.userId).toEqual(userId);
expect(member.name).toEqual(userName);
- httpBackend.flush("/events", 1).done(function() {
+ httpBackend.flush("/sync", 1).done(function() {
done();
});
});
- httpBackend.flush("/initialSync", 1);
+ httpBackend.flush("/sync", 1);
});
it("should be updated correctly when the send request finishes " +
@@ -110,12 +158,12 @@ describe("MatrixClient room timelines", function() {
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId
});
- eventData.chunk = [
- utils.mkMessage({
- body: "I am a fish", user: userId, room: roomId
- })
- ];
- eventData.chunk[0].event_id = eventId;
+
+ var ev = utils.mkMessage({
+ body: "I am a fish", user: userId, room: roomId
+ });
+ ev.event_id = eventId;
+ setNextSyncData([ev]);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
@@ -123,14 +171,14 @@ describe("MatrixClient room timelines", function() {
client.sendTextMessage(roomId, "I am a fish", "txn1").done(
function() {
expect(room.timeline[1].getId()).toEqual(eventId);
- httpBackend.flush("/events", 1).done(function() {
+ httpBackend.flush("/sync", 1).done(function() {
expect(room.timeline[1].getId()).toEqual(eventId);
done();
});
});
httpBackend.flush("/txn1", 1);
});
- httpBackend.flush("/initialSync", 1);
+ httpBackend.flush("/sync", 1);
});
it("should be updated correctly when the send request finishes " +
@@ -139,18 +187,18 @@ describe("MatrixClient room timelines", function() {
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId
});
- eventData.chunk = [
- utils.mkMessage({
- body: "I am a fish", user: userId, room: roomId
- })
- ];
- eventData.chunk[0].event_id = eventId;
+
+ var ev = utils.mkMessage({
+ body: "I am a fish", user: userId, room: roomId
+ });
+ ev.event_id = eventId;
+ setNextSyncData([ev]);
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() {
+ httpBackend.flush("/sync", 1).done(function() {
// expect 3rd msg, it doesn't know this is the request is just did
expect(room.timeline.length).toEqual(3);
httpBackend.flush("/txn1", 1);
@@ -162,7 +210,7 @@ describe("MatrixClient room timelines", function() {
});
});
- httpBackend.flush("/initialSync", 1);
+ httpBackend.flush("/sync", 1);
});
});
@@ -195,9 +243,9 @@ describe("MatrixClient room timelines", function() {
});
httpBackend.flush("/messages", 1);
- httpBackend.flush("/events", 1);
+ httpBackend.flush("/sync", 1);
});
- httpBackend.flush("/initialSync", 1);
+ httpBackend.flush("/sync", 1);
});
it("should set the right event.sender values", function(done) {
@@ -238,9 +286,9 @@ describe("MatrixClient room timelines", function() {
});
httpBackend.flush("/messages", 1);
- httpBackend.flush("/events", 1);
+ httpBackend.flush("/sync", 1);
});
- httpBackend.flush("/initialSync", 1);
+ httpBackend.flush("/sync", 1);
});
it("should add it them to the right place in the timeline", function(done) {
@@ -267,9 +315,9 @@ describe("MatrixClient room timelines", function() {
});
httpBackend.flush("/messages", 1);
- httpBackend.flush("/events", 1);
+ httpBackend.flush("/sync", 1);
});
- httpBackend.flush("/initialSync", 1);
+ httpBackend.flush("/sync", 1);
});
it("should use 'end' as the next pagination token", function(done) {
@@ -289,21 +337,23 @@ describe("MatrixClient room timelines", function() {
expect(room.oldState.paginationToken).toEqual(sbEndTok);
});
- httpBackend.flush("/events", 1);
+ httpBackend.flush("/sync", 1);
httpBackend.flush("/messages", 1).done(function() {
done();
});
});
- httpBackend.flush("/initialSync", 1);
+ httpBackend.flush("/sync", 1);
});
});
describe("new events", function() {
it("should be added to the right place in the timeline", function(done) {
- eventData.chunk = [
+ var eventData = [
utils.mkMessage({user: userId, room: roomId}),
utils.mkMessage({user: userId, room: roomId})
];
+ setNextSyncData(eventData);
+
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
@@ -312,37 +362,40 @@ describe("MatrixClient room timelines", function() {
client.on("Room.timeline", function(event, rm, toStart) {
expect(toStart).toBe(false);
expect(rm).toEqual(room);
- expect(event.event).toEqual(eventData.chunk[index]);
+ expect(event.event).toEqual(eventData[index]);
index += 1;
});
httpBackend.flush("/messages", 1);
- httpBackend.flush("/events", 1).done(function() {
+ httpBackend.flush("/sync", 1).done(function() {
expect(index).toEqual(2);
expect(room.timeline[room.timeline.length - 1].event).toEqual(
- eventData.chunk[1]
+ eventData[1]
);
expect(room.timeline[room.timeline.length - 2].event).toEqual(
- eventData.chunk[0]
+ eventData[0]
);
done();
});
});
- httpBackend.flush("/initialSync", 1);
+ httpBackend.flush("/sync", 1);
});
it("should set the right event.sender values", function(done) {
- eventData.chunk = [
+ var eventData = [
utils.mkMessage({user: userId, room: roomId}),
utils.mkMembership({
user: userId, room: roomId, mship: "join", name: "New Name"
}),
utils.mkMessage({user: userId, room: roomId})
];
+ eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
+ setNextSyncData(eventData);
+
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
- httpBackend.flush("/events", 1).done(function() {
+ httpBackend.flush("/sync", 1).done(function() {
var preNameEvent = room.timeline[room.timeline.length - 3];
var postNameEvent = room.timeline[room.timeline.length - 1];
expect(preNameEvent.sender.name).toEqual(userName);
@@ -350,17 +403,18 @@ describe("MatrixClient room timelines", function() {
done();
});
});
- httpBackend.flush("/initialSync", 1);
+ httpBackend.flush("/sync", 1);
});
it("should set the right room.name", function(done) {
- eventData.chunk = [
- utils.mkEvent({
- user: userId, room: roomId, type: "m.room.name", content: {
- name: "Room 2"
- }
- })
- ];
+ var secondRoomNameEvent = utils.mkEvent({
+ user: userId, room: roomId, type: "m.room.name", content: {
+ name: "Room 2"
+ }
+ });
+ secondRoomNameEvent.__prev_event = ROOM_NAME_EVENT;
+ setNextSyncData([secondRoomNameEvent]);
+
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
@@ -369,32 +423,32 @@ describe("MatrixClient room timelines", function() {
nameEmitCount += 1;
});
- httpBackend.flush("/events", 1).done(function() {
+ httpBackend.flush("/sync", 1).done(function() {
expect(nameEmitCount).toEqual(1);
expect(room.name).toEqual("Room 2");
// do another round
- eventData.chunk = [
- utils.mkEvent({
- user: userId, room: roomId, type: "m.room.name", content: {
- name: "Room 3"
- }
- })
- ];
- httpBackend.when("GET", "/events").respond(200, eventData);
- httpBackend.flush("/events", 1).done(function() {
+ var thirdRoomNameEvent = utils.mkEvent({
+ user: userId, room: roomId, type: "m.room.name", content: {
+ name: "Room 3"
+ }
+ });
+ thirdRoomNameEvent.__prev_event = secondRoomNameEvent;
+ setNextSyncData([thirdRoomNameEvent]);
+ httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
+ httpBackend.flush("/sync", 1).done(function() {
expect(nameEmitCount).toEqual(2);
expect(room.name).toEqual("Room 3");
done();
});
});
});
- httpBackend.flush("/initialSync", 1);
+ httpBackend.flush("/sync", 1);
});
it("should set the right room members", function(done) {
var userC = "@cee:bar";
var userD = "@dee:bar";
- eventData.chunk = [
+ var eventData = [
utils.mkMembership({
user: userC, room: roomId, mship: "join", name: "C"
}),
@@ -402,10 +456,14 @@ describe("MatrixClient room timelines", function() {
user: userC, room: roomId, mship: "invite", skey: userD
})
];
+ eventData[0].__prev_event = null;
+ eventData[1].__prev_event = null;
+ setNextSyncData(eventData);
+
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
- httpBackend.flush("/events", 1).done(function() {
+ httpBackend.flush("/sync", 1).done(function() {
expect(room.currentState.getMembers().length).toEqual(4);
expect(room.currentState.getMember(userC).name).toEqual("C");
expect(room.currentState.getMember(userC).membership).toEqual(
@@ -418,7 +476,7 @@ describe("MatrixClient room timelines", function() {
done();
});
});
- httpBackend.flush("/initialSync", 1);
+ httpBackend.flush("/sync", 1);
});
});
});
diff --git a/spec/integ/matrix-client-syncing.spec.js b/spec/integ/matrix-client-syncing.spec.js
index 7238f6429..ba222db2b 100644
--- a/spec/integ/matrix-client-syncing.spec.js
+++ b/spec/integ/matrix-client-syncing.spec.js
@@ -26,6 +26,7 @@ describe("MatrixClient syncing", function() {
accessToken: selfAccessToken
});
httpBackend.when("GET", "/pushrules").respond(200, {});
+ httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
});
afterEach(function() {
@@ -33,20 +34,14 @@ describe("MatrixClient syncing", function() {
});
describe("startClient", function() {
- var initialSync = {
- end: "s_5_3",
- presence: [],
- rooms: []
- };
- var eventData = {
- start: "s_5_3",
- end: "e_6_7",
- chunk: []
+ var syncData = {
+ next_batch: "batch_token",
+ rooms: {},
+ presence: {}
};
- it("should start with /initialSync then move onto /events.", function(done) {
- httpBackend.when("GET", "/initialSync").respond(200, initialSync);
- httpBackend.when("GET", "/events").respond(200, eventData);
+ it("should /sync after /pushrules and /filter.", function(done) {
+ httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
@@ -55,12 +50,12 @@ describe("MatrixClient syncing", function() {
});
});
- it("should pass the 'end' token from /initialSync to the from= param " +
- " of /events", function(done) {
- httpBackend.when("GET", "/initialSync").respond(200, initialSync);
- httpBackend.when("GET", "/events").check(function(req) {
- expect(req.queryParams.from).toEqual(initialSync.end);
- }).respond(200, eventData);
+ it("should pass the 'next_batch' token from /sync to the since= param " +
+ " of the next /sync", function(done) {
+ httpBackend.when("GET", "/sync").respond(200, syncData);
+ httpBackend.when("GET", "/sync").check(function(req) {
+ expect(req.queryParams.since).toEqual(syncData.next_batch);
+ }).respond(200, syncData);
client.startClient();
@@ -71,56 +66,56 @@ describe("MatrixClient syncing", function() {
});
describe("resolving invites to profile info", function() {
- var initialSync = {
- end: "s_5_3",
- presence: [],
- rooms: [{
- membership: "join",
- room_id: roomOne,
- messages: {
- start: "f_1_1",
- end: "f_2_2",
- chunk: [
+
+ var syncData = {
+ next_batch: "s_5_3",
+ presence: {
+ events: []
+ },
+ rooms: {
+ join: {
+
+ }
+ }
+ };
+
+ beforeEach(function() {
+ syncData.presence.events = [];
+ syncData.rooms.join[roomOne] = {
+ timeline: {
+ events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello"
})
]
},
- state: [
- utils.mkMembership({
- room: roomOne, mship: "join", user: otherUserId
- }),
- utils.mkMembership({
- room: roomOne, mship: "join", user: selfUserId
- }),
- utils.mkEvent({
- type: "m.room.create", room: roomOne, user: selfUserId,
- content: {
- creator: selfUserId
- }
- })
- ]
- }]
- };
- var eventData = {
- start: "s_5_3",
- end: "e_6_7",
- chunk: []
- };
-
- beforeEach(function() {
- eventData.chunk = [];
+ state: {
+ events: [
+ utils.mkMembership({
+ room: roomOne, mship: "join", user: otherUserId
+ }),
+ utils.mkMembership({
+ room: roomOne, mship: "join", user: selfUserId
+ }),
+ utils.mkEvent({
+ type: "m.room.create", room: roomOne, user: selfUserId,
+ content: {
+ creator: selfUserId
+ }
+ })
+ ]
+ }
+ };
});
- it("should resolve incoming invites from /events", function(done) {
- eventData.chunk = [
+ it("should resolve incoming invites from /sync", function(done) {
+ syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC
})
- ];
+ );
- httpBackend.when("GET", "/initialSync").respond(200, initialSync);
- httpBackend.when("GET", "/events").respond(200, eventData);
+ httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/profile/" + encodeURIComponent(userC)).respond(
200, {
avatar_url: "mxc://flibble/wibble",
@@ -143,17 +138,18 @@ describe("MatrixClient syncing", function() {
});
it("should use cached values from m.presence wherever possible", function(done) {
- eventData.chunk = [
+ syncData.presence.events = [
utils.mkPresence({
user: userC, presence: "online", name: "The Ghost"
}),
+ ];
+ syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC
})
- ];
+ );
- httpBackend.when("GET", "/initialSync").respond(200, initialSync);
- httpBackend.when("GET", "/events").respond(200, eventData);
+ httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient({
resolveInvitesToProfiles: true
@@ -167,17 +163,18 @@ describe("MatrixClient syncing", function() {
});
it("should result in events on the room member firing", function(done) {
- eventData.chunk = [
+ syncData.presence.events = [
utils.mkPresence({
user: userC, presence: "online", name: "The Ghost"
- }),
+ })
+ ];
+ syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC
})
- ];
+ );
- httpBackend.when("GET", "/initialSync").respond(200, initialSync);
- httpBackend.when("GET", "/events").respond(200, eventData);
+ httpBackend.when("GET", "/sync").respond(200, syncData);
var latestFiredName = null;
client.on("RoomMember.name", function(event, m) {
@@ -197,14 +194,13 @@ describe("MatrixClient syncing", function() {
});
it("should no-op if resolveInvitesToProfiles is not set", function(done) {
- eventData.chunk = [
+ syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC
})
- ];
+ );
- httpBackend.when("GET", "/initialSync").respond(200, initialSync);
- httpBackend.when("GET", "/events").respond(200, eventData);
+ httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
@@ -220,44 +216,29 @@ describe("MatrixClient syncing", function() {
});
describe("users", function() {
- var initialSync = {
- end: "s_5_3",
- presence: [
- utils.mkPresence({
- user: userA, presence: "online"
- }),
- utils.mkPresence({
- user: userB, presence: "unavailable"
- })
- ],
- rooms: []
- };
- var eventData = {
- start: "s_5_3",
- end: "e_6_7",
- chunk: [
- // existing user change
- utils.mkPresence({
- user: userA, presence: "offline"
- }),
- // new user C
- utils.mkPresence({
- user: userC, presence: "online"
- })
- ]
+ var syncData = {
+ next_batch: "nb",
+ presence: {
+ events: [
+ utils.mkPresence({
+ user: userA, presence: "online"
+ }),
+ utils.mkPresence({
+ user: userB, presence: "unavailable"
+ })
+ ]
+ }
};
- it("should create users for presence events from /initialSync and /events",
+ it("should create users for presence events from /sync",
function(done) {
- httpBackend.when("GET", "/initialSync").respond(200, initialSync);
- httpBackend.when("GET", "/events").respond(200, eventData);
+ httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush().done(function() {
- expect(client.getUser(userA).presence).toEqual("offline");
+ expect(client.getUser(userA).presence).toEqual("online");
expect(client.getUser(userB).presence).toEqual("unavailable");
- expect(client.getUser(userC).presence).toEqual("online");
done();
});
});
@@ -266,108 +247,128 @@ describe("MatrixClient syncing", function() {
describe("room state", function() {
var msgText = "some text here";
var otherDisplayName = "Bob Smith";
- var initialSync = {
- end: "s_5_3",
- presence: [],
- rooms: [
- {
- membership: "join",
- room_id: roomOne,
- messages: {
- start: "f_1_1",
- end: "f_2_2",
- chunk: [
- utils.mkMessage({
- room: roomOne, user: otherUserId, msg: "hello"
- })
- ]
- },
- state: [
- utils.mkEvent({
- type: "m.room.name", room: roomOne, user: otherUserId,
- content: {
- name: "Old room name"
- }
- }),
- utils.mkMembership({
- room: roomOne, mship: "join", user: otherUserId
- }),
- utils.mkMembership({
- room: roomOne, mship: "join", user: selfUserId
- }),
- utils.mkEvent({
- type: "m.room.create", room: roomOne, user: selfUserId,
- content: {
- creator: selfUserId
- }
- })
- ]
- },
- {
- membership: "join",
- room_id: roomTwo,
- messages: {
- start: "f_1_1",
- end: "f_2_2",
- chunk: [
- utils.mkMessage({
- room: roomTwo, user: otherUserId, msg: "hiii"
- })
- ]
- },
- state: [
- utils.mkMembership({
- room: roomTwo, mship: "join", user: otherUserId,
- name: otherDisplayName
- }),
- utils.mkMembership({
- room: roomTwo, mship: "join", user: selfUserId
- }),
- utils.mkEvent({
- type: "m.room.create", room: roomTwo, user: selfUserId,
- content: {
- creator: selfUserId
- }
- })
- ]
+
+ var syncData = {
+ rooms: {
+ join: {
+
}
- ]
+ }
};
- var eventData = {
- start: "s_5_3",
- end: "e_6_7",
- chunk: [
- utils.mkEvent({
- type: "m.room.name", room: roomOne, user: selfUserId,
- content: { name: "A new room name" }
- }),
- utils.mkMessage({
- room: roomTwo, user: otherUserId, msg: msgText
- }),
- utils.mkEvent({
- type: "m.typing", room: roomTwo,
- content: { user_ids: [otherUserId] }
- })
- ]
+ syncData.rooms.join[roomOne] = {
+ timeline: {
+ events: [
+ utils.mkMessage({
+ room: roomOne, user: otherUserId, msg: "hello"
+ })
+ ]
+ },
+ state: {
+ events: [
+ utils.mkEvent({
+ type: "m.room.name", room: roomOne, user: otherUserId,
+ content: {
+ name: "Old room name"
+ }
+ }),
+ utils.mkMembership({
+ room: roomOne, mship: "join", user: otherUserId
+ }),
+ utils.mkMembership({
+ room: roomOne, mship: "join", user: selfUserId
+ }),
+ utils.mkEvent({
+ type: "m.room.create", room: roomOne, user: selfUserId,
+ content: {
+ creator: selfUserId
+ }
+ })
+ ]
+ }
+ };
+ syncData.rooms.join[roomTwo] = {
+ timeline: {
+ events: [
+ utils.mkMessage({
+ room: roomTwo, user: otherUserId, msg: "hiii"
+ })
+ ]
+ },
+ state: {
+ events: [
+ utils.mkMembership({
+ room: roomTwo, mship: "join", user: otherUserId,
+ name: otherDisplayName
+ }),
+ utils.mkMembership({
+ room: roomTwo, mship: "join", user: selfUserId
+ }),
+ utils.mkEvent({
+ type: "m.room.create", room: roomTwo, user: selfUserId,
+ content: {
+ creator: selfUserId
+ }
+ })
+ ]
+ }
+ };
+
+ var nextSyncData = {
+ rooms: {
+ join: {
+
+ }
+ }
+ };
+
+ nextSyncData.rooms.join[roomOne] = {
+ state: {
+ events: [
+ utils.mkEvent({
+ type: "m.room.name", room: roomOne, user: selfUserId,
+ content: { name: "A new room name" }
+ })
+ ]
+ }
+ };
+
+ nextSyncData.rooms.join[roomTwo] = {
+ timeline: {
+ events: [
+ utils.mkMessage({
+ room: roomTwo, user: otherUserId, msg: msgText
+ })
+ ]
+ },
+ ephemeral: {
+ events: [
+ utils.mkEvent({
+ type: "m.typing", room: roomTwo,
+ content: { user_ids: [otherUserId] }
+ })
+ ]
+ }
};
it("should continually recalculate the right room name.", function(done) {
- httpBackend.when("GET", "/initialSync").respond(200, initialSync);
- httpBackend.when("GET", "/events").respond(200, eventData);
+ httpBackend.when("GET", "/sync").respond(200, syncData);
+ httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomOne);
// should have clobbered the name to the one from /events
- expect(room.name).toEqual(eventData.chunk[0].content.name);
+ expect(room.name).toEqual(
+ nextSyncData.rooms.join[roomOne].state.events[0].content.name
+ );
done();
});
});
it("should store the right events in the timeline.", function(done) {
- httpBackend.when("GET", "/initialSync").respond(200, initialSync);
- httpBackend.when("GET", "/events").respond(200, eventData);
+ httpBackend.when("GET", "/sync").respond(200, syncData);
+ httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
@@ -381,8 +382,8 @@ describe("MatrixClient syncing", function() {
});
it("should set the right room name.", function(done) {
- httpBackend.when("GET", "/initialSync").respond(200, initialSync);
- httpBackend.when("GET", "/events").respond(200, eventData);
+ httpBackend.when("GET", "/sync").respond(200, syncData);
+ httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
httpBackend.flush().done(function() {
@@ -394,8 +395,8 @@ describe("MatrixClient syncing", function() {
});
it("should set the right user's typing flag.", function(done) {
- httpBackend.when("GET", "/initialSync").respond(200, initialSync);
- httpBackend.when("GET", "/events").respond(200, eventData);
+ httpBackend.when("GET", "/sync").respond(200, syncData);
+ httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
@@ -421,23 +422,23 @@ describe("MatrixClient syncing", function() {
});
describe("receipts", function() {
- var initialSync = {
- end: "s_5_3",
- presence: [],
- receipts: [],
- rooms: [{
- membership: "join",
- room_id: roomOne,
- messages: {
- start: "f_1_1",
- end: "f_2_2",
- chunk: [
- utils.mkMessage({
- room: roomOne, user: otherUserId, msg: "hello"
- })
- ]
- },
- state: [
+ var syncData = {
+ rooms: {
+ join: {
+
+ }
+ }
+ };
+ syncData.rooms.join[roomOne] = {
+ timeline: {
+ events: [
+ utils.mkMessage({
+ room: roomOne, user: otherUserId, msg: "hello"
+ })
+ ]
+ },
+ state: {
+ events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
@@ -457,21 +458,17 @@ describe("MatrixClient syncing", function() {
}
})
]
- }]
- };
- var eventData = {
- start: "s_5_3",
- end: "e_6_7",
- chunk: []
+ }
};
beforeEach(function() {
- eventData.chunk = [];
- initialSync.receipts = [];
+ syncData.rooms.join[roomOne].ephemeral = {
+ events: []
+ };
});
- it("should sync receipts from /initialSync.", function(done) {
- var ackEvent = initialSync.rooms[0].messages.chunk[0];
+ it("should sync receipts from /sync.", function(done) {
+ var ackEvent = syncData.rooms.join[roomOne].timeline.events[0];
var receipt = {};
receipt[ackEvent.event_id] = {
"m.read": {}
@@ -479,45 +476,12 @@ describe("MatrixClient syncing", function() {
receipt[ackEvent.event_id]["m.read"][otherUserId] = {
ts: 176592842636
};
- initialSync.receipts = [{
+ syncData.rooms.join[roomOne].ephemeral.events = [{
content: receipt,
room_id: roomOne,
type: "m.receipt"
}];
- httpBackend.when("GET", "/initialSync").respond(200, initialSync);
- httpBackend.when("GET", "/events").respond(200, eventData);
-
- client.startClient();
-
- httpBackend.flush().done(function() {
- var room = client.getRoom(roomOne);
- expect(room.getReceiptsForEvent(new MatrixEvent(ackEvent))).toEqual([{
- type: "m.read",
- userId: otherUserId,
- data: {
- ts: 176592842636
- }
- }]);
- done();
- });
- });
-
- it("should sync receipts from /events.", function(done) {
- var ackEvent = initialSync.rooms[0].messages.chunk[0];
- var receipt = {};
- receipt[ackEvent.event_id] = {
- "m.read": {}
- };
- receipt[ackEvent.event_id]["m.read"][otherUserId] = {
- ts: 176592842636
- };
- eventData.chunk = [{
- content: receipt,
- room_id: roomOne,
- type: "m.receipt"
- }];
- httpBackend.when("GET", "/initialSync").respond(200, initialSync);
- httpBackend.when("GET", "/events").respond(200, eventData);
+ httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
diff --git a/spec/test-utils.js b/spec/test-utils.js
index 92a74107f..4f4e26573 100644
--- a/spec/test-utils.js
+++ b/spec/test-utils.js
@@ -61,7 +61,7 @@ module.exports.mkEvent = function(opts) {
var event = {
type: opts.type,
room_id: opts.room,
- user_id: opts.user,
+ sender: opts.user,
content: opts.content,
event_id: "$" + Math.random() + "-" + Math.random()
};
@@ -88,8 +88,8 @@ module.exports.mkPresence = function(opts) {
var event = {
event_id: "$" + Math.random() + "-" + Math.random(),
type: "m.presence",
+ sender: opts.user,
content: {
- user_id: opts.user,
avatar_url: opts.url,
displayname: opts.name,
last_active_ago: opts.ago,
diff --git a/spec/unit/matrix-client.spec.js b/spec/unit/matrix-client.spec.js
index 5d5093e7a..daa67ad9d 100644
--- a/spec/unit/matrix-client.spec.js
+++ b/spec/unit/matrix-client.spec.js
@@ -9,20 +9,30 @@ describe("MatrixClient", function() {
var identityServerUrl = "https://identity.server";
var client, store, scheduler;
- var initialSyncData = {
- end: "s_5_3",
- presence: [],
- rooms: []
- };
-
- var eventData = {
- start: "s_START",
- end: "s_END",
- chunk: []
- };
-
var PUSH_RULES_RESPONSE = {
- method: "GET", path: "/pushrules/", data: {}
+ method: "GET",
+ path: "/pushrules/",
+ data: {}
+ };
+
+ var FILTER_PATH = "/user/" + encodeURIComponent(userId) + "/filter";
+
+ var FILTER_RESPONSE = {
+ method: "POST",
+ path: FILTER_PATH,
+ data: { filter_id: "f1lt3r" }
+ };
+
+ var SYNC_DATA = {
+ next_batch: "s_5_3",
+ presence: { events: [] },
+ rooms: {}
+ };
+
+ var SYNC_RESPONSE = {
+ method: "GET",
+ path: "/sync",
+ data: SYNC_DATA
};
var httpLookups = [
@@ -107,7 +117,8 @@ describe("MatrixClient", function() {
]);
store = jasmine.createSpyObj("store", [
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
- "setSyncToken", "storeEvents", "storeRoom", "storeUser"
+ "setSyncToken", "storeEvents", "storeRoom", "storeUser",
+ "getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter"
]);
client = new MatrixClient({
baseUrl: "https://my.home.server",
@@ -130,9 +141,8 @@ describe("MatrixClient", function() {
pendingLookup = null;
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
- httpLookups.push({
- method: "GET", path: "/initialSync", data: initialSyncData
- });
+ httpLookups.push(FILTER_RESPONSE);
+ httpLookups.push(SYNC_RESPONSE);
});
afterEach(function() {
@@ -149,6 +159,23 @@ describe("MatrixClient", function() {
});
});
+ it("should not POST /filter if a filter already exists", function(done) {
+ httpLookups = [];
+ httpLookups.push(PUSH_RULES_RESPONSE);
+ httpLookups.push(SYNC_RESPONSE);
+ var filterId = "ehfewf";
+ store.getFilterIdByName.andReturn(filterId);
+ client.startClient();
+
+ client.on("sync", function syncListener(state) {
+ if (state === "SYNCING") {
+ expect(httpLookups.length).toEqual(0);
+ client.removeListener("sync", syncListener);
+ done();
+ }
+ });
+ });
+
describe("getSyncState", function() {
it("should return null if the client isn't started", function() {
@@ -156,9 +183,10 @@ describe("MatrixClient", function() {
});
it("should return the same sync state as emitted sync events", function(done) {
- client.on("sync", function(state) {
+ client.on("sync", function syncListener(state) {
expect(state).toEqual(client.getSyncState());
if (state === "SYNCING") {
+ client.removeListener("sync", syncListener);
done();
}
});
@@ -172,40 +200,46 @@ describe("MatrixClient", function() {
expect(client.retryImmediately()).toBe(false);
});
- it("should work on /initialSync", function(done) {
+ it("should work on /filter", function(done) {
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({
- method: "GET", path: "/initialSync", error: { errcode: "NOPE_NOPE_NOPE" }
+ method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" }
});
httpLookups.push({
- method: "GET", path: "/initialSync", error: { errcode: "NOPE_NOPE_NOPE" }
+ method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" }
});
- client.on("sync", function(state) {
+ client.on("sync", function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(1);
expect(client.retryImmediately()).toBe(true);
expect(httpLookups.length).toEqual(0);
+ client.removeListener("sync", syncListener);
done();
}
});
client.startClient();
});
- it("should work on /events", function(done) {
+ it("should work on /sync", function(done) {
httpLookups.push({
- method: "GET", path: "/events", error: { errcode: "NOPE_NOPE_NOPE" }
+ method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }
});
httpLookups.push({
- method: "GET", path: "/events", data: eventData
+ method: "GET", path: "/sync", data: SYNC_DATA
});
- client.on("sync", function(state) {
+ client.on("sync", function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(1);
- expect(client.retryImmediately()).toBe(true);
- expect(httpLookups.length).toEqual(0);
+ expect(client.retryImmediately()).toBe(
+ true, "retryImmediately returned false"
+ );
+ expect(httpLookups.length).toEqual(
+ 0, "more httpLookups remaining than expected"
+ );
+ client.removeListener("sync", syncListener);
done();
}
});
@@ -221,11 +255,12 @@ describe("MatrixClient", function() {
method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" }
});
- client.on("sync", function(state) {
+ client.on("sync", function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(1);
expect(client.retryImmediately()).toBe(true);
expect(httpLookups.length).toEqual(0);
+ client.removeListener("sync", syncListener);
done();
}
});
@@ -234,10 +269,9 @@ describe("MatrixClient", function() {
});
describe("emitted sync events", function() {
- var expectedStates;
- function syncChecker(done) {
- return function(state, old) {
+ function syncChecker(expectedStates, done) {
+ return function syncListener(state, old) {
var expected = expectedStates.shift();
console.log(
"'sync' curr=%s old=%s EXPECT=%s", state, old, expected
@@ -249,6 +283,7 @@ describe("MatrixClient", function() {
expect(state).toEqual(expected[0]);
expect(old).toEqual(expected[1]);
if (expectedStates.length === 0) {
+ client.removeListener("sync", syncListener);
done();
}
// standard retry time is 4s
@@ -256,94 +291,107 @@ describe("MatrixClient", function() {
};
}
- beforeEach(function() {
- expectedStates = [
- // [current, old]
- ];
- });
-
- it("should transition null -> PREPARED after /initialSync", function(done) {
+ it("should transition null -> PREPARED after the first /sync", function(done) {
+ var expectedStates = [];
expectedStates.push(["PREPARED", null]);
- client.on("sync", syncChecker(done));
+ client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
- it("should transition null -> ERROR after a failed /initialSync", function(done) {
+ it("should transition null -> ERROR after a failed /filter", function(done) {
+ var expectedStates = [];
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({
- method: "GET", path: "/initialSync", error: { errcode: "NOPE_NOPE_NOPE" }
+ method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" }
});
expectedStates.push(["ERROR", null]);
- client.on("sync", syncChecker(done));
+ client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
- it("should transition ERROR -> PREPARED after /initialSync if prev failed",
+ it("should transition ERROR -> PREPARED after /sync if prev failed",
function(done) {
+ var expectedStates = [];
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
+ httpLookups.push(FILTER_RESPONSE);
httpLookups.push({
- method: "GET", path: "/initialSync", error: { errcode: "NOPE_NOPE_NOPE" }
+ method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }
});
httpLookups.push({
- method: "GET", path: "/initialSync", data: initialSyncData
+ method: "GET", path: "/sync", data: SYNC_DATA
});
expectedStates.push(["ERROR", null]);
expectedStates.push(["PREPARED", "ERROR"]);
- client.on("sync", syncChecker(done));
+ client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
- it("should transition PREPARED -> SYNCING after /initialSync", function(done) {
+ it("should transition PREPARED -> SYNCING after /sync", function(done) {
+ var expectedStates = [];
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
- client.on("sync", syncChecker(done));
+ client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
- it("should transition SYNCING -> ERROR after a failed /events", function(done) {
+ it("should transition SYNCING -> ERROR after a failed /sync", function(done) {
+ var expectedStates = [];
httpLookups.push({
- method: "GET", path: "/events", error: { errcode: "NONONONONO" }
+ method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
});
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
- client.on("sync", syncChecker(done));
+ client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
- it("should transition ERROR -> SYNCING after /events if prev failed",
+ xit("should transition ERROR -> SYNCING after /sync if prev failed",
function(done) {
+ var expectedStates = [];
httpLookups.push({
- method: "GET", path: "/events", error: { errcode: "NONONONONO" }
- });
- httpLookups.push({
- method: "GET", path: "/events", data: eventData
+ method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
});
+ httpLookups.push(SYNC_RESPONSE);
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
- client.on("sync", syncChecker(done));
+ client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
- it("should transition ERROR -> ERROR if multiple /events fails", function(done) {
+ it("should transition SYNCING -> SYNCING on subsequent /sync successes",
+ function(done) {
+ var expectedStates = [];
+ httpLookups.push(SYNC_RESPONSE);
+ httpLookups.push(SYNC_RESPONSE);
+
+ expectedStates.push(["PREPARED", null]);
+ expectedStates.push(["SYNCING", "PREPARED"]);
+ expectedStates.push(["SYNCING", "SYNCING"]);
+ client.on("sync", syncChecker(expectedStates, done));
+ client.startClient();
+ });
+
+ it("should transition ERROR -> ERROR if multiple /sync fails", function(done) {
+ var expectedStates = [];
httpLookups.push({
- method: "GET", path: "/events", error: { errcode: "NONONONONO" }
+ method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
});
httpLookups.push({
- method: "GET", path: "/events", error: { errcode: "NONONONONO" }
+ method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
});
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
expectedStates.push(["ERROR", "ERROR"]);
- client.on("sync", syncChecker(done));
+ client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
});
@@ -373,15 +421,13 @@ describe("MatrixClient", function() {
"!foo:bar", "!baz:bar"
];
- it("should be set via setGuestRooms and used in /events calls", function(done) {
+ it("should be set via setGuestRooms and used in /sync calls", function(done) {
httpLookups = []; // no /pushrules
- httpLookups.push({
- method: "GET", path: "/initialSync", data: initialSyncData
- });
+ httpLookups.push(FILTER_RESPONSE);
httpLookups.push({
method: "GET",
- path: "/events",
- data: eventData,
+ path: "/sync",
+ data: SYNC_DATA,
expectQueryParams: {
room_id: roomIds
},