Merge pull request #53 from matrix-org/kegan/v2-sync
Use /sync instead of /initialSync and /events
This commit is contained in:
+20
-508
@@ -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
@@ -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
@@ -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
@@ -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 || {};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user