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. *
  • SYNCING : The client is currently polling for new events from the server. - * The client may fire this before or after processing latest events from a sync.
  • + * This will be called after processing latest events from a sync. *
  • ERROR : The client has had a problem syncing with the server. If this is * called before PREPARED then there was a problem performing the initial * sync. If this is called after PREPARED then there was a problem polling @@ -3013,7 +2523,7 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * Transitions: * * * @event module:client~MatrixClient#"sync" diff --git a/lib/http-api.js b/lib/http-api.js index 6e487ef4a..1f4d42a43 100644 --- a/lib/http-api.js +++ b/lib/http-api.js @@ -242,6 +242,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 @@ -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 },