Merge branch 'develop' into matthew/room-tags
This commit is contained in:
+181
-57
@@ -151,7 +151,8 @@ function MatrixClient(opts) {
|
||||
setupCallEventHandler(this);
|
||||
this._supportsVoip = true;
|
||||
}
|
||||
|
||||
this._syncState = null;
|
||||
this._syncingRetry = null;
|
||||
}
|
||||
utils.inherits(MatrixClient, EventEmitter);
|
||||
|
||||
@@ -179,6 +180,32 @@ MatrixClient.prototype.supportsVoip = function() {
|
||||
return this._supportsVoip;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current sync state.
|
||||
* @return {?string} the sync state, which may be null.
|
||||
* @see module:client~MatrixClient#event:"sync"
|
||||
*/
|
||||
MatrixClient.prototype.getSyncState = function() {
|
||||
return this._syncState;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retry a backed off syncing request immediately. This should only be used when
|
||||
* the user <b>explicitly</b> attempts to retry their lost connection.
|
||||
* @return {boolean} True if this resulted in a request being retried.
|
||||
*/
|
||||
MatrixClient.prototype.retryImmediately = function() {
|
||||
if (!this._syncingRetry) {
|
||||
return false;
|
||||
}
|
||||
// stop waiting
|
||||
clearTimeout(this._syncingRetry.timeoutId);
|
||||
// invoke immediately
|
||||
this._syncingRetry.fn();
|
||||
this._syncingRetry = null;
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Is end-to-end crypto enabled for this client.
|
||||
* @return {boolean} True if end-to-end is enabled.
|
||||
@@ -991,6 +1018,9 @@ function _sendEvent(client, room, event, callback) {
|
||||
// the fake event we made above. If we don't find it, we're still
|
||||
// waiting on the real event and so should assign the fake event
|
||||
// with the real event_id for matching later.
|
||||
|
||||
// FIXME: This manipulation of the room should probably be done
|
||||
// inside the room class, not by the client.
|
||||
var matchingEvent = utils.findElement(room.timeline, function(ev) {
|
||||
return ev.getId() === eventId;
|
||||
}, true);
|
||||
@@ -1003,13 +1033,13 @@ function _sendEvent(client, room, event, callback) {
|
||||
matchingEvent.event.content = event.event.content;
|
||||
matchingEvent.event.type = event.event.type;
|
||||
}
|
||||
utils.removeElement(room.timeline, function(ev) {
|
||||
return ev.getId() === event.getId();
|
||||
}, true);
|
||||
room.removeEvents([event.getId()]);
|
||||
}
|
||||
else {
|
||||
room.removeEvents([event.getId()]);
|
||||
event.event.event_id = res.event_id;
|
||||
event.status = null;
|
||||
room.addEventsToTimeline([event]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1675,7 +1705,7 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) {
|
||||
var matrixEvents = utils.map(res.chunk, _PojoToMatrixEventMapper(self));
|
||||
room.addEventsToTimeline(matrixEvents, true);
|
||||
room.oldState.paginationToken = res.end;
|
||||
if (res.chunk.length < limit) {
|
||||
if (res.chunk.length === 0) {
|
||||
room.oldState.paginationToken = null;
|
||||
}
|
||||
self.store.storeEvents(room, matrixEvents, res.end, true);
|
||||
@@ -1756,27 +1786,25 @@ MatrixClient.prototype.loginWithSAML2 = function(relayState, callback) {
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
* @param {string} redirectUrl The URL to redirect to after the HS
|
||||
* authenticates with CAS.
|
||||
* @return {string} The HS URL to hit to begin the CAS login process.
|
||||
*/
|
||||
MatrixClient.prototype.getCasServer = function(callback) {
|
||||
return this._http.request(
|
||||
callback, "GET", "/login/cas", undefined, undefined
|
||||
);
|
||||
MatrixClient.prototype.getCasLoginUrl = function(redirectUrl) {
|
||||
return this._http.getUrl("/login/cas/redirect", {
|
||||
"redirectUrl": redirectUrl
|
||||
}, httpApi.PREFIX_V1);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} ticket (Received from CAS)
|
||||
* @param {string} service Service to which the token was granted
|
||||
* @param {string} token Login token previously received from homeserver
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.loginWithCas = function(ticket, service, callback) {
|
||||
return this.login("m.login.cas", {
|
||||
ticket: ticket,
|
||||
service: service
|
||||
MatrixClient.prototype.loginWithToken = function(token, callback) {
|
||||
return this.login("m.login.token", {
|
||||
token: token
|
||||
}, callback);
|
||||
};
|
||||
|
||||
@@ -1915,8 +1943,10 @@ MatrixClient.prototype.isLoggedIn = function() {
|
||||
* @param {MatrixClient} client
|
||||
* @param {integer} historyLen
|
||||
* @param {integer} includeArchived
|
||||
* @param {integer} attempt
|
||||
*/
|
||||
function doInitialSync(client, historyLen, includeArchived) {
|
||||
function doInitialSync(client, historyLen, includeArchived, attempt) {
|
||||
attempt = attempt || 1;
|
||||
var qps = { limit: historyLen };
|
||||
if (includeArchived) {
|
||||
qps.archived = true;
|
||||
@@ -2024,12 +2054,17 @@ function doInitialSync(client, historyLen, includeArchived) {
|
||||
}
|
||||
|
||||
client.clientRunning = true;
|
||||
client.emit("syncComplete");
|
||||
updateSyncState(client, "PREPARED");
|
||||
// assume success until we fail which may be 30+ secs
|
||||
updateSyncState(client, "SYNCING");
|
||||
_pollForEvents(client);
|
||||
}, function(err) {
|
||||
console.error("/initialSync error: %s", err);
|
||||
client.emit("syncError", err);
|
||||
// TODO: Retries.
|
||||
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 });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2046,6 +2081,8 @@ function doInitialSync(client, historyLen, includeArchived) {
|
||||
* @param {Boolean} opts.resolveInvitesToProfiles True to do /profile requests
|
||||
* on every invite event if the displayname/avatar_url is not known for this user ID.
|
||||
* Default: false.
|
||||
* @param {Number} opts.pollTimeout The number of milliseconds to wait on /events.
|
||||
* Default: 30000 (30 seconds).
|
||||
*/
|
||||
MatrixClient.prototype.startClient = function(opts) {
|
||||
if (this.clientRunning) {
|
||||
@@ -2063,6 +2100,7 @@ MatrixClient.prototype.startClient = function(opts) {
|
||||
opts.initialSyncLimit = opts.initialSyncLimit || 8;
|
||||
opts.includeArchivedRooms = opts.includeArchivedRooms || false;
|
||||
opts.resolveInvitesToProfiles = opts.resolveInvitesToProfiles || false;
|
||||
opts.pollTimeout = opts.pollTimeout || (30 * 1000);
|
||||
this._config = opts;
|
||||
|
||||
if (CRYPTO_ENABLED && this.sessionStore !== null) {
|
||||
@@ -2078,20 +2116,34 @@ MatrixClient.prototype.startClient = function(opts) {
|
||||
// periodically poll for turn servers if we support voip
|
||||
checkTurnServers(this);
|
||||
|
||||
var self = this;
|
||||
this.pushRules().done(function(result) {
|
||||
self.pushRules = result;
|
||||
doInitialSync(self, opts.initialSyncLimit, opts.includeArchivedRooms);
|
||||
}, function(err) {
|
||||
self.emit("syncError", err);
|
||||
});
|
||||
prepareForSync(this);
|
||||
};
|
||||
|
||||
function prepareForSync(client, attempt) {
|
||||
attempt = attempt || 1;
|
||||
client.pushRules().done(function(result) {
|
||||
client.pushRules = result;
|
||||
doInitialSync(
|
||||
client,
|
||||
client._config.initialSyncLimit,
|
||||
client._config.includeArchivedRooms
|
||||
);
|
||||
}, function(err) {
|
||||
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) {
|
||||
function _pollForEvents(client, attempt) {
|
||||
attempt = attempt || 1;
|
||||
var self = client;
|
||||
if (!client.clientRunning) {
|
||||
return;
|
||||
@@ -2101,11 +2153,11 @@ function _pollForEvents(client) {
|
||||
discardResult = true;
|
||||
console.error("/events request timed out.");
|
||||
_pollForEvents(client);
|
||||
}, 40000);
|
||||
}, client._config.pollTimeout + (20 * 1000)); // 20s buffer
|
||||
|
||||
client._http.authedRequest(undefined, "GET", "/events", {
|
||||
from: client.store.getSyncToken(),
|
||||
timeout: 30000
|
||||
timeout: client._config.pollTimeout
|
||||
}).done(function(data) {
|
||||
if (discardResult) {
|
||||
return;
|
||||
@@ -2113,6 +2165,11 @@ function _pollForEvents(client) {
|
||||
else {
|
||||
clearTimeout(timeoutObj);
|
||||
}
|
||||
|
||||
if (self._syncState !== "SYNCING") {
|
||||
updateSyncState(self, "SYNCING");
|
||||
}
|
||||
|
||||
try {
|
||||
var events = [];
|
||||
if (data) {
|
||||
@@ -2208,12 +2265,12 @@ function _pollForEvents(client) {
|
||||
else {
|
||||
clearTimeout(timeoutObj);
|
||||
}
|
||||
self.emit("syncError", err);
|
||||
// retry every few seconds
|
||||
// FIXME: this should be exponential backoff with an option to nudge
|
||||
setTimeout(function() {
|
||||
_pollForEvents(self);
|
||||
}, 2000);
|
||||
|
||||
attempt += 1;
|
||||
startSyncingRetryTimer(self, attempt, function() {
|
||||
_pollForEvents(self, attempt);
|
||||
});
|
||||
updateSyncState(self, "ERROR", { error: err });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2223,7 +2280,8 @@ function _syncRoom(client, room) {
|
||||
}
|
||||
var defer = q.defer();
|
||||
client._syncingRooms[room.roomId] = defer.promise;
|
||||
client.roomInitialSync(room.roomId, 8).done(function(res) {
|
||||
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);
|
||||
@@ -2492,6 +2550,20 @@ 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;
|
||||
@@ -2527,7 +2599,7 @@ function createNewUser(client, userId) {
|
||||
|
||||
function createNewRoom(client, roomId) {
|
||||
var room = new Room(roomId);
|
||||
reEmit(client, room, ["Room.name", "Room.tags", "Room.timeline"]);
|
||||
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
|
||||
@@ -2548,6 +2620,12 @@ function createNewRoom(client, roomId) {
|
||||
return room;
|
||||
}
|
||||
|
||||
function retryTimeMsForAttempt(attempt) {
|
||||
// 2,4,8,16,32,64,128,128,128,... seconds
|
||||
// max 2^7 secs = 2.1 mins
|
||||
return Math.pow(2, Math.min(attempt, 7)) * 1000;
|
||||
}
|
||||
|
||||
function _reject(callback, defer, err) {
|
||||
if (callback) {
|
||||
callback(err);
|
||||
@@ -2635,23 +2713,69 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever the SDK has a problem syncing. <strong>This event is experimental
|
||||
* and may change.</strong>
|
||||
* @event module:client~MatrixClient#"syncError"
|
||||
* @param {MatrixError} err The matrix error which caused this event to fire.
|
||||
* Fires whenever the SDK's syncing state is updated. The state can be one of:
|
||||
* <ul>
|
||||
* <li>PREPARED : The client has synced with the server at least once and is
|
||||
* ready for methods to be called on it. This will be immediately followed by
|
||||
* a state of SYNCING. <i>This is the equivalent of "syncComplete" in the
|
||||
* previous API.</i></li>
|
||||
* <li>SYNCING : The client is currently polling for new events from the server.
|
||||
* The client may fire this before or after processing latest events from a sync.</li>
|
||||
* <li>ERROR : The client has had a problem syncing with the server. If this is
|
||||
* called <i>before</i> PREPARED then there was a problem performing the initial
|
||||
* sync. If this is called <i>after</i> PREPARED then there was a problem polling
|
||||
* the server for updates. This may be called multiple times even if the state is
|
||||
* already ERROR. <i>This is the equivalent of "syncError" in the previous
|
||||
* API.</i></li>
|
||||
* </ul>
|
||||
* State transition diagram:
|
||||
* <pre>
|
||||
* +----->PREPARED -------> SYNCING <--+
|
||||
* | ^ | |
|
||||
* null ------+ | +---------------+ |
|
||||
* | | V |
|
||||
* +------->ERROR ---------------------+
|
||||
*
|
||||
* NB: 'null' will never be emitted by this event.
|
||||
* </pre>
|
||||
* Transitions:
|
||||
* <ul>
|
||||
* <li><code>null -> PREPARED</code> : Occurs when the initial sync is completed
|
||||
* first time.
|
||||
* <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.
|
||||
* <li><code>PREPARED -> SYNCING</code> : Occurs immediately after transitioning
|
||||
* to PREPARED. Starts listening for live updates rather than catching up.
|
||||
* <li><code>SYNCING -> ERROR</code> : Occurs the first time a client cannot perform a
|
||||
* live update.
|
||||
* <li><code>ERROR -> SYNCING</code> : Occurs when the client has performed a
|
||||
* live update after having previously failed.
|
||||
* <li><code>ERROR -> ERROR</code> : Occurs when the client has failed to sync
|
||||
* for a second time or more.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @event module:client~MatrixClient#"sync"
|
||||
* @param {string} state An enum representing the syncing state. One of "PREPARED",
|
||||
* "SYNCING", "ERROR".
|
||||
* @param {?string} prevState An enum representing the previous syncing state.
|
||||
* One of "PREPARED", "SYNCING", "ERROR" <b>or null</b>.
|
||||
* @param {?Object} data Data about this transition.
|
||||
* @param {MatrixError} data.err The matrix error if <code>state=ERROR</code>.
|
||||
* @example
|
||||
* matrixClient.on("syncError", function(err){
|
||||
* // update UI to say "Connection Lost"
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires when the SDK has finished catching up and is now listening for live
|
||||
* events. <strong>This event is experimental and may change.</strong>
|
||||
* @event module:client~MatrixClient#"syncComplete"
|
||||
* @example
|
||||
* matrixClient.on("syncComplete", function(){
|
||||
* var rooms = matrixClient.getRooms();
|
||||
* matrixClient.on("sync", function(state, prevState, data) {
|
||||
* switch (state) {
|
||||
* case "ERROR":
|
||||
* // update UI to say "Connection Lost"
|
||||
* break;
|
||||
* case "SYNCING":
|
||||
* // update UI to remove any "Connection Lost" message
|
||||
* break;
|
||||
* case "PREPARED":
|
||||
* // the client instance is ready to be queried.
|
||||
* var rooms = matrixClient.getRooms();
|
||||
* break;
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
|
||||
|
||||
@@ -296,6 +296,25 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
return this._request(callback, method, fullUri, queryParams, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Form and return a homeserver request URL based on the given path
|
||||
* params and prefix.
|
||||
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
|
||||
* "/createRoom".
|
||||
* @param {Object} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded).
|
||||
* @param {string} prefix The full prefix to use e.g.
|
||||
* "/_matrix/client/v2_alpha".
|
||||
* @return {string} URL
|
||||
*/
|
||||
getUrl: function(path, queryParams, prefix) {
|
||||
var queryString = "";
|
||||
if (queryParams) {
|
||||
queryString = "?" + utils.encodeParams(queryParams);
|
||||
}
|
||||
return this.opts.baseUrl + prefix + path + queryString;
|
||||
},
|
||||
|
||||
_request: function(callback, method, uri, queryParams, data) {
|
||||
if (callback !== undefined && !utils.isFunction(callback)) {
|
||||
throw Error(
|
||||
|
||||
+60
-3
@@ -200,7 +200,26 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline) {
|
||||
else {
|
||||
this.timeline.push(events[i]);
|
||||
}
|
||||
this.emit("Room.timeline", events[i], this, Boolean(toStartOfTimeline));
|
||||
|
||||
// synthesize and inject implicit read receipts
|
||||
// Done after adding the event because otherwise the app would get a read receipt
|
||||
// pointing to an event that wasn't yet in the timeline
|
||||
|
||||
// This is really ugly because JS has no way to express an object literal
|
||||
// where the name of a key comes from an expression
|
||||
if (events[i].sender) {
|
||||
var fakeReceipt = {content: {}};
|
||||
fakeReceipt.content[events[i].getId()] = {
|
||||
'm.read': {
|
||||
}
|
||||
};
|
||||
fakeReceipt.content[events[i].getId()]['m.read'][events[i].sender.userId] = {
|
||||
ts: events[i].getTs()
|
||||
};
|
||||
this.addReceipt(new MatrixEvent(fakeReceipt));
|
||||
}
|
||||
|
||||
this.emit("Room.timeline", events[i], this, Boolean(toStartOfTimeline), false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -269,6 +288,29 @@ Room.prototype.addEvents = function(events, duplicateStrategy) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes events from this room.
|
||||
* @param {String} event_ids A list of event_ids to remove.
|
||||
*/
|
||||
Room.prototype.removeEvents = function(event_ids) {
|
||||
// avoids defining a function in the loop, which is a lint error
|
||||
function removeEventWithId(timeline, id) {
|
||||
// NB. we supply reverse to search from the end,
|
||||
// on the assumption that recents events are much
|
||||
// more likley to be removed than older ones.
|
||||
return utils.removeElement(timeline, function(e) {
|
||||
return e.getId() == id;
|
||||
}, true);
|
||||
}
|
||||
|
||||
for (var i = 0; i < event_ids.length; ++i) {
|
||||
var removed = removeEventWithId(this.timeline, event_ids[i]);
|
||||
if (removed) {
|
||||
this.emit("Room.timeline", removed, this, undefined, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Recalculate various aspects of the room, including the room name and
|
||||
* room summary. Call this any time the room's current state is modified.
|
||||
@@ -335,7 +377,6 @@ Room.prototype.getUsersReadUpTo = function(event) {
|
||||
* have received no read receipts from them.
|
||||
* @param {String} userId The user ID to get read receipt event ID for
|
||||
* @return {String} ID of the latest event that the given user has read, or null.
|
||||
* an empty list.
|
||||
*/
|
||||
Room.prototype.getEventReadUpTo = function(userId) {
|
||||
if (
|
||||
@@ -408,6 +449,10 @@ Room.prototype.addReceipt = function(event) {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// send events after we've regenerated the cache, otherwise things that
|
||||
// listened for the event would read from a stale cache
|
||||
this.emit("Room.receipt", event, this);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -464,7 +509,7 @@ function calculateRoomName(room, userId) {
|
||||
// check for an alias, if any. for now, assume first alias is the
|
||||
// official one.
|
||||
var mRoomName = room.currentState.getStateEvents("m.room.name", "");
|
||||
if (mRoomName) {
|
||||
if (mRoomName && mRoomName.getContent() && mRoomName.getContent().name) {
|
||||
return mRoomName.getContent().name;
|
||||
}
|
||||
|
||||
@@ -539,6 +584,7 @@ module.exports = Room;
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {Room} room The room whose Room.timeline was updated.
|
||||
* @param {boolean} toStartOfTimeline True if this event was added to the start
|
||||
* @param {boolean} removed True if this event has just been removed from the timeline
|
||||
* (beginning; oldest) of the timeline e.g. due to pagination.
|
||||
* @example
|
||||
* matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline){
|
||||
@@ -558,6 +604,17 @@ module.exports = Room;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever a receipt is received for a room
|
||||
* @event module:client~MatrixClient#"Room.receipt"
|
||||
* @param {event} event The receipt event
|
||||
* @param {Room} room The room whose receipts was updated.
|
||||
* @example
|
||||
* matrixClient.on("Room.receipt", function(event, room){
|
||||
* var receiptContent = event.getContent();
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever a room's tags are updated.
|
||||
* @event module:client~MatrixClient#"Room.tags"
|
||||
|
||||
@@ -133,6 +133,12 @@ MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) {
|
||||
// client error; no amount of retrying with save you now.
|
||||
return -1;
|
||||
}
|
||||
// we ship with browser-request which returns { cors: rejected } when trying
|
||||
// with no connection, so if we match that, give up since they have no conn.
|
||||
if (err.cors === "rejected") {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (err.name === "M_LIMIT_EXCEEDED") {
|
||||
var waitTime = err.data.retry_after_ms;
|
||||
if (waitTime) {
|
||||
|
||||
+5
-2
@@ -152,19 +152,22 @@ module.exports.findElement = function(array, fn, reverse) {
|
||||
*/
|
||||
module.exports.removeElement = function(array, fn, reverse) {
|
||||
var i;
|
||||
var removed;
|
||||
if (reverse) {
|
||||
for (i = array.length - 1; i >= 0; i--) {
|
||||
if (fn(array[i], i, array)) {
|
||||
removed = array[i];
|
||||
array.splice(i, 1);
|
||||
return true;
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (i = 0; i < array.length; i++) {
|
||||
if (fn(array[i], i, array)) {
|
||||
removed = array[i];
|
||||
array.splice(i, 1);
|
||||
return true;
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,8 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
it("should be added immediately after calling MatrixClient.sendEvent " +
|
||||
"with EventStatus.SENDING and the right event.sender", function(done) {
|
||||
client.on("syncComplete", function() {
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
@@ -116,7 +117,8 @@ describe("MatrixClient room timelines", function() {
|
||||
];
|
||||
eventData.chunk[0].event_id = eventId;
|
||||
|
||||
client.on("syncComplete", function() {
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
client.sendTextMessage(roomId, "I am a fish", "txn1").done(
|
||||
function() {
|
||||
@@ -144,7 +146,8 @@ describe("MatrixClient room timelines", function() {
|
||||
];
|
||||
eventData.chunk[0].event_id = eventId;
|
||||
|
||||
client.on("syncComplete", function() {
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
var promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
httpBackend.flush("/events", 1).done(function() {
|
||||
@@ -180,7 +183,8 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
it("should set Room.oldState.paginationToken to null at the start" +
|
||||
" of the timeline.", function(done) {
|
||||
client.on("syncComplete", function() {
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
@@ -219,7 +223,8 @@ describe("MatrixClient room timelines", function() {
|
||||
})
|
||||
];
|
||||
|
||||
client.on("syncComplete", function() {
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
@@ -249,7 +254,8 @@ describe("MatrixClient room timelines", function() {
|
||||
})
|
||||
];
|
||||
|
||||
client.on("syncComplete", function() {
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
@@ -274,7 +280,8 @@ describe("MatrixClient room timelines", function() {
|
||||
})
|
||||
];
|
||||
|
||||
client.on("syncComplete", function() {
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
expect(room.oldState.paginationToken).toBeDefined();
|
||||
|
||||
@@ -297,7 +304,8 @@ describe("MatrixClient room timelines", function() {
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({user: userId, room: roomId})
|
||||
];
|
||||
client.on("syncComplete", function() {
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
|
||||
var index = 0;
|
||||
@@ -331,7 +339,8 @@ describe("MatrixClient room timelines", function() {
|
||||
}),
|
||||
utils.mkMessage({user: userId, room: roomId})
|
||||
];
|
||||
client.on("syncComplete", function() {
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
httpBackend.flush("/events", 1).done(function() {
|
||||
var preNameEvent = room.timeline[room.timeline.length - 3];
|
||||
@@ -352,7 +361,8 @@ describe("MatrixClient room timelines", function() {
|
||||
}
|
||||
})
|
||||
];
|
||||
client.on("syncComplete", function() {
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
var nameEmitCount = 0;
|
||||
client.on("Room.name", function(rm) {
|
||||
@@ -392,7 +402,8 @@ describe("MatrixClient room timelines", function() {
|
||||
user: userC, room: roomId, mship: "invite", skey: userD
|
||||
})
|
||||
];
|
||||
client.on("syncComplete", function() {
|
||||
client.on("sync", function(state) {
|
||||
if (state !== "PREPARED") { return; }
|
||||
var room = client.getRoom(roomId);
|
||||
httpBackend.flush("/events", 1).done(function() {
|
||||
expect(room.currentState.getMembers().length).toEqual(4);
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
"use strict";
|
||||
var q = require("q");
|
||||
var sdk = require("../..");
|
||||
var MatrixClient = sdk.MatrixClient;
|
||||
var utils = require("../test-utils");
|
||||
|
||||
describe("MatrixClient", function() {
|
||||
var userId = "@alice:bar";
|
||||
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: {}
|
||||
};
|
||||
|
||||
var httpLookups = [
|
||||
// items are objects which look like:
|
||||
// {
|
||||
// method: "GET",
|
||||
// path: "/initialSync",
|
||||
// data: {},
|
||||
// error: { errcode: M_FORBIDDEN } // if present will reject promise
|
||||
// }
|
||||
// items are popped off when processed and block if no items left.
|
||||
];
|
||||
var pendingLookup = null;
|
||||
function httpReq(cb, method, path, qp, data, prefix) {
|
||||
var next = httpLookups.shift();
|
||||
var logLine = (
|
||||
"MatrixClient[UT] RECV " + method + " " + path + " " +
|
||||
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
|
||||
);
|
||||
console.log(logLine);
|
||||
|
||||
if (!next) { // no more things to return
|
||||
if (pendingLookup) {
|
||||
if (pendingLookup.method === method && pendingLookup.path === path) {
|
||||
return pendingLookup.promise;
|
||||
}
|
||||
// >1 pending thing, and they are different, whine.
|
||||
expect(false).toBe(
|
||||
true, ">1 pending request. You should probably handle them. " +
|
||||
"PENDING: " + JSON.stringify(pendingLookup) + " JUST GOT: " +
|
||||
method + " " + path
|
||||
);
|
||||
}
|
||||
pendingLookup = {
|
||||
promise: q.defer().promise,
|
||||
method: method,
|
||||
path: path
|
||||
};
|
||||
return pendingLookup.promise;
|
||||
}
|
||||
if (next.path === path && next.method === method) {
|
||||
console.log(
|
||||
"MatrixClient[UT] Matched. Returning " +
|
||||
(next.error ? "BAD" : "GOOD") + " response"
|
||||
);
|
||||
if (next.error) {
|
||||
return q.reject({
|
||||
errcode: next.error.errcode,
|
||||
name: next.error.errcode,
|
||||
message: "Expected testing error",
|
||||
data: next.error
|
||||
});
|
||||
}
|
||||
return q(next.data);
|
||||
}
|
||||
expect(true).toBe(false, "Expected different request. " + logLine);
|
||||
return q.defer().promise;
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
jasmine.Clock.useMock();
|
||||
scheduler = jasmine.createSpyObj("scheduler", [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
"setProcessFunction"
|
||||
]);
|
||||
store = jasmine.createSpyObj("store", [
|
||||
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
|
||||
"setSyncToken", "storeEvents", "storeRoom", "storeUser"
|
||||
]);
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
request: function() {}, // NOP
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: userId
|
||||
});
|
||||
// FIXME: We shouldn't be yanking _http like this.
|
||||
client._http = jasmine.createSpyObj("httpApi", [
|
||||
"authedRequest", "authedRequestWithPrefix", "getContentUri",
|
||||
"request", "requestWithPrefix", "uploadContent"
|
||||
]);
|
||||
client._http.authedRequest.andCallFake(httpReq);
|
||||
client._http.authedRequestWithPrefix.andCallFake(httpReq);
|
||||
|
||||
// set reasonable working defaults
|
||||
pendingLookup = null;
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/initialSync", data: initialSyncData
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
// need to re-stub the requests with NOPs because there are no guarantees
|
||||
// clients from previous tests will be GC'd before the next test. This
|
||||
// means they may call /events and then fail an expect() which will fail
|
||||
// a DIFFERENT test (pollution between tests!) - we return unresolved
|
||||
// promises to stop the client from continuing to run.
|
||||
client._http.authedRequest.andCallFake(function() {
|
||||
return q.defer().promise;
|
||||
});
|
||||
client._http.authedRequestWithPrefix.andCallFake(function() {
|
||||
return q.defer().promise;
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSyncState", function() {
|
||||
|
||||
it("should return null if the client isn't started", function() {
|
||||
expect(client.getSyncState()).toBeNull();
|
||||
});
|
||||
|
||||
it("should return the same sync state as emitted sync events", function(done) {
|
||||
client.on("sync", function(state) {
|
||||
expect(state).toEqual(client.getSyncState());
|
||||
if (state === "SYNCING") {
|
||||
done();
|
||||
}
|
||||
});
|
||||
client.startClient();
|
||||
});
|
||||
});
|
||||
|
||||
describe("retryImmediately", function() {
|
||||
it("should return false if there is no request waiting", function() {
|
||||
client.startClient();
|
||||
expect(client.retryImmediately()).toBe(false);
|
||||
});
|
||||
|
||||
it("should work on /initialSync", function(done) {
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/initialSync", error: { errcode: "NOPE_NOPE_NOPE" }
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/initialSync", error: { errcode: "NOPE_NOPE_NOPE" }
|
||||
});
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(1);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
done();
|
||||
}
|
||||
});
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should work on /events", function(done) {
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/events", error: { errcode: "NOPE_NOPE_NOPE" }
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/events", data: eventData
|
||||
});
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(1);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
done();
|
||||
}
|
||||
});
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should work on /pushrules", function(done) {
|
||||
httpLookups = [];
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" }
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" }
|
||||
});
|
||||
|
||||
client.on("sync", function(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(1);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
done();
|
||||
}
|
||||
});
|
||||
client.startClient();
|
||||
});
|
||||
});
|
||||
|
||||
describe("emitted sync events", function() {
|
||||
var expectedStates;
|
||||
|
||||
function syncChecker(done) {
|
||||
return function(state, old) {
|
||||
var expected = expectedStates.shift();
|
||||
console.log(
|
||||
"'sync' curr=%s old=%s EXPECT=%s", state, old, expected
|
||||
);
|
||||
if (!expected) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
expect(state).toEqual(expected[0]);
|
||||
expect(old).toEqual(expected[1]);
|
||||
if (expectedStates.length === 0) {
|
||||
done();
|
||||
}
|
||||
// standard retry time is 4s
|
||||
jasmine.Clock.tick(4001);
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
expectedStates = [
|
||||
// [current, old]
|
||||
];
|
||||
});
|
||||
|
||||
it("should transition null -> PREPARED after /initialSync", function(done) {
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
client.on("sync", syncChecker(done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition null -> ERROR after a failed /initialSync", function(done) {
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/initialSync", error: { errcode: "NOPE_NOPE_NOPE" }
|
||||
});
|
||||
expectedStates.push(["ERROR", null]);
|
||||
client.on("sync", syncChecker(done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition ERROR -> PREPARED after /initialSync if prev failed",
|
||||
function(done) {
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/initialSync", error: { errcode: "NOPE_NOPE_NOPE" }
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/initialSync", data: initialSyncData
|
||||
});
|
||||
|
||||
expectedStates.push(["ERROR", null]);
|
||||
expectedStates.push(["PREPARED", "ERROR"]);
|
||||
client.on("sync", syncChecker(done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition PREPARED -> SYNCING after /initialSync", function(done) {
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
client.on("sync", syncChecker(done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition SYNCING -> ERROR after a failed /events", function(done) {
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/events", error: { errcode: "NONONONONO" }
|
||||
});
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
client.on("sync", syncChecker(done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition ERROR -> SYNCING after /events if prev failed",
|
||||
function(done) {
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/events", error: { errcode: "NONONONONO" }
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/events", data: eventData
|
||||
});
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
client.on("sync", syncChecker(done));
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition ERROR -> ERROR if multiple /events fails", function(done) {
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/events", error: { errcode: "NONONONONO" }
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/events", 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.startClient();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -684,6 +684,21 @@ describe("Room", function() {
|
||||
}]);
|
||||
});
|
||||
|
||||
it("should emit an event when a receipt is added",
|
||||
function() {
|
||||
var listener = jasmine.createSpy('spy');
|
||||
room.on("Room.receipt", listener);
|
||||
|
||||
var ts = 13787898424;
|
||||
|
||||
var receiptEvent = mkReceipt(roomId, [
|
||||
mkRecord(eventToAck.getId(), "m.read", userB, ts)
|
||||
]);
|
||||
|
||||
room.addReceipt(receiptEvent);
|
||||
expect(listener).toHaveBeenCalledWith(receiptEvent, room);
|
||||
});
|
||||
|
||||
it("should clobber receipts based on type and user ID", function() {
|
||||
var nextEventToAck = utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "I AM HERE YOU KNOW",
|
||||
|
||||
Reference in New Issue
Block a user