Merge pull request #53 from matrix-org/kegan/v2-sync

Use /sync instead of /initialSync and /events
This commit is contained in:
Kegsay
2015-12-15 14:24:26 +00:00
17 changed files with 1381 additions and 1093 deletions
+20 -508
View File
@@ -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. <i>This is the equivalent of "syncComplete" in the
* previous API.</i></li>
* <li>SYNCING : The client is currently polling for new events from the server.
* The client may fire this before or after processing latest events from a sync.</li>
* This will be called <i>after</i> processing latest events from a sync.</li>
* <li>ERROR : The client has had a problem syncing with the server. If this is
* called <i>before</i> PREPARED then there was a problem performing the initial
* sync. If this is called <i>after</i> PREPARED then there was a problem polling
@@ -3013,7 +2523,7 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
* Transitions:
* <ul>
* <li><code>null -> PREPARED</code> : Occurs when the initial sync is completed
* first time.
* first time. This involves setting up filters and obtaining push rules.
* <li><code>null -> ERROR</code> : Occurs when the initial sync failed first time.
* <li><code>ERROR -> PREPARED</code> : Occurs when the initial sync succeeds
* after previously failing.
@@ -3025,6 +2535,8 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
* live update after having previously failed.
* <li><code>ERROR -> ERROR</code> : Occurs when the client has failed to sync
* for a second time or more.</li>
* <li><code>SYNCING -> SYNCING</code> : Occurs when the client has performed a live
* update. This is called <i>after</i> processing.</li>
* </ul>
*
* @event module:client~MatrixClient#"sync"
+47 -10
View File
@@ -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 <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
@@ -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 <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
@@ -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 <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
@@ -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 <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
@@ -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) {
+3 -1
View File
@@ -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);
};
+6 -2
View File
@@ -63,7 +63,7 @@ module.exports.MatrixEvent.prototype = {
* @return {string} The user ID, e.g. <code>@alice:matrix.org</code>
*/
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 || {};
}
};
+18
View File
@@ -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()) {
+1 -1
View File
@@ -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);
+38 -1
View File
@@ -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
+18
View File
@@ -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
+514
View File
@@ -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;
}
/**
* <b>Internal class - unstable.</b>
* 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;
+68
View File
@@ -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
};
}
};
/**
+19 -14
View File
@@ -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({
+96 -89
View File
@@ -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;
+42 -44
View File
@@ -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
+159 -101
View File
@@ -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);
});
});
});
+216 -252
View File
@@ -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();
+2 -2
View File
@@ -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,
+114 -68
View File
@@ -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
},