Compare commits

...

80 Commits

Author SHA1 Message Date
Richard van der Hoff 2c40932080 0.5.2 2016-04-19 13:10:12 +01:00
Richard van der Hoff 3777b3e211 Prepare changelog for v0.5.2 2016-04-19 13:09:56 +01:00
Matthew Hodgson 2f7d7308a1 Merge pull request #128 from matrix-org/matthew/syjs-28
Track the absolute time that presence events are received, so that the relative lastActiveAgo value is meaningful.
2016-04-18 19:15:03 +01:00
Matthew Hodgson 0e606c6fe2 incorporate PR feedback 2016-04-18 14:26:59 +01:00
Matthew Hodgson 3af35c8209 Merge branch 'develop' into matthew/syjs-28 2016-04-18 01:34:11 +01:00
Matthew Hodgson a2aed99f56 track lastPresenceTs 2016-04-18 01:25:34 +01:00
Richard van der Hoff 523a684d3f Merge pull request #127 from matrix-org/rav/refactor_add_events
Refactor the addition of events to rooms
2016-04-17 18:11:02 +01:00
Richard van der Hoff dc386bab46 Fix debug flag name 2016-04-14 17:53:57 +01:00
Richard van der Hoff 69079a2f9a A handy hook script 2016-04-14 17:41:52 +01:00
Richard van der Hoff df33f7aceb Fix lint failures 2016-04-14 17:36:25 +01:00
Richard van der Hoff d87e5471fa Refactor the addition of events to rooms
... and add some sanity checks

Two things here:

1. Clean up the Room API for adding new events to the timeline. Where before
we had addEvents and addEventsToTimeline, whose purposes were unclear, we now
have addLiveEvents which must be used for adding events to the end of the live
timeline, and addEventsToTimeline which should be used for pagination (either
back-pagination of the live timeline, or pagination of an old timeline).

2. Add some sanity checks for the live timeline. Today we have seen problems
where somehow the live timeline had gained a forward pagination token, or the
live timeline had got joined to another timeline, leading to much confusion -
and I would like to notice these sooner.
2016-04-14 17:03:25 +01:00
Richard van der Hoff 90101c0340 Give timelines a name
The debug of "joined timeline [Object] to timeline [Object]" isn't terribly
helpful, so let's at least give them an identifier.
2016-04-14 16:11:48 +01:00
Richard van der Hoff 950fce80c8 Whitespace fix
remove trailing ws
2016-04-14 16:09:51 +01:00
Richard van der Hoff 9135c50b83 Merge pull request #126 from matrix-org/rav/clean_up_testexit
Clean up test shutdown
2016-04-14 15:54:56 +01:00
Richard van der Hoff 83bad6ee0d Fix lint error
semicolons rool
2016-04-14 13:52:34 +01:00
Richard van der Hoff 3404751eb9 Clean up test shutdown
Make sure that the integration tests actually kill off all of their timers, so
that jasmine exits cleanly.

This probably also fixes https://github.com/vector-im/vector-web/issues/1365.
2016-04-14 12:01:23 +01:00
Richard van der Hoff 0282021e09 Add a cachebuster to initial /sync
... in the hope of fending off weird firefox restore issues
2016-04-13 22:17:10 +01:00
Richard van der Hoff 526e1d59e9 Log sync token when starting sync loop
A little bit of debug that might help with
https://github.com/vector-im/vector-web/issues/1354
2016-04-13 14:08:59 +01:00
David Baker dbc3a9a500 Merge pull request #125 from matrix-org/dbkr/get_pushers
Add methods to get (and set) pushers
2016-04-12 13:25:17 +01:00
David Baker cff7c8a59f Add methods to get (and set) pushers 2016-04-12 13:11:00 +01:00
Matthew Hodgson 64b8047f01 Merge pull request #122 from matrix-org/matthew/preview_urls
URL previewing support
2016-04-11 17:35:53 +01:00
Matthew Hodgson 11e4760935 improve commentary 2016-04-08 22:00:07 +01:00
Richard van der Hoff c21d5634bb Merge pull request #124 from matrix-org/rav/limit_pagination
Avoid paginating forever in private rooms
2016-04-08 15:55:37 +01:00
Richard van der Hoff dc56d7f784 Fix typo
s/evnets/events/
2016-04-08 15:54:57 +01:00
Matthew Hodgson 1ff1064295 Merge branch 'develop' into matthew/preview_urls 2016-04-07 17:26:10 +01:00
Richard van der Hoff a493a0ddb3 Fix lint errors 2016-04-07 14:30:35 +01:00
Richard van der Hoff 7573171d05 Avoid paginating forever in private rooms
In TimelineWindow.paginate, keep a count of the number of API requests we have
made, and bail out if it gets too high, to ensure that we don't get stuck in a
loop of paginating right back to the start of the room.
2016-04-07 14:16:02 +01:00
Richard van der Hoff 989e7cf78b Merge pull request #123 from matrix-org/rav/no_recreate_filter
Fix a bug where we recreated sync filters
2016-04-06 18:35:01 +01:00
Richard van der Hoff 384c474800 fix failure of deepCompare to compare arrays
what a difference a character can make
2016-04-06 18:14:34 +01:00
Richard van der Hoff 1d2c705e13 Fix a bug where we recreated sync filters
Fix the object comparison used for client filters (JSON.stringify is
non-deterministic)
2016-04-06 15:56:32 +01:00
Matthew Hodgson c469ff4c8d oops, fix sig 2016-04-03 01:19:34 +01:00
Matthew Hodgson c7575f3f16 cache url preview results 2016-04-03 01:18:01 +01:00
Matthew Hodgson 415251dd70 WIP url previewing 2016-03-31 18:38:34 +01:00
Richard van der Hoff 458cc55dec Merge pull request #121 from matrix-org/rav/realtime_callbacks
Implement HTTP callbacks in realtime
2016-03-31 16:33:25 +01:00
Richard van der Hoff d2adb30ded Implement HTTP callbacks in realtime
Hopefully this will improve our recovery time after a laptop is suspended. The
idea is to treat the timeouts on the http apis as being in realtime, rather
than in elapsed time while the machine is awake.

To do this, we add a layer on top of window.setTimeout. We run a callback every
second, which then checks the wallclock time and runs any pending callbacks.
2016-03-31 13:51:18 +01:00
Richard van der Hoff bbe41aa7b4 0.5.1 2016-03-30 13:18:42 +01:00
Richard van der Hoff ece9391878 Prepare changelog for v0.5.1 2016-03-30 13:17:10 +01:00
David Baker 64640287cf Merge pull request #119 from matrix-org/dbkr/member_count_only_joined
Only count joined members for the member count in notifications.
2016-03-24 14:01:45 +00:00
David Baker 57f88b00ba d'oh, no es6 in js-sdk 2016-03-24 13:57:55 +00:00
David Baker e618ad9589 Only count joined members for the member count in notifications. Fixes https://github.com/vector-im/vector-web/issues/1067 2016-03-24 13:55:30 +00:00
Richard van der Hoff 69c4d7b66e Merge pull request #118 from matrix-org/dbkr/maysendevent
Add maySendEvent to match maySendStateEvent
2016-03-24 11:43:29 +00:00
David Baker d7b3b91eec Failed to remove extra param 2016-03-23 17:08:22 +00:00
David Baker 88cc63e2a2 Add maySendEvent to match maySendStateEvent. Make them use the same function internally. Also add convenience maySendMessage. Also tests. 2016-03-23 15:10:51 +00:00
Richard van der Hoff cdea96fa0a Release script tweaks
- be more helpful if update_changelog is not installed
- behave sanely if v is omitted on tag arg
2016-03-23 14:50:03 +00:00
Matthew Hodgson cfc10fa82d fix invite picker info again... 2016-03-23 12:03:56 +00:00
Richard van der Hoff 9d8973bf1f 0.5.0 2016-03-22 17:56:32 +00:00
Richard van der Hoff 7f95237e02 Prepare changelog for v0.5.0 2016-03-22 17:56:05 +00:00
Matthew Hodgson e1415d9829 Merge pull request #117 from matrix-org/matthew/roomlist
get/setRoomVisibility API
2016-03-22 12:30:18 +00:00
Richard van der Hoff 19a12b3c79 Merge pull request #115 from matrix-org/rav/txnid_clashes
Include a counter in generated transaction IDs
2016-03-22 12:20:02 +00:00
Matthew Hodgson bec41e4f94 incorporate review 2016-03-22 10:20:02 +00:00
Matthew Hodgson 5f177aeec4 get/setRoomVisibility API 2016-03-22 00:55:53 +00:00
Matthew Hodgson b422916452 add to existing users if present, to avoid destroying presence data 2016-03-21 18:15:55 +00:00
Matthew Hodgson 10378c0e7f Merge pull request #116 from matrix-org/matthew/fix-invite-picker-info
update store user metadata based on membership events rather than presence
2016-03-21 16:12:42 +00:00
Matthew Hodgson fba4d5fb0a Merge pull request #114 from matrix-org/matthew/stop-peeking
API to stop peeking
2016-03-21 16:12:29 +00:00
Matthew Hodgson 77101823f5 track kicked rooms correctly 2016-03-19 02:18:37 +00:00
Matthew Hodgson 15bc608368 presence no longer returns profile data, so we have to update our store's users based on membership events instead 2016-03-19 01:45:10 +00:00
Richard van der Hoff dfc4b34d09 Include a counter in generated transaction IDs
Fixes a flaky test which sometimes failed due to sending two events in the same
millisecond.
2016-03-18 21:32:15 +00:00
Richard van der Hoff ad9daecbd4 Pass the right options into SyncApi when peeking
When we peek into a room, we create its Room object. We need to make sure it is
created with the same options as we would if it were created via the /sync
calls.

Save the options passed in when startClient is called, and then pass them into
the SyncApi each time we create it.
2016-03-18 20:59:23 +00:00
Matthew Hodgson d29302716d oops 2016-03-18 19:25:22 +00:00
Matthew Hodgson 6c7d13f8ce API to stop peeking 2016-03-18 19:22:34 +00:00
Richard van der Hoff e15a2d138c Merge pull request #112 from matrix-org/rav/cancel_send
Support for cancelling pending events
2016-03-18 16:17:44 +00:00
Richard van der Hoff 8bc9c19278 Merge pull request #111 from matrix-org/rav/pending_event_list
Implement 'pendingEventList'
2016-03-18 16:17:30 +00:00
David Baker dd86fade11 Merge pull request #113 from matrix-org/dbkr/threepid_lookup
Add a method to the js sdk to look up 3pids on the ID server.
2016-03-18 15:54:43 +00:00
David Baker ba1991aa8f Add more docs :) 2016-03-18 15:54:19 +00:00
David Baker f4fd8d9ba6 Add a method to the js sdk to look up 3pids on the ID server. 2016-03-18 15:15:10 +00:00
Richard van der Hoff 02be0f659a Support for cancelling pending events
Implement client.cancelPendingEvent which will cancel queued or not_sent events
2016-03-17 22:15:46 +00:00
Richard van der Hoff c7be310bdf Fix addPendingEvent invocation in unit test 2016-03-17 22:10:40 +00:00
Richard van der Hoff 55d8f56f98 update docs 2016-03-17 17:53:20 +00:00
Richard van der Hoff ab35fff9e8 Implement 'pendingEventList'
The existing 'pendingEventOrdering'=='end' semantics had been substantially
broken by the introduction of timelines and gappy syncs: after a gappy
sync, pending events would get stuck in the old timeline section. (Part of
https://github.com/vector-im/vector-web/issues/1120).
2016-03-17 17:05:23 +00:00
Richard van der Hoff fdbc7a3112 Merge pull request #110 from matrix-org/rav/refactor_remote_echo
Refactor transmitted-messages code
2016-03-17 16:40:47 +00:00
Richard van der Hoff 3c6bd4774d Refactor transmitted-messages code
This is some preparatory work for fixing up the problems with te timeline
ordering of unsent messages
(https://github.com/vector-im/vector-web/issues/1120). The functional changes
here should be minimal (bar an extra `Room.localEchoUpdated` when the local
echo is first added to the timeline).

Give `MatrixClient.sendEvent` its own entry point `Room.addPendingMessage`
instead of pushing it through `addEventsToTimeline`; this considerably
simplifies the implementation of the latter and also means that we can contain
the `_txnId` ming to MatrixClient.

Move the code which deals with a successful `/send` response from
`MatrixClient` into `Room.updatePendingEvent`, since it involves fiddling with
the innards of the Room.

Also adds a new EventStatus 'SENT' for events which have been successfully sent
but whose remote echo we still haven't received.
2016-03-17 14:26:36 +00:00
Richard van der Hoff a2861c5781 Merge pull request #109 from matrix-org/rav/log_sync_error_stack
Log the stack when we get a sync error
2016-03-17 14:24:07 +00:00
Richard van der Hoff eaf3fe16eb sync error: Don't log the exception twice
If we have e.stack, then it will include the description of the exception.
2016-03-17 12:05:01 +00:00
Richard van der Hoff 963eaf7ec7 Log the stack when we get a sync error
If we have the stack for an exception in the /sync loop, we should log it.
2016-03-17 11:54:43 +00:00
Richard van der Hoff e6e5b9b748 release.sh: fix -z option 2016-03-17 01:33:51 +00:00
Richard van der Hoff 9ad031c857 Make changelog file and jsdoc generation switchable 2016-03-17 01:27:48 +00:00
Richard van der Hoff a0d465cb34 Merge master to develop after release 2016-03-17 01:12:39 +00:00
Richard van der Hoff 2dcf5227f0 Merge remote-tracking branch 'origin/master' into develop 2016-03-17 01:12:18 +00:00
Richard van der Hoff 518e92027c Add missing "Changes in" to changelog 2016-03-17 01:07:34 +00:00
Matthew Hodgson ebc95667b8 workaround for unicode regexp matches - https://github.com/vector-im/vector-web/issues/568 2016-03-17 01:02:50 +00:00
37 changed files with 49753 additions and 496 deletions
+63 -3
View File
@@ -1,5 +1,65 @@
[0.4.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.4.2) (2016-03-17)
=====================================================================================
Changes in [0.5.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.2) (2016-04-19)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.1...v0.5.2)
* Track the absolute time that presence events are received, so that the
relative lastActiveAgo value is meaningful.
[\#128](https://github.com/matrix-org/matrix-js-sdk/pull/128)
* Refactor the addition of events to rooms
[\#127](https://github.com/matrix-org/matrix-js-sdk/pull/127)
* Clean up test shutdown
[\#126](https://github.com/matrix-org/matrix-js-sdk/pull/126)
* Add methods to get (and set) pushers
[\#125](https://github.com/matrix-org/matrix-js-sdk/pull/125)
* URL previewing support
[\#122](https://github.com/matrix-org/matrix-js-sdk/pull/122)
* Avoid paginating forever in private rooms
[\#124](https://github.com/matrix-org/matrix-js-sdk/pull/124)
* Fix a bug where we recreated sync filters
[\#123](https://github.com/matrix-org/matrix-js-sdk/pull/123)
* Implement HTTP timeouts in realtime
[\#121](https://github.com/matrix-org/matrix-js-sdk/pull/121)
Changes in [0.5.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.1) (2016-03-30)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.0...v0.5.1)
* Only count joined members for the member count in notifications.
[\#119](https://github.com/matrix-org/matrix-js-sdk/pull/119)
* Add maySendEvent to match maySendStateEvent
[\#118](https://github.com/matrix-org/matrix-js-sdk/pull/118)
Changes in [0.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.0) (2016-03-22)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.4.2...v0.5.0)
**BREAKING CHANGES**:
* `opts.pendingEventOrdering`==`end` is no longer supported in the arguments to
`MatrixClient.startClient()`. Instead we provide a `detached` option, which
puts pending events into a completely separate list in the Room, accessible
via `Room.getPendingEvents()`.
[\#111](https://github.com/matrix-org/matrix-js-sdk/pull/111)
Other improvements:
* Log the stack when we get a sync error
[\#109](https://github.com/matrix-org/matrix-js-sdk/pull/109)
* Refactor transmitted-messages code
[\#110](https://github.com/matrix-org/matrix-js-sdk/pull/110)
* Add a method to the js sdk to look up 3pids on the ID server.
[\#113](https://github.com/matrix-org/matrix-js-sdk/pull/113)
* Support for cancelling pending events
[\#112](https://github.com/matrix-org/matrix-js-sdk/pull/112)
* API to stop peeking
[\#114](https://github.com/matrix-org/matrix-js-sdk/pull/114)
* update store user metadata based on membership events rather than presence
[\#116](https://github.com/matrix-org/matrix-js-sdk/pull/116)
* Include a counter in generated transaction IDs
[\#115](https://github.com/matrix-org/matrix-js-sdk/pull/115)
* get/setRoomVisibility API
[\#117](https://github.com/matrix-org/matrix-js-sdk/pull/117)
Changes in [0.4.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.4.2) (2016-03-17)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.4.1...v0.4.2)
* Try again if a pagination request gives us no new messages
@@ -41,7 +101,7 @@ Improvements:
* Avoid getting stuck in a pagination loop when the server sends us only
messages we've already seen
(https://github.com/matrix-org/matrix-js-sdk/pull/96).
New methods:
* Add `MatrixClient.setPushRuleActions` to set the actions for a push
notification rule (https://github.com/matrix-org/matrix-js-sdk/pull/90)
+15785
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+15832
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+16305
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+24
View File
@@ -0,0 +1,24 @@
#!/bin/sh
#
# pre-commit: script to run checks on a working copy before commit
#
# To use, symlink it into .git/hooks:
# ln -s ../../git-hooks/pre-commit .git/hooks
#
set -e
# create a temp dir
tmpdir=`mktemp -d`
trap 'rm -rf "$tmpdir"' EXIT
# get a copy of the index
git checkout-index --prefix="$tmpdir/" -a
# keep node_modules/.bin on the path
rootdir=`git rev-parse --show-toplevel`
export PATH="$rootdir/node_modules/.bin:$PATH"
# now run our checks
cd "$tmpdir"
npm run lint
+186 -62
View File
@@ -144,7 +144,10 @@ function MatrixClient(opts) {
var self = this;
this.scheduler.setProcessFunction(function(eventToSend) {
var room = self.getRoom(eventToSend.getRoomId());
_updateLocalEchoStatus(room, eventToSend, EventStatus.SENDING);
if (eventToSend.status !== EventStatus.SENDING) {
_updatePendingEventStatus(room, eventToSend,
EventStatus.SENDING);
}
return _sendEventHttpRequest(self, eventToSend);
});
}
@@ -180,8 +183,9 @@ function MatrixClient(opts) {
this._peekSync = null;
this._isGuest = false;
this._ongoingScrollbacks = {};
this._txnCtr = 0;
this.timelineSupport = Boolean(opts.timelineSupport);
this.urlPreviewCache = {};
}
utils.inherits(MatrixClient, EventEmitter);
@@ -694,7 +698,7 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) {
return self._http.authedRequest(undefined, "POST", path, undefined, data);
}).then(function(res) {
var roomId = res.room_id;
var syncApi = new SyncApi(self);
var syncApi = new SyncApi(self, self._clientOpts);
var room = syncApi.createRoom(roomId);
if (opts.syncRoom) {
// v2 will do this for us
@@ -718,10 +722,32 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) {
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.resendEvent = function(event, room) {
_updateLocalEchoStatus(room, event, EventStatus.SENDING);
_updatePendingEventStatus(room, event, EventStatus.SENDING);
return _sendEvent(this, room, event);
};
/**
* Cancel a queued or unsent event.
*
* @param {MatrixEvent} event Event to cancel
* @throws Error if the event is not in QUEUED or NOT_SENT state
*/
MatrixClient.prototype.cancelPendingEvent = function(event) {
if ([EventStatus.QUEUED, EventStatus.NOT_SENT].indexOf(event.status) < 0) {
throw new Error("cannot cancel an event with status " + event.status);
}
// first tell the scheduler to forget about it, if it's queued
if (this.scheduler) {
this.scheduler.removeEventFromQueue(event);
}
// then tell the room about the change of state, which will remove it
// from the room's list of pending events.
var room = this.getRoom(event.getRoomId());
_updatePendingEventStatus(room, event, EventStatus.CANCELLED);
};
/**
* @param {string} roomId
* @param {string} name
@@ -882,7 +908,7 @@ MatrixClient.prototype.getStateEvent = function(roomId, eventType, stateKey, cal
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.sendStateEvent = function(roomId, eventType, content, stateKey,
MatrixClient.prototype.sendStateEvent = function(roomId, eventType, content, stateKey,
callback) {
var pathParams = {
$roomId: roomId,
@@ -912,7 +938,7 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
if (utils.isFunction(txnId)) { callback = txnId; txnId = undefined; }
if (!txnId) {
txnId = "m" + new Date().getTime();
txnId = "m" + new Date().getTime() + "." + (this._txnCtr++);
}
// we always construct a MatrixEvent when sending because the store and
@@ -928,11 +954,11 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
content: content
});
localEvent._txnId = txnId;
localEvent.status = EventStatus.SENDING;
// add this event immediately to the local store as 'sending'.
if (room) {
localEvent.status = EventStatus.SENDING;
room.addEventsToTimeline([localEvent]);
room.addPendingEvent(localEvent, txnId);
}
if (eventType === "m.room.message" && this.sessionStore && CRYPTO_ENABLED) {
@@ -1140,11 +1166,6 @@ function _badEncryptedMessage(event, reason) {
}
function _sendEvent(client, room, event, callback) {
// cache the local event ID here because if /sync returns before /send then
// event.getId() will return a REAL event ID which we will then incorrectly
// remove!
var localEventId = event.getId();
var defer = q.defer();
var promise;
// this event may be queued
@@ -1157,7 +1178,7 @@ function _sendEvent(client, room, event, callback) {
if (promise && client.scheduler.getQueueForEvent(event).length > 1) {
// event is processed FIFO so if the length is 2 or more we know
// this event is stuck behind an earlier event.
_updateLocalEchoStatus(room, event, EventStatus.QUEUED);
_updatePendingEventStatus(room, event, EventStatus.QUEUED);
}
}
@@ -1167,42 +1188,13 @@ function _sendEvent(client, room, event, callback) {
promise.done(function(res) { // the request was sent OK
if (room) {
var eventId = res.event_id;
// FIXME: This manipulation of the room should probably be done
// inside the room class, not by the client.
var timeline = room.getTimelineForEvent(eventId);
if (!timeline) {
// we haven't yet received the event from the stream; we
// need to update the fake event with the right event id.
//
// best way to make sure the room timeline structures are updated
// correctly is to remove the event and add it again with the right
// ID.
//
// This will also make us synthesize our own read receipt for the
// sent message.
var oldStatus = event.status;
room.removeEvents([localEventId]);
event.event.event_id = res.event_id;
// TODO: at this point, we're still expecting the remote echo
// to come back and update the server-generated fields for
// us. We should probably set the status to some distinct value
// so that the client app can figure out what is going on.
event.status = null;
room.addEventsToTimeline([event]);
// FIXME: doing this here is a horrible fudge, but this all
// needs unpicking, which will touch the crypto code.
room.emit("Room.localEchoUpdated", event, room, localEventId,
oldStatus);
}
room.updatePendingEvent(event, EventStatus.SENT, res.event_id);
}
_resolve(callback, defer, res);
}, function(err) {
// the request failed to send.
_updateLocalEchoStatus(room, event, EventStatus.NOT_SENT);
_updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
_reject(callback, defer, err);
});
@@ -1210,9 +1202,9 @@ function _sendEvent(client, room, event, callback) {
return defer.promise;
}
function _updateLocalEchoStatus(room, event, newStatus) {
function _updatePendingEventStatus(room, event, newStatus) {
if (room) {
room.updateLocalEchoStatus(event, newStatus);
room.updatePendingEvent(event, newStatus);
} else {
event.status = newStatus;
}
@@ -1457,6 +1449,41 @@ MatrixClient.prototype.getCurrentUploads = function() {
return this._http.getCurrentUploads();
};
/**
* Get a preview of the given URL as of (roughly) the given point in time,
* described as an object with OpenGraph keys and associated values.
* Attributes may be synthesized where actual OG metadata is lacking.
* Caches results to prevent hammering the server.
* @param {string} url The URL to get preview data for
* @param {Number} ts The preferred point in time that the preview should
* describe (ms since epoch). The preview returned will either be the most
* recent one preceding this timestamp if available, or failing that the next
* most recent available preview.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: Object of OG metadata.
* @return {module:http-api.MatrixError} Rejects: with an error response.
* May return synthesized attributes if the URL lacked OG meta.
*/
MatrixClient.prototype.getUrlPreview = function(url, ts, callback) {
var key = ts + "_" + url;
var og = this.urlPreviewCache[key];
if (og) {
return q(og);
}
var self = this;
return this._http.authedRequestWithPrefix(
callback, "GET", "/preview_url", {
url: url,
ts: ts,
}, undefined, httpApi.PREFIX_MEDIA_R0
).then(function(response) {
// TODO: expire cache occasionally
self.urlPreviewCache[key] = response;
return response;
});
};
/**
* @param {string} roomId
* @param {boolean} isTyping
@@ -1702,7 +1729,7 @@ MatrixClient.prototype.kick = function(roomId, userId, reason, callback) {
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
function _setMembershipState(client, roomId, userId, membershipValue, reason,
function _setMembershipState(client, roomId, userId, membershipValue, reason,
callback) {
if (utils.isFunction(reason)) { callback = reason; reason = undefined; }
@@ -1913,6 +1940,38 @@ MatrixClient.prototype.setPresence = function(presence, callback) {
);
};
// Pushers
// =======
/**
* Gets all pushers registered for the logged-in user
*
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: Array of objects representing pushers
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.getPushers = function(callback) {
var path = "/pushers";
return this._http.authedRequest(
callback, "GET", path, undefined, undefined
);
};
/**
* Adds a new pusher or updates an existing pusher
*
* @param {Object} pusher Object representing a pusher
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: Empty json object on success
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setPusher = function(pusher, callback) {
var path = "/pushers/set";
return this._http.authedRequest(
callback, "POST", path, null, pusher
);
};
// Public (non-authed) operations
// ==============================
@@ -1945,6 +2004,39 @@ MatrixClient.prototype.resolveRoomAlias = function(roomAlias, callback) {
return this._http.request(callback, "GET", path);
};
/**
* Get the visibility of a room in the current HS's room directory
* @param {string} roomId
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.getRoomDirectoryVisibility =
function(roomId, callback) {
var path = utils.encodeUri("/directory/list/room/$roomId", {
$roomId: roomId
});
return this._http.authedRequest(callback, "GET", path);
};
/**
* Set the visbility of a room in the current HS's room directory
* @param {string} roomId
* @param {string} visibility
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setRoomDirectoryVisibility =
function(roomId, visibility, callback) {
var path = utils.encodeUri("/directory/list/room/$roomId", {
$roomId: roomId
});
return this._http.authedRequest(
callback, "PUT", path, undefined, { "visibility": visibility }
);
};
/**
* @param {string} roomId
* @param {Number} limit
@@ -2039,7 +2131,7 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) {
return self._http.authedRequest(callback, "GET", path, params);
}).done(function(res) {
var matrixEvents = utils.map(res.chunk, _PojoToMatrixEventMapper(self));
room.addEventsToTimeline(matrixEvents, true);
room.addEventsToTimeline(matrixEvents, true, room.getLiveTimeline());
room.oldState.paginationToken = res.end;
if (res.chunk.length === 0) {
room.oldState.paginationToken = null;
@@ -2322,10 +2414,20 @@ MatrixClient.prototype.peekInRoom = function(roomId) {
if (this._peekSync) {
this._peekSync.stopPeeking();
}
this._peekSync = new SyncApi(this);
this._peekSync = new SyncApi(this, this._clientOpts);
return this._peekSync.peek(roomId);
};
/**
* Stop any ongoing room peeking.
*/
MatrixClient.prototype.stopPeeking = function() {
if (this._peekSync) {
this._peekSync.stopPeeking();
this._peekSync = null;
}
};
/**
* Set r/w flags for guest access in a room.
* @param {string} roomId The room to configure guest access in.
@@ -2801,7 +2903,7 @@ MatrixClient.prototype.syncLeftRooms = function() {
return this._syncLeftRoomsPromise; // return the ongoing request
}
var self = this;
var syncApi = new SyncApi(this);
var syncApi = new SyncApi(this, this._clientOpts);
this._syncLeftRoomsPromise = syncApi.syncLeftRooms();
// cleanup locks
@@ -2931,11 +3033,14 @@ MatrixClient.prototype.isLoggedIn = function() {
* @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 {String=} opts.pendingEventOrdering Controls where pending messages appear
* in a room's timeline. If "<b>chronological</b>", messages will appear in the timeline
* when the call to <code>sendEvent</code> was made. If "<b>end</b>", pending messages
* will always appear at the end of the timeline (multiple pending messages will be sorted
* chronologically). Default: "chronological".
*
* @param {String=} opts.pendingEventOrdering Controls where pending messages
* appear in a room's timeline. If "<b>chronological</b>", messages will appear
* in the timeline when the call to <code>sendEvent</code> was made. If
* "<b>detached</b>", pending messages will appear in a separate list,
* accessbile via {@link module:models/room~Room#getPendingEvents}. Default:
* "chronological".
*
* @param {Number=} opts.pollTimeout The number of milliseconds to wait on /events.
* Default: 30000 (30 seconds).
*/
@@ -2952,6 +3057,8 @@ MatrixClient.prototype.startClient = function(opts) {
};
}
this._clientOpts = opts;
if (CRYPTO_ENABLED && this.sessionStore !== null) {
this.uploadKeys(5);
}
@@ -3264,12 +3371,29 @@ MatrixClient.prototype.requestEmailToken = function(email, clientSecret,
return this._http.idServerRequest(
callback, "POST", "/validate/email/requestToken",
params, httpApi.PREFIX_IDENTITY_V1
).then(function(res) {
if (typeof res === "string") {
return JSON.parse(res);
}
return res;
});
);
};
/**
* Looks up the public Matrix ID mapping for a given 3rd party
* identifier from the Identity Server
* @param {string} medium The medium of the threepid, eg. 'email'
* @param {string} address The textual address of the threepid
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: A threepid mapping
* object or the empty object if no mapping
* exists
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.lookupThreePid = function(medium, address, callback) {
var params = {
medium: medium,
address: address,
};
return this._http.idServerRequest(
callback, "GET", "/lookup",
params, httpApi.PREFIX_IDENTITY_V1
);
};
/**
+27 -8
View File
@@ -21,6 +21,11 @@ limitations under the License.
var q = require("q");
var utils = require("./utils");
// we use our own implementation of setTimeout, so that if we get suspended in
// the middle of a /sync, we cancel the sync as soon as we awake, rather than
// waiting for the delay to elapse.
var callbacks = require("./realtime-callbacks");
/*
TODO:
- CS: complete register function (doing stages)
@@ -42,6 +47,11 @@ module.exports.PREFIX_UNSTABLE = "/_matrix/client/unstable";
*/
module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
/**
* URI path for the media repo API
*/
module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0";
/**
* Construct a MatrixHttpApi.
* @constructor
@@ -125,12 +135,12 @@ module.exports.MatrixHttpApi.prototype = {
cb(new Error('Timeout'));
};
xhr.timeout_timer = setTimeout(timeout_fn, 30000);
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
xhr.onreadystatechange = function() {
switch (xhr.readyState) {
case global.XMLHttpRequest.DONE:
clearTimeout(xhr.timeout_timer);
callbacks.clearTimeout(xhr.timeout_timer);
var err;
if (!xhr.responseText) {
err = new Error('No response body.');
@@ -152,10 +162,10 @@ module.exports.MatrixHttpApi.prototype = {
}
};
xhr.upload.addEventListener("progress", function(ev) {
clearTimeout(xhr.timeout_timer);
callbacks.clearTimeout(xhr.timeout_timer);
upload.loaded = ev.loaded;
upload.total = ev.total;
xhr.timeout_timer = setTimeout(timeout_fn, 30000);
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
defer.notify(ev);
});
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
@@ -245,7 +255,12 @@ module.exports.MatrixHttpApi.prototype = {
opts,
requestCallback(defer, callback, this.opts.onlyData)
);
return defer.promise;
// ID server does not always take JSON, so we can't use requests' 'json'
// option as we do with the home server, but it does return JSON, so
// parse it manually
return defer.promise.then(function(response) {
return JSON.parse(response);
});
},
/**
@@ -445,9 +460,13 @@ module.exports.MatrixHttpApi.prototype = {
var timeoutId;
var timedOut = false;
var req;
if (localTimeoutMs) {
timeoutId = setTimeout(function() {
timeoutId = callbacks.setTimeout(function() {
timedOut = true;
if (req && req.abort) {
req.abort();
}
defer.reject(new module.exports.MatrixError({
error: "Locally timed out waiting for a response",
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
@@ -459,7 +478,7 @@ module.exports.MatrixHttpApi.prototype = {
var reqPromise = defer.promise;
try {
var req = this.opts.request(
req = this.opts.request(
{
uri: uri,
method: method,
@@ -472,7 +491,7 @@ module.exports.MatrixHttpApi.prototype = {
},
function(err, response, body) {
if (localTimeoutMs) {
clearTimeout(timeoutId);
callbacks.clearTimeout(timeoutId);
if (timedOut) {
return; // already rejected promise
}
+12 -14
View File
@@ -42,6 +42,8 @@ function EventTimeline(roomId) {
// this is used by client.js
this._paginationRequests = {'b': null, 'f': null};
this._name = roomId + ":" + new Date().toISOString();
}
/**
@@ -211,10 +213,8 @@ EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction)
*
* @param {MatrixEvent} event new event
* @param {boolean} atStart true to insert new event at the start
* @param {boolean} [spliceBeforeLocalEcho = false] insert this event before any
* localecho events at the end of the timeline. Ignored if atStart == true
*/
EventTimeline.prototype.addEvent = function(event, atStart, spliceBeforeLocalEcho) {
EventTimeline.prototype.addEvent = function(event, atStart) {
var stateContext = atStart ? this._startState : this._endState;
setEventMetadata(event, stateContext, atStart);
@@ -243,17 +243,6 @@ EventTimeline.prototype.addEvent = function(event, atStart, spliceBeforeLocalEch
insertIndex = 0;
} else {
insertIndex = this._events.length;
// if this is a real event, we might need to splice it in before any pending
// local echo events.
if (spliceBeforeLocalEcho) {
for (var j = this._events.length - 1; j >= 0; j--) {
if (!this._events[j].status) { // real events don't have a status
insertIndex = j + 1;
break;
}
}
}
}
this._events.splice(insertIndex, 0, event); // insert element
@@ -303,6 +292,15 @@ EventTimeline.prototype.removeEvent = function(eventId) {
return null;
};
/**
* Return a string to identify this timeline, for debugging
*
* @return {string} name for this timeline
*/
EventTimeline.prototype.toString = function() {
return this._name;
};
/**
* The EventTimeline class
+7 -1
View File
@@ -32,7 +32,13 @@ module.exports.EventStatus = {
/** The event is in the process of being sent. */
SENDING: "sending",
/** The event is in a queue waiting to be sent. */
QUEUED: "queued"
QUEUED: "queued",
/** The event has been sent to the server, but we have not yet received the
* echo. */
SENT: "sent",
/** The event was cancelled before it was successfully sent. */
CANCELLED: "cancelled",
};
/**
+48 -4
View File
@@ -239,6 +239,30 @@ RoomState.prototype.getUserIdsWithDisplayName = function(displayName) {
return this._displayNameToUserIds[displayName] || [];
};
/**
* Short-form for maySendEvent('m.room.message', userId)
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID should be permitted to send
* message events into the given room.
*/
RoomState.prototype.maySendMessage = function(userId) {
return this._maySendEventOfType('m.room.message', userId, false);
};
/**
* Returns true if the given user ID has permission to send a normal
* event of type `eventType` into this room.
* @param {string} type The type of event to test
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID should be permitted to send
* the given type of event into this room,
* according to the room's state.
*/
RoomState.prototype.maySendEvent = function(eventType, userId) {
return this._maySendEventOfType(eventType, userId, false);
};
/**
* Returns true if the given MatrixClient has permission to send a state
* event of type `stateEventType` into this room.
@@ -265,6 +289,22 @@ RoomState.prototype.mayClientSendStateEvent = function(stateEventType, cli) {
* according to the room's state.
*/
RoomState.prototype.maySendStateEvent = function(stateEventType, userId) {
return this._maySendEventOfType(stateEventType, userId, true);
};
/**
* Returns true if the given user ID has permission to send a normal or state
* event of type `eventType` into this room.
* @param {string} type The type of event to test
* @param {string} userId The user ID of the user to test permission for
* @param {boolean} state If true, tests if the user may send a state
event of this type. Otherwise tests whether
they may send a regular event.
* @return {boolean} true if the given user ID should be permitted to send
* the given type of event into this room,
* according to the room's state.
*/
RoomState.prototype._maySendEventOfType = function(eventType, userId, state) {
var member = this.getMember(userId);
if (!member || member.membership == 'leave') { return false; }
@@ -277,6 +317,7 @@ RoomState.prototype.maySendStateEvent = function(stateEventType, userId) {
var user_levels = [];
var state_default = 0;
var events_default = 0;
if (power_levels_event) {
power_levels = power_levels_event.getContent();
events_levels = power_levels.events || {};
@@ -289,13 +330,16 @@ RoomState.prototype.maySendStateEvent = function(stateEventType, userId) {
} else {
state_default = 50;
}
if (power_levels.events_default !== undefined) {
events_default = power_levels.events_default;
}
}
var state_event_level = state_default;
if (events_levels[stateEventType] !== undefined) {
state_event_level = events_levels[stateEventType];
var required_level = state ? state_default : events_default;
if (events_levels[eventType] !== undefined) {
required_level = events_levels[eventType];
}
return member.powerLevel >= state_event_level;
return member.powerLevel >= required_level;
};
/**
+336 -159
View File
@@ -26,6 +26,18 @@ var utils = require("../utils");
var ContentRepo = require("../content-repo");
var EventTimeline = require("./event-timeline");
// var DEBUG = false;
var DEBUG = true;
if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console);
} else {
var debuglog = function() {};
}
function synthesizeReceipt(userId, event, receiptType) {
// console.log("synthesizing receipt for "+event.getId());
// This is really ugly because JS has no way to express an object literal
@@ -68,11 +80,14 @@ function synthesizeReceipt(userId, event, receiptType) {
* @param {*} opts.storageToken Optional. The token which a data store can use
* to remember the state of the room. What this means is dependent on the store
* implementation.
* @param {String=} opts.pendingEventOrdering Controls where pending messages appear
* in a room's timeline. If "<b>chronological</b>", messages will appear in the timeline
* when the call to <code>sendEvent</code> was made. If "<b>end</b>", pending messages
* will always appear at the end of the timeline (multiple pending messages will be sorted
* chronologically). Default: "chronological".
*
* @param {String=} opts.pendingEventOrdering Controls where pending messages
* appear in a room's timeline. If "<b>chronological</b>", messages will appear
* in the timeline when the call to <code>sendEvent</code> was made. If
* "<b>detached</b>", pending messages will appear in a separate list,
* accessbile via {@link module:models/room~Room#getPendingEvents}. Default:
* "chronological".
*
* @param {boolean} [opts.timelineSupport = false] Set to true to enable improved
* timeline support.
*
@@ -99,10 +114,10 @@ function Room(roomId, opts) {
opts = opts || {};
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
if (["chronological", "end"].indexOf(opts.pendingEventOrdering) === -1) {
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
throw new Error(
"opts.pendingEventOrdering MUST be either 'chronological' or " +
"'end'. Got: '" + opts.pendingEventOrdering + "'"
"'detached'. Got: '" + opts.pendingEventOrdering + "'"
);
}
@@ -151,9 +166,31 @@ function Room(roomId, opts) {
this._eventIdToTimeline = {};
this._timelineSupport = Boolean(opts.timelineSupport);
if (this._opts.pendingEventOrdering == "detached") {
this._pendingEventList = [];
}
}
utils.inherits(Room, EventEmitter);
/**
* Get the list of pending sent events for this room
*
* @return {module:models/event.MatrixEvent[]} A list of the sent events
* waiting for remote echo.
*
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
*/
Room.prototype.getPendingEvents = function() {
if (this._opts.pendingEventOrdering !== "detached") {
throw new Error(
"Cannot call getPendingEventList with pendingEventOrdering == " +
this._opts.pendingEventOrdering);
}
return this._pendingEventList;
};
/**
* Get the live timeline for this room.
*
@@ -369,9 +406,9 @@ Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
*/
Room.prototype.addTimeline = function() {
if (!this._timelineSupport) {
throw Error("timeline support is disabled. Set the 'timelineSupport'" +
" parameter to true when creating MatrixClient to enable" +
" it.");
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
" parameter to true when creating MatrixClient to enable" +
" it.");
}
var timeline = new EventTimeline(this.roomId);
@@ -391,8 +428,8 @@ Room.prototype.addTimeline = function() {
* (oldest) instead of the end (newest) of the timeline. If true, the oldest
* event will be the <b>last</b> element of 'events'.
*
* @param {module:models/event-timeline~EventTimeline=} timeline timeline to
* add events to. If not given, events will be added to the live timeline
* @param {module:models/event-timeline~EventTimeline} timeline timeline to
* add events to.
*
* @param {string=} paginationToken token for the next batch of events
*
@@ -402,13 +439,16 @@ Room.prototype.addTimeline = function() {
Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
timeline, paginationToken) {
if (!timeline) {
timeline = this._liveTimeline;
throw new Error(
"'timeline' not specified for Room.addEventsToTimeline"
);
}
if (!toStartOfTimeline && timeline == this._liveTimeline) {
// special treatment for live events
this._addLiveEvents(events);
return;
throw new Error(
"Room.addEventsToTimeline cannot be used for adding events to " +
"the live timeline - use Room.addLiveEvents instead"
);
}
var direction = toStartOfTimeline ? EventTimeline.BACKWARDS :
@@ -504,7 +544,7 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
lastEventWasNew = false;
if (existingTimeline == timeline) {
console.log("Event " + eventId + " already in timeline " + timeline);
debuglog("Event " + eventId + " already in timeline " + timeline);
continue;
}
@@ -520,10 +560,10 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
// that would happen, so I'm going to ignore it for now.
//
if (existingTimeline == neighbour) {
console.log("Event " + eventId + " in neighbouring timeline - " +
debuglog("Event " + eventId + " in neighbouring timeline - " +
"switching to " + existingTimeline);
} else {
console.log("Event " + eventId + " already in a different " +
debuglog("Event " + eventId + " already in a different " +
"timeline " + existingTimeline);
}
timeline = existingTimeline;
@@ -549,7 +589,7 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
};
/**
* Check for redactions, and otherwise add event to the given timeline. Assumes
* Add event to the given timeline, and emit Room.timeline. Assumes
* we have already checked we don't know about this event.
*
* Will fire "Room.timeline" for each event added.
@@ -558,18 +598,13 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
* @param {EventTimeline} timeline
* @param {boolean} toStartOfTimeline
*
* @param {boolean} spliceBeforeLocalEcho if true, insert this event before
* any localecho events at the end of the timeline. Ignored if
* toStartOfTimeline == true.
*
* @fires module:client~MatrixClient#event:"Room.timeline"
*
* @private
*/
Room.prototype._addEventToTimeline = function(event, timeline, toStartOfTimeline,
spliceBeforeLocalEcho) {
Room.prototype._addEventToTimeline = function(event, timeline, toStartOfTimeline) {
var eventId = event.getId();
timeline.addEvent(event, toStartOfTimeline, spliceBeforeLocalEcho);
timeline.addEvent(event, toStartOfTimeline);
this._eventIdToTimeline[eventId] = timeline;
var data = {
@@ -581,150 +616,324 @@ Room.prototype._addEventToTimeline = function(event, timeline, toStartOfTimeline
/**
* Add some events to the end of this room's live timeline. Will fire
* "Room.timeline" for each event added.
* Add an event to the end of this room's live timeline. Will fire
* "Room.timeline"..
*
* @param {MatrixEvent[]} events A list of events to add.
* @param {MatrixEvent} event Event to be added
* @param {string?} duplicateStrategy 'ignore' or 'replace'
* @fires module:client~MatrixClient#event:"Room.timeline"
* @private
*/
Room.prototype._addLiveEvents = function(events) {
var addLocalEchoToEnd = this._opts.pendingEventOrdering === "end";
Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
if (event.getType() === "m.room.redaction") {
var redactId = event.event.redacts;
for (var i = 0; i < events.length; i++) {
var isLocalEcho = (
events[i].status === EventStatus.SENDING ||
events[i].status === EventStatus.QUEUED
);
// if we know about this event, redact its contents now.
var redactedEvent = this.findEventById(redactId);
if (redactedEvent) {
redactedEvent.makeRedacted(event);
this.emit("Room.redaction", event, this);
// FIXME: HORRIBLE ASSUMPTION THAT THIS PROP EXISTS
// Exists due to client.js:815 (MatrixClient.sendEvent)
// We should make txnId a first class citizen.
if (events[i]._txnId) {
// this is the outgoing copy of the event (ie, the local echo).
this._txnToEvent[events[i]._txnId] = events[i];
// TODO: we stash user displaynames (among other things) in
// RoomMember objects which are then attached to other events
// (in the sender and target fields). We should get those
// RoomMember objects to update themselves when the events that
// they are based on are changed.
}
else if (events[i].getUnsigned().transaction_id) {
// NB: We continue to add the redaction event to the timeline so
// clients can say "so and so redacted an event" if they wish to. Also
// this may be needed to trigger an update.
}
if (event.getUnsigned().transaction_id) {
var existingEvent = this._txnToEvent[event.getUnsigned().transaction_id];
if (existingEvent) {
// remote echo of an event we sent earlier
var existingEvent = this._txnToEvent[events[i].getUnsigned().transaction_id];
if (existingEvent) {
var oldEventId = existingEvent.getId();
var oldStatus = existingEvent.status;
this._handleRemoteEcho(event, existingEvent);
return;
}
}
// no longer pending
delete this._txnToEvent[events[i].getUnsigned().transaction_id];
var timeline = this._eventIdToTimeline[event.getId()];
if (timeline) {
if (duplicateStrategy === "replace") {
debuglog("Room._addLiveEvent: replacing duplicate event " +
event.getId());
var tlEvents = timeline.getEvents();
for (var j = 0; j < tlEvents.length; j++) {
if (tlEvents[j].getId() === event.getId()) {
// still need to set the right metadata on this event
setEventMetadata(
event,
timeline.getState(EventTimeline.FORWARDS),
false
);
// update the timeline map, because the event id has changed
var existingTimeline = this._eventIdToTimeline[oldEventId];
if (existingTimeline) {
delete this._eventIdToTimeline[oldEventId];
this._eventIdToTimeline[events[i].getId()] = existingTimeline;
if (!tlEvents[j].encryptedType) {
tlEvents[j] = event;
}
// XXX: we need to fire an event when this happens.
break;
}
// replace the event source, but preserve the original content
// and type in case it was encrypted (we won't be able to
// decrypt it, even though we sent it.)
var existingSource = existingEvent.event;
existingEvent.event = events[i].event;
existingEvent.event.content = existingSource.content;
existingEvent.event.type = existingSource.type;
// successfully sent.
existingEvent.status = null;
this.emit("Room.localEchoUpdated", existingEvent, this, oldEventId,
oldStatus);
continue;
}
} else {
debuglog("Room._addLiveEvent: ignoring duplicate event " +
event.getId());
}
return;
}
// TODO: pass through filter to see if this should be added to the timeline.
this._addEventToTimeline(event, this._liveTimeline, false);
if (events[i].getType() === "m.room.redaction") {
var redactId = events[i].event.redacts;
// 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
if (event.sender) {
this.addReceipt(synthesizeReceipt(
event.sender.userId, event, "m.read"
), true);
// if we know about this event, redact its contents now.
var redactedEvent = this.findEventById(redactId);
if (redactedEvent) {
redactedEvent.makeRedacted(events[i]);
this.emit("Room.redaction", events[i], this);
// TODO: we stash user displaynames (among other things) in
// RoomMember objects which are then attached to other events
// (in the sender and target fields). We should get those
// RoomMember objects to update themselves when the events that
// they are based on are changed.
}
// NB: We continue to add the redaction event to the timeline so
// clients can say "so and so redacted an event" if they wish to. Also
// this may be needed to trigger an update.
}
var spliceBeforeLocalEcho = !isLocalEcho && addLocalEchoToEnd;
if (!this._eventIdToTimeline[events[i].getId()]) {
// TODO: pass through filter to see if this should be added to the timeline.
this._addEventToTimeline(events[i], this._liveTimeline, false,
spliceBeforeLocalEcho);
}
// 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
//
// (we don't do this for local echoes, as they have temporary event
// ids, which don't make much sense as RRs).
if (events[i].sender && !isLocalEcho) {
this.addReceipt(synthesizeReceipt(
events[i].sender.userId, events[i], "m.read"
), true);
}
// Any live events from a user could be taken as implicit
// presence information: evidence that they are currently active.
// ...except in a world where we use 'user.currentlyActive' to reduce
// presence spam, this isn't very useful - we'll get a transition when
// they are no longer currently active anyway. So don't bother to
// reset the lastActiveAgo and lastPresenceTs from the RoomState's user.
}
};
/**
* Update the status field on a local echo, to reflect its transmission
* Add a pending outgoing event to this room.
*
* <p>The event is added to either the pendingEventList, or the live timeline,
* depending on the setting of opts.pendingEventOrdering.
*
* <p>This is an internal method, intended for use by MatrixClient.
*
* @param {module:models/event.MatrixEvent} event The event to add.
*
* @param {string} txnId Transaction id for this outgoing event
*
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
*
* @throws if the event doesn't have status SENDING, or we aren't given a
* unique transaction id.
*/
Room.prototype.addPendingEvent = function(event, txnId) {
if (event.status !== EventStatus.SENDING) {
throw new Error("addPendingEvent called on an event with status " +
event.status);
}
if (this._txnToEvent[txnId]) {
throw new Error("addPendingEvent called on an event with known txnId " +
txnId);
}
// call setEventMetadata to set up event.sender etc
setEventMetadata(
event,
this._liveTimeline.getState(EventTimeline.FORWARDS),
false
);
this._txnToEvent[txnId] = event;
if (this._opts.pendingEventOrdering == "detached") {
this._pendingEventList.push(event);
} else {
this._addEventToTimeline(event, this._liveTimeline, false);
}
this.emit("Room.localEchoUpdated", event, this, null, null);
};
/**
* Deal with the echo of a message we sent.
*
* <p>We move the event to the live timeline if it isn't there already, and
* update it.
*
* @param {module:models/event~MatrixEvent} remoteEvent The event received from
* /sync
* @param {module:models/event~MatrixEvent} localEvent The local echo, which
* should be either in the _pendingEventList or the timeline.
*
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
* @private
*/
Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
var oldEventId = localEvent.getId();
var newEventId = remoteEvent.getId();
var oldStatus = localEvent.status;
// no longer pending
delete this._txnToEvent[remoteEvent.transaction_id];
// if it's in the pending list, remove it
if (this._pendingEventList) {
utils.removeElement(
this._pendingEventList,
function(ev) { return ev.getId() == oldEventId; },
false
);
}
// replace the event source, but preserve the original content
// and type in case it was encrypted (we won't be able to
// decrypt it, even though we sent it.)
var existingSource = localEvent.event;
localEvent.event = remoteEvent.event;
localEvent.event.content = existingSource.content;
localEvent.event.type = existingSource.type;
// successfully sent.
localEvent.status = null;
// if it's already in the timeline, update the timeline map. If it's not, add it.
var existingTimeline = this._eventIdToTimeline[oldEventId];
if (existingTimeline) {
delete this._eventIdToTimeline[oldEventId];
this._eventIdToTimeline[newEventId] = existingTimeline;
} else {
this._addEventToTimeline(localEvent, this._liveTimeline, false);
}
this.emit("Room.localEchoUpdated", localEvent, this,
oldEventId, oldStatus);
};
/* a map from current event status to a list of allowed next statuses
*/
var ALLOWED_TRANSITIONS = {};
ALLOWED_TRANSITIONS[EventStatus.SENDING] =
[EventStatus.QUEUED, EventStatus.NOT_SENT, EventStatus.SENT];
ALLOWED_TRANSITIONS[EventStatus.QUEUED] =
[EventStatus.SENDING, EventStatus.CANCELLED];
ALLOWED_TRANSITIONS[EventStatus.SENT] =
[];
ALLOWED_TRANSITIONS[EventStatus.NOT_SENT] =
[EventStatus.SENDING, EventStatus.QUEUED, EventStatus.CANCELLED];
ALLOWED_TRANSITIONS[EventStatus.CANCELLED] =
[];
/**
* Update the status / event id on a pending event, to reflect its transmission
* progress.
*
* <p>This is an internal method.
*
* @param {MatrixEvent} event local echo event
* @param {EventStatus} newStatus status to assign
* @param {string} newEventId new event id to assign. Ignored unless
* newStatus == EventStatus.SENT.
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
*/
Room.prototype.updateLocalEchoStatus = function(event, newStatus) {
if (!event.status) {
throw new Error("updateLocalEchoStatus called on an event which is " +
"not a local echo.");
Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
// if the message was sent, we expect an event id
if (newStatus == EventStatus.SENT && !newEventId) {
throw new Error("updatePendingEvent called with status=SENT, " +
"but no new event id");
}
if (!this.getTimelineForEvent(event.getId())) {
throw new Error("updateLocalEchoStatus called on an unknown event.");
// SENT races against /sync, so we have to special-case it.
if (newStatus == EventStatus.SENT) {
var timeline = this._eventIdToTimeline[newEventId];
if (timeline) {
// we've already received the event via the event stream.
// nothing more to do here.
return;
}
}
var oldStatus = event.status;
var oldEventId = event.getId();
if (!oldStatus) {
throw new Error("updatePendingEventStatus called on an event which is " +
"not a local echo.");
}
var allowed = ALLOWED_TRANSITIONS[oldStatus];
if (!allowed || allowed.indexOf(newStatus) < 0) {
throw new Error("Invalid EventStatus transition " + oldStatus + "->" +
newStatus);
}
event.status = newStatus;
if (newStatus == EventStatus.SENT) {
// update the event id
event.event.event_id = newEventId;
// if the event was already in the timeline (which will be the case if
// opts.pendingEventOrdering==chronological), we need to update the
// timeline map.
var existingTimeline = this._eventIdToTimeline[oldEventId];
if (existingTimeline) {
delete this._eventIdToTimeline[oldEventId];
this._eventIdToTimeline[newEventId] = existingTimeline;
}
}
else if (newStatus == EventStatus.CANCELLED) {
// remove it from the pending event list, or the timeline.
if (this._pendingEventList) {
utils.removeElement(
this._pendingEventList,
function(ev) { return ev.getId() == oldEventId; },
false
);
}
this.removeEvent(oldEventId);
}
this.emit("Room.localEchoUpdated", event, this, event.getId(), oldStatus);
};
/**
* Add some events to this room. This can include state events, message
* events and typing notifications. These events are treated as "live" so
* they will go to the end of the timeline.
*
* @param {MatrixEvent[]} events A list of events to add.
*
* @param {string} duplicateStrategy Optional. Applies to events in the
* timeline only. If this is not specified, no duplicate suppression is
* performed (this improves performance). If this is 'replace' then if a
* duplicate is encountered, the event passed to this function will replace the
* existing event in the timeline. If this is 'ignore', then the event passed to
* timeline only. If this is 'replace' then if a duplicate is encountered, the
* event passed to this function will replace the existing event in the
* timeline. If this is not specified, or is 'ignore', then the event passed to
* this function will be ignored entirely, preserving the existing event in the
* timeline. Events are identical based on their event ID <b>only</b>.
*
* @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'.
*/
Room.prototype.addEvents = function(events, duplicateStrategy) {
Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
}
// sanity check that the live timeline is still live
if (this._liveTimeline.getPaginationToken(EventTimeline.FORWARDS)) {
throw new Error(
"live timeline is no longer live - it has a pagination token (" +
this._liveTimeline.getPaginationToken(EventTimeline.FORWARDS) + ")"
);
}
if (this._liveTimeline.getNeighbouringTimeline(EventTimeline.FORWARDS)) {
throw new Error(
"live timeline is no longer live - it has a neighbouring timeline"
);
}
for (var i = 0; i < events.length; i++) {
if (events[i].getType() === "m.typing") {
this.currentState.setTypingEvent(events[i]);
@@ -735,41 +944,9 @@ Room.prototype.addEvents = function(events, duplicateStrategy) {
// N.B. account_data is added directly by /sync to avoid
// having to maintain an event.isAccountData() here
else {
var timeline = this._eventIdToTimeline[events[i].getId()];
if (timeline && duplicateStrategy) {
// is there a duplicate?
var shouldIgnore = false;
var tlEvents = timeline.getEvents();
for (var j = 0; j < tlEvents.length; j++) {
if (tlEvents[j].getId() === events[i].getId()) {
if (duplicateStrategy === "replace") {
// still need to set the right metadata on this event
setEventMetadata(
events[i],
timeline.getState(EventTimeline.FORWARDS),
false
);
if (!tlEvents[j].encryptedType) {
tlEvents[j] = events[i];
}
// skip the insert so we don't add this event twice.
// Don't break in case we replace multiple events.
shouldIgnore = true;
}
else if (duplicateStrategy === "ignore") {
shouldIgnore = true;
break; // stop searching, we're skipping the insert
}
}
}
if (shouldIgnore) {
continue; // skip the insertion of this event.
}
}
// TODO: We should have a filter to say "only add state event
// types X Y Z to the timeline".
this._addLiveEvents([events[i]]);
this._addLiveEvent(events[i], duplicateStrategy);
}
}
};
@@ -1382,14 +1559,14 @@ module.exports = Room;
*
* <p>Once the /send request completes, if the remote echo has not already
* arrived, the event is updated with a new event id and the status is set to
* null. The server-generated fields are of course not updated yet.
* 'SENT'. The server-generated fields are of course not updated yet.
*
* <p>Finally, the /send might fail. In this case, the event's status is set to
* <p>If the /send fails, In this case, the event's status is set to
* 'NOT_SENT'. If it is later resent, the process starts again, setting the
* status to 'SENDING'.
* status to 'SENDING'. Alternatively, the message may be cancelled, which
* removes the event from the room, and sets the status to 'CANCELLED'.
*
* <p>This event is raised to reflect each of the transitions above (except the
* first send attempt).
* <p>This event is raised to reflect each of the transitions above.
*
* @event module:client~MatrixClient#"Room.localEchoUpdated"
*
+17 -1
View File
@@ -30,7 +30,12 @@ limitations under the License.
* @prop {string} displayName The 'displayname' of the user if known.
* @prop {string} avatarUrl The 'avatar_url' of the user if known.
* @prop {string} presence The presence enum if known.
* @prop {Number} lastActiveAgo The last time the user performed some action in ms.
* @prop {Number} lastActiveAgo The time elapsed in ms since the user interacted
* proactively with the server, or we saw a message from the user
* @prop {Number} lastPresenceTs Timestamp (ms since the epoch) for when we last
* received presence data for this user. We can subtract
* lastActiveAgo from this to approximate an absolute value for
* when a user was last active.
* @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be
* an approximation and that the user should be seen as active 'now'
* @prop {Object} events The events describing this user.
@@ -42,6 +47,7 @@ function User(userId) {
this.displayName = userId;
this.avatarUrl = null;
this.lastActiveAgo = 0;
this.lastPresenceTs = 0;
this.currentlyActive = false;
this.events = {
presence: null,
@@ -82,6 +88,7 @@ User.prototype.setPresenceEvent = function(event) {
this.displayName = event.getContent().displayname;
this.avatarUrl = event.getContent().avatar_url;
this.lastActiveAgo = event.getContent().last_active_ago;
this.lastPresenceTs = Date.now();
this.currentlyActive = event.getContent().currently_active;
if (eventsToFire.length > 0) {
@@ -136,6 +143,15 @@ User.prototype.getLastModifiedTime = function() {
return this._modified;
};
/**
* Get the absolute timestamp when this User was last known active on the server.
* It is *NOT* accurate if this.currentlyActive is true.
* @return {number} The timestamp
*/
User.prototype.getLastActiveTs = function() {
return this.lastPresenceTs - this.lastActiveAgo;
};
/**
* The User class.
*/
+7 -3
View File
@@ -125,7 +125,9 @@ function PushProcessor(client) {
var room = client.getRoom(ev.room_id);
if (!room || !room.currentState || !room.currentState.members) { return false; }
var memberCount = Object.keys(room.currentState.members).length;
var memberCount = Object.keys(room.currentState.members).filter(function(m) {
return room.currentState.members[m].membership == 'join';
}).length;
var m = cond.is.match(/^([=<>]*)([0-9]*)$/);
if (!m) { return false; }
@@ -160,7 +162,9 @@ function PushProcessor(client) {
var displayName = room.currentState.getMember(client.credentials.userId).name;
var pat = new RegExp("\\b" + escapeRegExp(displayName) + "\\b", 'i');
// N.B. we can't use \b as it chokes on unicode. however \W seems to be okay
// as shorthand for [^0-9A-Za-z_].
var pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", 'i');
return ev.content.body.search(pat) > -1;
};
@@ -174,7 +178,7 @@ function PushProcessor(client) {
var pat;
if (cond.key == 'content.body') {
pat = '\\b' + globToRegexp(cond.pattern) + '\\b';
pat = '(^|\\W)' + globToRegexp(cond.pattern) + '(\\W|$)';
} else {
pat = '^' + globToRegexp(cond.pattern) + '$';
}
+203
View File
@@ -0,0 +1,203 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* A re-implementation of the javascript callback functions (setTimeout,
* clearTimeout; setInterval and clearInterval are not yet implemented) which
* try to improve handling of large clock jumps (as seen when
* suspending/resuming the system).
*
* In particular, if a timeout would have fired while the system was suspended,
* it will instead fire as soon as possible after resume.
*/
"use strict";
// we schedule a callback at least this often, to check if we've missed out on
// some wall-clock time due to being suspended.
var TIMER_CHECK_PERIOD_MS = 1000;
// counter, for making up ids to return from setTimeout
var _count = 0;
// the key for our callback with the real global.setTimeout
var _realCallbackKey;
// a sorted list of the callbacks to be run.
// each is an object with keys [runAt, func, params, key].
var _callbackList = [];
// var debuglog = console.log.bind(console);
var debuglog = function() {};
/**
* Replace the function used by this module to get the current time.
*
* Intended for use by the unit tests.
*
* @param {function} f function which should return a millisecond counter
*
* @internal
*/
module.exports.setNow = function(f) {
_now = f || Date.now;
};
var _now = Date.now;
/**
* reimplementation of window.setTimeout, which will call the callback if
* the wallclock time goes past the deadline.
*
* @param {function} func callback to be called after a delay
* @param {Number} delayMs number of milliseconds to delay by
*
* @return {Number} an identifier for this callback, which may be passed into
* clearTimeout later.
*/
module.exports.setTimeout = function(func, delayMs) {
delayMs = delayMs || 0;
if (delayMs < 0) {
delayMs = 0;
}
var params = Array.prototype.slice.call(arguments, 2);
var runAt = _now() + delayMs;
var key = _count++;
debuglog("setTimeout: scheduling cb", key, "at", runAt,
"(delay", delayMs, ")");
var data = {
runAt: runAt,
func: func,
params: params,
key: key,
};
// figure out where it goes in the list
var idx = binarySearch(
_callbackList, function(el) {
return el.runAt - runAt;
}
);
_callbackList.splice(idx, 0, data);
_scheduleRealCallback();
return key;
};
/**
* reimplementation of window.clearTimeout, which mirrors setTimeout
*
* @param {Number} key result from an earlier setTimeout call
*/
module.exports.clearTimeout = function(key) {
if (_callbackList.length === 0) {
return;
}
// remove the element from the list
var i;
for (i = 0; i < _callbackList.length; i++) {
var cb = _callbackList[i];
if (cb.key == key) {
_callbackList.splice(i, 1);
break;
}
}
// iff it was the first one in the list, reschedule our callback.
if (i === 0) {
_scheduleRealCallback();
}
};
// use the real global.setTimeout to schedule a callback to _runCallbacks.
function _scheduleRealCallback() {
if (_realCallbackKey) {
global.clearTimeout(_realCallbackKey);
}
var first = _callbackList[0];
if (!first) {
debuglog("_scheduleRealCallback: no more callbacks, not rescheduling");
return;
}
var now = _now();
var delayMs = Math.min(first.runAt - now, TIMER_CHECK_PERIOD_MS);
debuglog("_scheduleRealCallback: now:", now, "delay:", delayMs);
_realCallbackKey = global.setTimeout(_runCallbacks, delayMs);
}
function _runCallbacks() {
var cb;
var now = _now();
debuglog("_runCallbacks: now:", now);
// get the list of things to call
var callbacksToRun = [];
while (true) {
var first = _callbackList[0];
if (!first || first.runAt > now) {
break;
}
cb = _callbackList.shift();
debuglog("_runCallbacks: popping", cb.key);
callbacksToRun.push(cb);
}
// reschedule the real callback before running our functions, to
// keep the codepaths the same whether or not our functions
// register their own setTimeouts.
_scheduleRealCallback();
for (var i = 0; i < callbacksToRun.length; i++) {
cb = callbacksToRun[i];
try {
cb.func.apply(null, cb.params);
} catch (e) {
console.error("Uncaught exception in callback function",
e.stack || e);
}
}
}
/* search in a sorted array.
*
* returns the index of the last element for which func returns
* greater than zero, or array.length if no such element exists.
*/
function binarySearch(array, func) {
// min is inclusive, max exclusive.
var min = 0,
max = array.length;
while (min < max) {
var mid = (min + max) >> 1;
var res = func(array[mid]);
if (res > 0) {
// the element at 'mid' is too big; set it as the new max.
max = mid;
} else {
// the element at 'mid' is too small. 'min' is inclusive, so +1.
min = mid + 1;
}
}
// presumably, min==max now.
return min;
}
+7 -13
View File
@@ -94,20 +94,14 @@ module.exports.MatrixInMemoryStore.prototype = {
// which would then show up in these lists (!)
return;
}
// We don't clobber any existing entry in the user map which has presence
// so user entries with presence info are preferred. This does mean we will
// clobber room member entries constantly, which is desirable to keep things
// like display names and avatar URLs up-to-date.
if (this.users[member.userId] && this.users[member.userId].events.presence) {
return;
}
var user = new User(member.userId);
user.setDisplayName(member.name);
var rawUrl = (
member.events.member ? member.events.member.getContent().avatar_url : null
);
user.setAvatarUrl(rawUrl);
var user = this.users[member.userId] || new User(member.userId);
if (member.name) {
user.setDisplayName(member.name);
}
if (member.events.member && member.events.member.getContent().avatar_url) {
user.setAvatarUrl(member.events.member.getContent().avatar_url);
}
this.users[user.userId] = user;
},
+2 -2
View File
@@ -373,7 +373,7 @@ WebStorageStore.prototype.scrollback = function(room, limit) {
);
room.addEventsToTimeline(utils.map(scrollback, function(e) {
return new MatrixEvent(e);
}), true);
}), true, room.getLiveTimeline());
this._tokens[room.storageToken] = {
earliestIndex: earliestIndex
@@ -594,7 +594,7 @@ function loadRoom(store, roomId, numEvents, tokenArray) {
index--;
}
// add events backwards to diverge old state correctly.
room.addEventsToTimeline(recentEvents.reverse(), true);
room.addEventsToTimeline(recentEvents.reverse(), true, room.getLiveTimeline());
room.oldState.paginationToken = currentStateMap.pagination_token;
// set the token data to let us know which index this room instance is at
// for scrollback.
+47 -16
View File
@@ -262,7 +262,8 @@ SyncApi.prototype.peek = function(roomId) {
// will overwrite the pagination token, so make sure it overwrites
// it with the right thing.
peekRoom.addEventsToTimeline(messages.reverse(), true,
undefined, response.messages.start);
peekRoom.getLiveTimeline(),
response.messages.start);
client.store.storeRoom(peekRoom);
client.emit("Room", peekRoom);
@@ -328,7 +329,7 @@ SyncApi.prototype._peekPoll = function(roomId, token) {
return e.room_id === roomId;
}).map(self.client.getEventMapper());
var room = self.client.getRoom(roomId);
room.addEvents(events);
room.addLiveEvents(events);
self._peekPoll(roomId, res.end);
}, function(err) {
console.error("[%s] Peek poll failed: %s", roomId, err);
@@ -351,7 +352,9 @@ SyncApi.prototype.getSyncState = function() {
* Main entry point
*/
SyncApi.prototype.sync = function() {
debuglog("SyncApi.sync");
debuglog("SyncApi.sync: starting with sync token " +
this.client.store.getSyncToken());
var client = this.client;
var self = this;
@@ -417,6 +420,10 @@ SyncApi.prototype.stop = function() {
}
this._running = false;
if (this._currentSyncRequest) { this._currentSyncRequest.abort(); }
if (this._keepAliveTimer) {
clearTimeout(this._keepAliveTimer);
this._keepAliveTimer = null;
}
};
/**
@@ -459,9 +466,17 @@ SyncApi.prototype._sync = function(syncOptions) {
var qps = {
filter: filterId,
timeout: this.opts.pollTimeout,
since: syncToken || undefined // do not send 'null'
};
if (syncToken) {
qps.since = syncToken;
} else {
// use a cachebuster for initialsyncs, to make sure that
// we don't get a stale sync
// (https://github.com/vector-im/vector-web/issues/1354)
qps._cacheBuster = Date.now();
}
if (self._syncConnectionLost) {
// 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
@@ -519,8 +534,9 @@ SyncApi.prototype._sync = function(syncOptions) {
self._processSyncResponse(syncToken, data);
}
catch (e) {
console.error("Caught /sync error:");
console.error(e);
// log the exception with stack if we have it, else fall back
// to the plain description
console.error("Caught /sync error", e.stack || e);
}
// emit synced events
@@ -702,7 +718,7 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
// XXX: should we be adding ephemeralEvents to the timeline?
// It feels like that for symmetry with room.addAccountData()
// there should be a room.addEphemeralEvents() or similar.
room.addEvents(ephemeralEvents);
room.addLiveEvents(ephemeralEvents);
// we deliberately don't add accountData to the timeline
room.addAccountData(accountDataEvents);
@@ -718,14 +734,28 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
accountDataEvents.forEach(function(e) { client.emit("event", e); });
});
// Handle leaves
// Handle leaves (e.g. kicked rooms)
leaveRooms.forEach(function(leaveObj) {
// Do the bear minimum to register rejected invites / you leaving rooms
var room = leaveObj.room;
var stateEvents =
self._mapSyncEventsFormat(leaveObj.state, room);
var timelineEvents =
self._mapSyncEventsFormat(leaveObj.timeline, room);
room.addEvents(timelineEvents);
var accountDataEvents =
self._mapSyncEventsFormat(leaveObj.account_data);
self._processRoomEvents(room, stateEvents, timelineEvents);
room.addAccountData(accountDataEvents);
room.recalculate(client.credentials.userId);
if (leaveObj.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); });
accountDataEvents.forEach(function(e) { client.emit("event", e); });
});
};
@@ -806,16 +836,17 @@ SyncApi.prototype._getOrCreateFilter = function(filterName, filter) {
promise = client.getFilter(client.credentials.userId,
filterId, true
).then(function(existingFilter) {
var oldStr = JSON.stringify(existingFilter.getDefinition());
var newStr = JSON.stringify(filter.getDefinition());
var oldDef = existingFilter.getDefinition();
var newDef = filter.getDefinition();
if (oldStr == newStr) {
if (utils.deepCompare(oldDef, newDef)) {
// super, just use that.
debuglog("Using existing filter ID %s: %s", filterId, oldStr);
debuglog("Using existing filter ID %s: %s", filterId,
JSON.stringify(oldDef));
return q(filterId);
}
debuglog("Existing filter ID %s: %s; new filter: %s",
filterId, oldStr, newStr);
filterId, JSON.stringify(oldDef), JSON.stringify(newDef));
return;
});
}
@@ -963,7 +994,7 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList,
// execute the timeline events, this will begin to diverge the current state
// if the timeline has any state events in it.
room.addEventsToTimeline(timelineEventList);
room.addLiveEvents(timelineEventList);
};
/**
+25 -3
View File
@@ -30,6 +30,13 @@ var DEBUG = false;
*/
var debuglog = DEBUG ? console.log.bind(console) : function() {};
/**
* the number of times we ask the server for more events before giving up
*
* @private
*/
var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
/**
* Construct a TimelineWindow.
*
@@ -179,10 +186,14 @@ TimelineWindow.prototype.canPaginate = function(direction) {
* even if there are fewer than 'size' of them, as we will just return those
* we already know about.)
*
* @param {number} [requestLimit = 5] limit for the number of API requests we
* should make.
*
* @return {module:client.Promise} Resolves to a boolean which is true if more events
* were successfully retrieved.
*/
TimelineWindow.prototype.paginate = function(direction, size, makeRequest) {
TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
requestLimit) {
// Either wind back the message cap (if there are enough events in the
// timeline to do so), or fire off a pagination request.
@@ -190,6 +201,10 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest) {
makeRequest = true;
}
if (requestLimit === undefined) {
requestLimit = DEFAULT_PAGINATE_LOOP_LIMIT;
}
var tl;
if (direction == EventTimeline.BACKWARDS) {
tl = this._start;
@@ -224,7 +239,9 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest) {
return q(true);
}
if (!makeRequest) {
if (!makeRequest || requestLimit === 0) {
// todo: should we return something different to indicate that there
// might be more events out there, but we haven't found them yet?
return q(false);
}
@@ -256,7 +273,12 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest) {
// token. In particular, we want to know if we've actually hit the
// start of the timeline, or if we just happened to know about all of
// the events thanks to https://matrix.org/jira/browse/SYN-645.
return self.paginate(direction, size, true);
//
// On the other hand, we necessarily want to wait forever for the
// server to make its mind up about whether there are other events,
// because it gives a bad user experience
// (https://github.com/vector-im/vector-web/issues/1204).
return self.paginate(direction, size, true, requestLimit - 1);
});
tl.pendingPaginate = prom;
return prom;
+86
View File
@@ -246,6 +246,92 @@ module.exports.deepCopy = function(obj) {
return JSON.parse(JSON.stringify(obj));
};
/**
* Compare two objects for equality. The objects MUST NOT have circular references.
*
* @param {Object} x The first object to compare.
* @param {Object} y The second object to compare.
*
* @return {boolean} true if the two objects are equal
*/
var deepCompare = module.exports.deepCompare = function(x, y) {
// Inspired by
// http://stackoverflow.com/questions/1068834/object-comparison-in-javascript#1144249
// Compare primitives and functions.
// Also check if both arguments link to the same object.
if (x === y) {
return true;
}
if (typeof x !== typeof y) {
return false;
}
// special-case NaN (since NaN !== NaN)
if (typeof x === 'number' && isNaN(x) && isNaN(y)) {
return true;
}
// special-case null (since typeof null == 'object', but null.constructor
// throws)
if (x === null || y === null) {
return x === y;
}
// everything else is either an unequal primitive, or an object
if (!(x instanceof Object)) {
return false;
}
// check they are the same type of object
if (x.constructor !== y.constructor || x.prototype !== y.prototype) {
return false;
}
// special-casing for some special types of object
if (x instanceof RegExp || x instanceof Date) {
return x.toString() === y.toString();
}
// the object algorithm works for Array, but it's sub-optimal.
if (x instanceof Array) {
if (x.length !== y.length) {
return false;
}
for (var i = 0; i < x.length; i++) {
if (!deepCompare(x[i], y[i])) {
return false;
}
}
} else {
// disable jshint "The body of a for in should be wrapped in an if
// statement"
/* jshint -W089 */
// check that all of y's direct keys are in x
var p;
for (p in y) {
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
return false;
}
}
// finally, compare each of x's keys with y
for (p in y) {
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
return false;
}
if (!deepCompare(x[p], y[p])) {
return false;
}
}
}
/* jshint +W089 */
return true;
};
/**
* Run polyfills to add Array.map and Array.filter if they are missing.
+3 -3
View File
@@ -1,11 +1,11 @@
{
"name": "matrix-js-sdk",
"version": "0.4.2",
"version": "0.5.2",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
"test": "istanbul cover --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" jasmine-node -- spec --verbose --junitreport --forceexit --captureExceptions",
"check": "jasmine-node spec --verbose --junitreport --forceexit --captureExceptions",
"test": "istanbul cover --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" jasmine-node -- spec --verbose --junitreport --captureExceptions",
"check": "jasmine-node spec --verbose --junitreport --captureExceptions",
"gendoc": "jsdoc -r lib -P package.json -R README.md -d .jsdoc",
"build": "jshint -c .jshint lib/ && browserify browser-index.js -o dist/browser-matrix-dev.js --ignore-missing",
"watch": "watchify browser-index.js -o dist/browser-matrix-dev.js -v",
+53 -35
View File
@@ -3,31 +3,41 @@
# Script to perform a release of matrix-js-sdk. Performs the steps documented
# in RELEASING.md
#
# Requires githib-changelog-generator; to install, do
# Requires github-changelog-generator; to install, do
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
set -e
USAGE="$0 [-x] vX.Y.Z"
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
help() {
cat <<EOF
$USAGE
-x: skip updating the changelog
-c changelog_file: specify name of file containing changelog
-x: skip updating the changelog
-z: skip generating the jsdoc
EOF
}
skip_changelog=
while getopts hx f; do
skip_jsdoc=
changelog_file="CHANGELOG.md"
while getopts hc:xz f; do
case $f in
h)
help
exit 0
;;
c)
changelog_file="$OPTARG"
;;
x)
skip_changelog=1
;;
z)
skip_jsdoc=1
;;
esac
done
shift `expr $OPTIND - 1`
@@ -37,22 +47,18 @@ if [ $# -ne 1 ]; then
exit 1
fi
tag="$1"
case "$tag" in
v*) ;;
*)
echo 2>&1 "Tag $tag must start with v"
exit 1
;;
esac
# strip leading 'v' to get release
release="${tag#v}"
# ignore leading v on release
release="${1#v}"
tag="v${release}"
rel_branch="release-$tag"
cd `dirname $0`
if [ -z "$skip_changelog" ]; then
if ! command -v update_changelog >/dev/null 2>&1; then
echo "release.sh requires github-changelog-generator. Try:" >&2
echo " pip install git+https://github.com/matrix-org/github-changelog-generator.git" >&2
exit 1
fi
fi
# we might already be on the release branch, in which case, yay
if [ $(git symbolic-ref --short HEAD) != "$rel_branch" ]; then
@@ -62,32 +68,35 @@ fi
if [ -z "$skip_changelog" ]; then
echo "Generating changelog"
update_changelog "$release"
read -p "Edit CHANGELOG.md manually, or press enter to continue " REPLY
update_changelog -f "$changelog_file" "$release"
read -p "Edit $changelog_file manually, or press enter to continue " REPLY
if [ -n "$(git ls-files --modified CHANGELOG.md)" ]; then
if [ -n "$(git ls-files --modified $changelog_file)" ]; then
echo "Committing updated changelog"
git commit "CHANGELOG.md" -m "Prepare changelog for $tag"
git commit "$changelog_file" -m "Prepare changelog for $tag"
fi
fi
set -x
# Bump package.json, build the dist, and tag
echo "npm version"
npm version "$release"
# generate the docs
echo "generating jsdocs"
npm run gendoc
if [ -z "$skip_jsdoc" ]; then
echo "generating jsdocs"
npm run gendoc
echo "copying jsdocs to gh-pages branch"
git checkout gh-pages
git pull
cp -ar ".jsdoc/matrix-js-sdk/$release" .
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' \
$release index.html
git add "$release"
git commit --no-verify -m "Add jsdoc for $release" index.html "$release"
echo "copying jsdocs to gh-pages branch"
git checkout gh-pages
git pull
cp -ar ".jsdoc/matrix-js-sdk/$release" .
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' \
$release index.html
git add "$release"
git commit --no-verify -m "Add jsdoc for $release" index.html "$release"
fi
# merge release branch to master
echo "updating master branch"
@@ -96,7 +105,16 @@ git pull
git merge --ff-only "$rel_branch"
# push everything to github
git push origin master "$rel_branch" "$tag" "gh-pages"
git push origin master "$rel_branch" "$tag"
if [ -z "$skip_jsdoc" ]; then
git push origin gh-pages
fi
# publish to npmjs
npm publish
# finally, merge master back onto develop
git checkout develop
git pull
git merge master
git push origin develop
@@ -24,6 +24,7 @@ describe("MatrixClient events", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
});
describe("emissions", function() {
@@ -97,6 +97,7 @@ function startClient(httpBackend, client) {
describe("getEventTimeline support", function() {
var httpBackend;
var client;
beforeEach(function() {
utils.beforeEach(this);
@@ -104,8 +105,14 @@ describe("getEventTimeline support", function() {
sdk.request(httpBackend.requestFn);
});
afterEach(function() {
if (client) {
client.stopClient();
}
});
it("timeline support must be enabled to work", function(done) {
var client = sdk.createClient({
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
@@ -120,7 +127,7 @@ describe("getEventTimeline support", function() {
});
it("timeline support works when enabled", function(done) {
var client = sdk.createClient({
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
@@ -141,7 +148,7 @@ describe("getEventTimeline support", function() {
it("scrollback should be able to scroll back to before a gappy /sync",
function(done) {
// need a client with timelineSupport disabled to make this work
var client = sdk.createClient({
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
@@ -229,6 +236,7 @@ describe("MatrixClient event timelines", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
});
describe("getEventTimeline", function() {
+1 -1
View File
@@ -34,7 +34,7 @@ describe("MatrixClient", function() {
it("should no-op if you've already joined a room", function() {
var roomId = "!foo:bar";
var room = new Room(roomId);
room.addEvents([
room.addLiveEvents([
utils.mkMembership({
user: userId, room: roomId, mship: "join", event: true
})
+4
View File
@@ -73,6 +73,10 @@ describe("MatrixClient opts", function() {
});
});
afterEach(function() {
client.stopClient();
});
it("should be able to send messages", function(done) {
var eventId = "$flibble:wibble";
httpBackend.when("PUT", "/txn1").respond(200, {
+52 -1
View File
@@ -2,22 +2,30 @@
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
var EventStatus = sdk.EventStatus;
describe("MatrixClient retrying", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend;
var scheduler;
var userId = "@alice:localhost";
var accessToken = "aseukfgwef";
var roomId = "!room:here";
var room;
beforeEach(function() {
utils.beforeEach(this);
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
scheduler = new sdk.MatrixScheduler();
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken
accessToken: accessToken,
scheduler: scheduler,
});
room = new sdk.Room(roomId);
client.store.storeRoom(room);
});
afterEach(function() {
@@ -40,6 +48,49 @@ describe("MatrixClient retrying", function() {
});
it("should mark events as EventStatus.CANCELLED when cancelled", function(done) {
// send a couple of events; the second will be queued
var ev1, ev2;
client.sendMessage(roomId, "m1").then(function(ev) {
expect(ev).toEqual(ev1);
});
client.sendMessage(roomId, "m2").then(function(ev) {
expect(ev).toEqual(ev2);
});
// both events should be in the timeline at this point
var tl = room.getLiveTimeline().getEvents();
expect(tl.length).toEqual(2);
ev1 = tl[0];
ev2 = tl[1];
expect(ev1.status).toEqual(EventStatus.SENDING);
expect(ev2.status).toEqual(EventStatus.QUEUED);
// now we can cancel the second and check everything looks sane
client.cancelPendingEvent(ev2);
expect(ev2.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(1);
// shouldn't be able to cancel the first message yet
expect(function() { client.cancelPendingEvent(ev1); })
.toThrow();
// fail the first send
httpBackend.when("PUT", "/send/m.room.message/")
.respond(400);
httpBackend.flush().then(function() {
expect(ev1.status).toEqual(EventStatus.NOT_SENT);
expect(tl.length).toEqual(1);
// cancel the first message
client.cancelPendingEvent(ev1);
expect(ev1.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(0);
}).catch(utils.failTest).done(done);
});
describe("resending", function() {
xit("should be able to resend a NOT_SENT event", function() {
+10 -11
View File
@@ -126,6 +126,7 @@ describe("MatrixClient room timelines", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
});
describe("local echo events", function() {
@@ -391,16 +392,16 @@ describe("MatrixClient room timelines", function() {
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1).done(function() {
httpBackend.flush("/sync", 1).then(function() {
expect(index).toEqual(2);
expect(room.timeline[room.timeline.length - 1].event).toEqual(
expect(room.timeline.length).toEqual(3);
expect(room.timeline[2].event).toEqual(
eventData[1]
);
expect(room.timeline[room.timeline.length - 2].event).toEqual(
expect(room.timeline[1].event).toEqual(
eventData[0]
);
done();
});
}).catch(utils.failTest).done(done);
});
httpBackend.flush("/sync", 1);
});
@@ -419,13 +420,12 @@ describe("MatrixClient room timelines", function() {
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
httpBackend.flush("/sync", 1).done(function() {
httpBackend.flush("/sync", 1).then(function() {
var preNameEvent = room.timeline[room.timeline.length - 3];
var postNameEvent = room.timeline[room.timeline.length - 1];
expect(preNameEvent.sender.name).toEqual(userName);
expect(postNameEvent.sender.name).toEqual("New Name");
done();
});
}).catch(utils.failTest).done(done);
});
httpBackend.flush("/sync", 1);
});
@@ -487,7 +487,7 @@ describe("MatrixClient room timelines", function() {
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
httpBackend.flush("/sync", 1).done(function() {
httpBackend.flush("/sync", 1).then(function() {
expect(room.currentState.getMembers().length).toEqual(4);
expect(room.currentState.getMember(userC).name).toEqual("C");
expect(room.currentState.getMember(userC).membership).toEqual(
@@ -497,8 +497,7 @@ describe("MatrixClient room timelines", function() {
expect(room.currentState.getMember(userD).membership).toEqual(
"invite"
);
done();
});
}).catch(utils.failTest).done(done);
});
httpBackend.flush("/sync", 1);
});
+1
View File
@@ -32,6 +32,7 @@ describe("MatrixClient syncing", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
});
describe("startClient", function() {
+14
View File
@@ -15,6 +15,20 @@ function HttpBackend() {
realReq.callback = callback;
console.log("HTTP backend received request: %s %s", opts.method, opts.uri);
self.requests.push(realReq);
var abort = function() {
var idx = self.requests.indexOf(realReq);
if (idx >= 0) {
console.log("Aborting HTTP request: %s %s", opts.method,
opts.uri);
self.requests.splice(idx, 1);
realReq.callback("aborted");
}
};
return {
abort: abort
};
};
}
HttpBackend.prototype = {
+179
View File
@@ -0,0 +1,179 @@
"use strict";
var callbacks = require("../../lib/realtime-callbacks");
var test_utils = require("../test-utils.js");
describe("realtime-callbacks", function() {
var clock = jasmine.Clock;
var fakeDate;
function tick(millis) {
// make sure we tick the fakedate first, otherwise nothing will happen!
fakeDate += millis;
clock.tick(millis);
}
beforeEach(function() {
test_utils.beforeEach(this);
clock.useMock();
fakeDate = Date.now();
callbacks.setNow(function() { return fakeDate; });
});
afterEach(function() {
callbacks.setNow();
});
describe("setTimeout", function() {
it("should call the callback after the timeout", function() {
var callback = jasmine.createSpy();
callbacks.setTimeout(callback, 100);
expect(callback).not.toHaveBeenCalled();
tick(100);
expect(callback).toHaveBeenCalled();
});
it("should default to a zero timeout", function() {
var callback = jasmine.createSpy();
callbacks.setTimeout(callback);
expect(callback).not.toHaveBeenCalled();
tick(0);
expect(callback).toHaveBeenCalled();
});
it("should pass any parameters to the callback", function() {
var callback = jasmine.createSpy();
callbacks.setTimeout(callback, 0, "a", "b", "c");
tick(0);
expect(callback).toHaveBeenCalledWith("a", "b", "c");
});
it("should set 'this' to the global object", function() {
var callback = jasmine.createSpy();
callback.andCallFake(function() {
expect(this).toBe(global);
expect(this.console).toBeDefined();
});
callbacks.setTimeout(callback);
tick(0);
expect(callback).toHaveBeenCalled();
});
it("should handle timeouts of several seconds", function() {
var callback = jasmine.createSpy();
callbacks.setTimeout(callback, 2000);
expect(callback).not.toHaveBeenCalled();
for (var i = 0; i < 4; i++) {
tick(500);
}
expect(callback).toHaveBeenCalled();
});
it("should call multiple callbacks in the right order", function() {
var callback1 = jasmine.createSpy("callback1");
var callback2 = jasmine.createSpy("callback2");
var callback3 = jasmine.createSpy("callback3");
callbacks.setTimeout(callback2, 200);
callbacks.setTimeout(callback1, 100);
callbacks.setTimeout(callback3, 300);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
expect(callback3).toHaveBeenCalled();
});
it("should treat -ve timeouts the same as a zero timeout", function() {
var callback1 = jasmine.createSpy("callback1");
var callback2 = jasmine.createSpy("callback2");
// check that cb1 is called before cb2
callback1.andCallFake(function() {
expect(callback2).not.toHaveBeenCalled();
});
callbacks.setTimeout(callback1);
callbacks.setTimeout(callback2, -100);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
});
it("should not get confused by chained calls", function() {
var callback2 = jasmine.createSpy("callback2");
var callback1 = jasmine.createSpy("callback1");
callback1.andCallFake(function() {
callbacks.setTimeout(callback2, 0);
expect(callback2).not.toHaveBeenCalled();
});
callbacks.setTimeout(callback1);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
});
it("should be immune to exceptions", function() {
var callback1 = jasmine.createSpy("callback1");
callback1.andCallFake(function() {
throw new Error("prepare to die");
});
var callback2 = jasmine.createSpy("callback2");
callbacks.setTimeout(callback1, 0);
callbacks.setTimeout(callback2, 0);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
});
});
describe("cancelTimeout", function() {
it("should cancel a pending timeout", function() {
var callback = jasmine.createSpy();
var k = callbacks.setTimeout(callback);
callbacks.clearTimeout(k);
tick(0);
expect(callback).not.toHaveBeenCalled();
});
it("should not affect sooner timeouts", function() {
var callback1 = jasmine.createSpy("callback1");
var callback2 = jasmine.createSpy("callback2");
callbacks.setTimeout(callback1, 100);
var k = callbacks.setTimeout(callback2, 200);
callbacks.clearTimeout(k);
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
tick(150);
expect(callback2).not.toHaveBeenCalled();
});
});
});
+70
View File
@@ -362,4 +362,74 @@ describe("RoomState", function() {
expect(state.maySendStateEvent('m.room.other_thing', userB)).toEqual(false);
});
});
describe("maySendEvent", function() {
it("should say non-joined members may not send events",
function() {
expect(state.maySendEvent(
'm.room.message', "@nobody:nowhere"
)).toEqual(false);
expect(state.maySendMessage("@nobody:nowhere")).toEqual(false);
});
it("should say any member may send events with no power level event",
function() {
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendMessage(userA)).toEqual(true);
});
it("should obey events_default",
function() {
var powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
state_default: 30,
events_default: 25,
users: {
}
}
};
powerLevelEvent.content.users[userA] = 26;
powerLevelEvent.content.users[userB] = 24;
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendEvent('m.room.message', userB)).toEqual(false);
expect(state.maySendMessage(userA)).toEqual(true);
expect(state.maySendMessage(userB)).toEqual(false);
});
it("should honour explicit event power levels in the power_levels event",
function() {
var powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
events: {
"m.room.other_thing": 33
},
users_default: 10,
state_default: 50,
events_default: 25,
users: {
}
}
};
powerLevelEvent.content.users[userA] = 40;
powerLevelEvent.content.users[userB] = 30;
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendEvent('m.room.message', userB)).toEqual(true);
expect(state.maySendMessage(userA)).toEqual(true);
expect(state.maySendMessage(userB)).toEqual(true);
expect(state.maySendEvent('m.room.other_thing', userA)).toEqual(true);
expect(state.maySendEvent('m.room.other_thing', userB)).toEqual(false);
});
});
});
+208 -153
View File
@@ -82,7 +82,7 @@ describe("Room", function() {
});
});
describe("addEvents", function() {
describe("addLiveEvents", function() {
var events = [
utils.mkMessage({
room: roomId, user: userA, msg: "changing room name", event: true
@@ -100,12 +100,12 @@ describe("Room", function() {
user_ids: [userA]
}
});
room.addEvents([typing]);
room.addLiveEvents([typing]);
expect(room.currentState.setTypingEvent).toHaveBeenCalledWith(typing);
});
it("should throw if duplicateStrategy isn't 'replace' or 'ignore'", function() {
expect(function() { room.addEvents(events, "foo"); }).toThrow();
expect(function() { room.addLiveEvents(events, "foo"); }).toThrow();
});
it("should replace a timeline event if dupe strategy is 'replace'", function() {
@@ -114,9 +114,9 @@ describe("Room", function() {
room: roomId, user: userA, msg: "dupe", event: true
});
dupe.event.event_id = events[0].getId();
room.addEvents(events);
room.addLiveEvents(events);
expect(room.timeline[0]).toEqual(events[0]);
room.addEvents([dupe], "replace");
room.addLiveEvents([dupe], "replace");
expect(room.timeline[0]).toEqual(dupe);
});
@@ -126,11 +126,113 @@ describe("Room", function() {
room: roomId, user: userA, msg: "dupe", event: true
});
dupe.event.event_id = events[0].getId();
room.addEvents(events);
room.addLiveEvents(events);
expect(room.timeline[0]).toEqual(events[0]);
room.addEvents([dupe], "ignore");
room.addLiveEvents([dupe], "ignore");
expect(room.timeline[0]).toEqual(events[0]);
});
it("should emit 'Room.timeline' events",
function() {
var callCount = 0;
room.on("Room.timeline", function(event, emitRoom, toStart) {
callCount += 1;
expect(room.timeline.length).toEqual(callCount);
expect(event).toEqual(events[callCount - 1]);
expect(emitRoom).toEqual(room);
expect(toStart).toBeFalsy();
});
room.addLiveEvents(events);
expect(callCount).toEqual(2);
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for new events", function() {
var events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, event: true,
content: {
name: "New room"
}
})
];
room.addLiveEvents(events);
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
[events[0]]
);
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
[events[1]]
);
expect(events[0].forwardLooking).toBe(true);
expect(events[1].forwardLooking).toBe(true);
expect(room.oldState.setStateEvents).not.toHaveBeenCalled();
});
it("should synthesize read receipts for the senders of events", function() {
var sentinel = {
userId: userA,
membership: "join",
name: "Alice"
};
room.currentState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
room.addLiveEvents(events);
expect(room.getEventReadUpTo(userA)).toEqual(events[1].getId());
});
it("should emit Room.localEchoUpdated when a local echo is updated", function() {
var localEvent = utils.mkMessage({
room: roomId, user: userA, event: true,
});
localEvent.status = EventStatus.SENDING;
var localEventId = localEvent.getId();
var remoteEvent = utils.mkMessage({
room: roomId, user: userA, event: true,
});
remoteEvent.event.unsigned = {transaction_id: "TXN_ID"};
var remoteEventId = remoteEvent.getId();
var callCount = 0;
room.on("Room.localEchoUpdated",
function(event, emitRoom, oldEventId, oldStatus) {
switch (callCount) {
case 0:
expect(event.getId()).toEqual(localEventId);
expect(event.status).toEqual(EventStatus.SENDING);
expect(emitRoom).toEqual(room);
expect(oldEventId).toBe(null);
expect(oldStatus).toBe(null);
break;
case 1:
expect(event.getId()).toEqual(remoteEventId);
expect(event.status).toBe(null);
expect(emitRoom).toEqual(room);
expect(oldEventId).toEqual(localEventId);
expect(oldStatus).toBe(EventStatus.SENDING);
break;
}
callCount += 1;
}
);
// first add the local echo
room.addPendingEvent(localEvent, "TXN_ID");
expect(room.timeline.length).toEqual(1);
// then the remoteEvent
room.addLiveEvents([remoteEvent]);
expect(room.timeline.length).toEqual(1);
expect(callCount).toEqual(2);
});
});
describe("addEventsToTimeline", function() {
@@ -144,34 +246,19 @@ describe("Room", function() {
})
];
it("should be able to add events to the end", function() {
room.addEventsToTimeline(events);
expect(room.timeline.length).toEqual(2);
expect(room.timeline[0]).toEqual(events[0]);
expect(room.timeline[1]).toEqual(events[1]);
it("should not be able to add events to the end", function() {
expect(function() {
room.addEventsToTimeline(events, false, room.getLiveTimeline());
}).toThrow();
});
it("should be able to add events to the start", function() {
room.addEventsToTimeline(events, true);
room.addEventsToTimeline(events, true, room.getLiveTimeline());
expect(room.timeline.length).toEqual(2);
expect(room.timeline[0]).toEqual(events[1]);
expect(room.timeline[1]).toEqual(events[0]);
});
it("should emit 'Room.timeline' events when added to the end",
function() {
var callCount = 0;
room.on("Room.timeline", function(event, emitRoom, toStart) {
callCount += 1;
expect(room.timeline.length).toEqual(callCount);
expect(event).toEqual(events[callCount - 1]);
expect(emitRoom).toEqual(room);
expect(toStart).toBeFalsy();
});
room.addEventsToTimeline(events);
expect(callCount).toEqual(2);
});
it("should emit 'Room.timeline' events when added to the start",
function() {
var callCount = 0;
@@ -182,10 +269,12 @@ describe("Room", function() {
expect(emitRoom).toEqual(room);
expect(toStart).toBe(true);
});
room.addEventsToTimeline(events, true);
room.addEventsToTimeline(events, true, room.getLiveTimeline());
expect(callCount).toEqual(2);
});
});
describe("event metadata handling", function() {
it("should set event.sender for new and old events", function() {
var sentinel = {
userId: userA,
@@ -218,9 +307,9 @@ describe("Room", function() {
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "Old Room Name" }
});
room.addEventsToTimeline([newEv]);
room.addLiveEvents([newEv]);
expect(newEv.sender).toEqual(sentinel);
room.addEventsToTimeline([oldEv], true);
room.addEventsToTimeline([oldEv], true, room.getLiveTimeline());
expect(oldEv.sender).toEqual(oldSentinel);
});
@@ -255,38 +344,12 @@ describe("Room", function() {
var oldEv = utils.mkMembership({
room: roomId, mship: "ban", user: userB, skey: userA, event: true
});
room.addEventsToTimeline([newEv]);
room.addLiveEvents([newEv]);
expect(newEv.target).toEqual(sentinel);
room.addEventsToTimeline([oldEv], true);
room.addEventsToTimeline([oldEv], true, room.getLiveTimeline());
expect(oldEv.target).toEqual(oldSentinel);
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for new events", function() {
var events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, event: true,
content: {
name: "New room"
}
})
];
room.addEventsToTimeline(events);
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
[events[0]]
);
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
[events[1]]
);
expect(events[0].forwardLooking).toBe(true);
expect(events[1].forwardLooking).toBe(true);
expect(room.oldState.setStateEvents).not.toHaveBeenCalled();
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for old events", function() {
var events = [
@@ -301,7 +364,7 @@ describe("Room", function() {
})
];
room.addEventsToTimeline(events, true);
room.addEventsToTimeline(events, true, room.getLiveTimeline());
expect(room.oldState.setStateEvents).toHaveBeenCalledWith(
[events[0]]
);
@@ -312,55 +375,6 @@ describe("Room", function() {
expect(events[1].forwardLooking).toBe(false);
expect(room.currentState.setStateEvents).not.toHaveBeenCalled();
});
it("should synthesize read receipts for the senders of events", function() {
var sentinel = {
userId: userA,
membership: "join",
name: "Alice"
};
room.currentState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
room.addEventsToTimeline(events);
expect(room.getEventReadUpTo(userA)).toEqual(events[1].getId());
});
it("should emit Room.localEchoUpdated when a local echo is updated", function() {
var localEvent = utils.mkMessage({
room: roomId, user: userA, event: true,
});
localEvent._txnId = "TXN_ID";
localEvent.status = EventStatus.SENDING;
var localEventId = localEvent.getId();
var remoteEvent = utils.mkMessage({
room: roomId, user: userA, event: true,
});
remoteEvent.event.unsigned = {transaction_id: "TXN_ID"};
var remoteEventId = remoteEvent.getId();
var callCount = 0;
room.on("Room.localEchoUpdated", function(event, emitRoom, oldEventId) {
callCount += 1;
expect(event.getId()).toEqual(remoteEventId);
expect(emitRoom).toEqual(room);
expect(oldEventId).toEqual(localEventId);
});
// first add the local echo to the timeline
room.addEventsToTimeline([localEvent]);
expect(room.timeline.length).toEqual(1);
// then the remoteEvent
room.addEventsToTimeline([remoteEvent]);
expect(room.timeline.length).toEqual(1);
expect(callCount).toEqual(1);
});
});
var resetTimelineTests = function(timelineSupport) {
@@ -383,11 +397,11 @@ describe("Room", function() {
});
it("should copy state from previous timeline", function() {
room.addEventsToTimeline([events[0], events[1]]);
room.addLiveEvents([events[0], events[1]]);
expect(room.getLiveTimeline().getEvents().length).toEqual(2);
room.resetLiveTimeline();
room.addEventsToTimeline([events[2]]);
room.addLiveEvents([events[2]]);
var oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS);
var newState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
expect(room.getLiveTimeline().getEvents().length).toEqual(1);
@@ -396,11 +410,11 @@ describe("Room", function() {
});
it("should reset the legacy timeline fields", function() {
room.addEventsToTimeline([events[0], events[1]]);
room.addLiveEvents([events[0], events[1]]);
expect(room.timeline.length).toEqual(2);
room.resetLiveTimeline();
room.addEventsToTimeline([events[2]]);
room.addLiveEvents([events[2]]);
var newLiveTimeline = room.getLiveTimeline();
expect(room.timeline).toEqual(newLiveTimeline.getEvents());
expect(room.oldState).toEqual(
@@ -429,7 +443,7 @@ describe("Room", function() {
it("should " + (timelineSupport ? "remember" : "forget") +
" old timelines", function() {
room.addEventsToTimeline([events[0]]);
room.addLiveEvents([events[0]]);
expect(room.timeline.length).toEqual(1);
var firstLiveTimeline = room.getLiveTimeline();
room.resetLiveTimeline();
@@ -463,7 +477,7 @@ describe("Room", function() {
];
it("should handle events in the same timeline", function() {
room.addEventsToTimeline(events);
room.addLiveEvents(events);
expect(room.compareEventOrdering(events[0].getId(),
events[1].getId()))
@@ -482,7 +496,7 @@ describe("Room", function() {
room.getLiveTimeline().setNeighbouringTimeline(oldTimeline, 'b');
room.addEventsToTimeline([events[0]], false, oldTimeline);
room.addEventsToTimeline([events[1]]);
room.addLiveEvents([events[1]]);
expect(room.compareEventOrdering(events[0].getId(),
events[1].getId()))
@@ -496,7 +510,7 @@ describe("Room", function() {
var oldTimeline = room.addTimeline();
room.addEventsToTimeline([events[0]], false, oldTimeline);
room.addEventsToTimeline([events[1]]);
room.addLiveEvents([events[1]]);
expect(room.compareEventOrdering(events[0].getId(),
events[1].getId()))
@@ -507,7 +521,7 @@ describe("Room", function() {
});
it("should return null for unknown events", function() {
room.addEventsToTimeline(events);
room.addLiveEvents(events);
expect(room.compareEventOrdering(events[0].getId(), "xxx"))
.toBe(null);
@@ -1054,7 +1068,7 @@ describe("Room", function() {
}),
];
room.addEventsToTimeline(events);
room.addLiveEvents(events);
var ts = 13787898424;
// check it initialises correctly
@@ -1129,10 +1143,11 @@ describe("Room", function() {
});
});
describe("pendingEventOrdering", function() {
it("should sort pending events to the end of the timeline if 'end'", function() {
describe("addPendingEvent", function() {
it("should add pending events to the pendingEventList if " +
"pendingEventOrdering == 'detached'", function() {
var room = new Room(roomId, {
pendingEventOrdering: "end"
pendingEventOrdering: "detached"
});
var eventA = utils.mkMessage({
room: roomId, user: userA, msg: "remote 1", event: true
@@ -1144,13 +1159,19 @@ describe("Room", function() {
var eventC = utils.mkMessage({
room: roomId, user: userA, msg: "remote 2", event: true
});
room.addEvents([eventA, eventB, eventC]);
room.addLiveEvents([eventA]);
room.addPendingEvent(eventB, "TXN1");
room.addLiveEvents([eventC]);
expect(room.timeline).toEqual(
[eventA, eventC, eventB]
[eventA, eventC]
);
expect(room.getPendingEvents()).toEqual(
[eventB]
);
});
it("should sort pending events chronologically if 'chronological'", function() {
it("should add pending events to the timeline if " +
"pendingEventOrdering == 'chronological'", function() {
room = new Room(roomId, {
pendingEventOrdering: "chronological"
});
@@ -1164,50 +1185,84 @@ describe("Room", function() {
var eventC = utils.mkMessage({
room: roomId, user: userA, msg: "remote 2", event: true
});
room.addEvents([eventA, eventB, eventC]);
room.addLiveEvents([eventA]);
room.addPendingEvent(eventB, "TXN1");
room.addLiveEvents([eventC]);
expect(room.timeline).toEqual(
[eventA, eventB, eventC]
);
});
});
it("should treat NOT_SENT events as local echo", function() {
describe("updatePendingEvent", function() {
it("should remove cancelled events from the pending list", function() {
var room = new Room(roomId, {
pendingEventOrdering: "end"
pendingEventOrdering: "detached"
});
var eventA = utils.mkMessage({
room: roomId, user: userA, msg: "remote 1", event: true
room: roomId, user: userA, event: true
});
var eventB = utils.mkMessage({
room: roomId, user: userA, msg: "local 1", event: true
});
eventB.status = EventStatus.NOT_SENT;
var eventC = utils.mkMessage({
room: roomId, user: userA, msg: "remote 2", event: true
});
room.addEvents([eventA, eventB, eventC]);
expect(room.timeline).toEqual(
[eventA, eventC, eventB]
eventA.status = EventStatus.SENDING;
var eventId = eventA.getId();
room.addPendingEvent(eventA, "TXN1");
expect(room.getPendingEvents()).toEqual(
[eventA]
);
// the event has to have been failed or queued before it can be
// cancelled
room.updatePendingEvent(eventA, EventStatus.NOT_SENT);
var callCount = 0;
room.on("Room.localEchoUpdated",
function(event, emitRoom, oldEventId, oldStatus) {
expect(event).toEqual(eventA);
expect(event.status).toEqual(EventStatus.CANCELLED);
expect(emitRoom).toEqual(room);
expect(oldEventId).toEqual(eventId);
expect(oldStatus).toEqual(EventStatus.NOT_SENT);
callCount++;
});
room.updatePendingEvent(eventA, EventStatus.CANCELLED);
expect(room.getPendingEvents()).toEqual([]);
expect(callCount).toEqual(1);
});
it("should treat QUEUED events as local echo", function() {
var room = new Room(roomId, {
pendingEventOrdering: "end"
});
it("should remove cancelled events from the timeline", function() {
var room = new Room(roomId);
var eventA = utils.mkMessage({
room: roomId, user: userA, msg: "remote 1", event: true
room: roomId, user: userA, event: true
});
var eventB = utils.mkMessage({
room: roomId, user: userA, msg: "local 1", event: true
});
eventB.status = EventStatus.QUEUED;
var eventC = utils.mkMessage({
room: roomId, user: userA, msg: "remote 2", event: true
});
room.addEvents([eventA, eventB, eventC]);
expect(room.timeline).toEqual(
[eventA, eventC, eventB]
eventA.status = EventStatus.SENDING;
var eventId = eventA.getId();
room.addPendingEvent(eventA, "TXN1");
expect(room.getLiveTimeline().getEvents()).toEqual(
[eventA]
);
// the event has to have been failed or queued before it can be
// cancelled
room.updatePendingEvent(eventA, EventStatus.NOT_SENT);
var callCount = 0;
room.on("Room.localEchoUpdated",
function(event, emitRoom, oldEventId, oldStatus) {
expect(event).toEqual(eventA);
expect(event.status).toEqual(EventStatus.CANCELLED);
expect(emitRoom).toEqual(room);
expect(oldEventId).toEqual(eventId);
expect(oldStatus).toEqual(EventStatus.NOT_SENT);
callCount++;
});
room.updatePendingEvent(eventA, EventStatus.CANCELLED);
expect(room.getLiveTimeline().getEvents()).toEqual([]);
expect(callCount).toEqual(1);
});
});
});
+38
View File
@@ -418,5 +418,43 @@ describe("TimelineWindow", function() {
}).catch(utils.failTest).done(done);
});
it("should limit the number of unsuccessful pagination requests",
function(done) {
var timeline = createTimeline();
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
var timelineWindow = createWindow(timeline, {windowLimit: 5});
var eventId = timeline.getEvents()[1].getId();
var paginateCount = 0;
client.paginateEventTimeline = function(timeline0, opts) {
expect(timeline0).toBe(timeline);
expect(opts.backwards).toBe(false);
expect(opts.limit).toEqual(2);
paginateCount += 1;
return q(true);
};
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2, true, 3);
}).then(function(success) {
expect(success).toBe(false);
expect(paginateCount).toEqual(3);
var expectedEvents = timeline.getEvents().slice(0, 3);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
}).catch(utils.failTest).done(done);
});
});
});
+68
View File
@@ -132,4 +132,72 @@ describe("utils", function() {
}, ["foo"]); }).not.toThrow();
});
});
describe("deepCompare", function() {
var assert = {
isTrue: function(x) { expect(x).toBe(true); },
isFalse: function(x) { expect(x).toBe(false); },
};
it("should handle primitives", function() {
assert.isTrue(utils.deepCompare(null, null));
assert.isFalse(utils.deepCompare(null, undefined));
assert.isTrue(utils.deepCompare("hi", "hi"));
assert.isTrue(utils.deepCompare(5, 5));
assert.isFalse(utils.deepCompare(5, 10));
});
it("should handle regexps", function() {
assert.isTrue(utils.deepCompare(/abc/, /abc/));
assert.isFalse(utils.deepCompare(/abc/, /123/));
var r = /abc/;
assert.isTrue(utils.deepCompare(r, r));
});
it("should handle dates", function() {
assert.isTrue(utils.deepCompare(new Date("2011-03-31"),
new Date("2011-03-31")));
assert.isFalse(utils.deepCompare(new Date("2011-03-31"),
new Date("1970-01-01")));
});
it("should handle arrays", function() {
assert.isTrue(utils.deepCompare([], []));
assert.isTrue(utils.deepCompare([1, 2], [1, 2]));
assert.isFalse(utils.deepCompare([1, 2], [2, 1]));
assert.isFalse(utils.deepCompare([1, 2], [1, 2, 3]));
});
it("should handle simple objects", function() {
assert.isTrue(utils.deepCompare({}, {}));
assert.isTrue(utils.deepCompare({a: 1, b: 2}, {a: 1, b: 2}));
assert.isTrue(utils.deepCompare({a: 1, b: 2}, {b: 2, a: 1}));
assert.isFalse(utils.deepCompare({a: 1, b: 2}, {a: 1, b: 3}));
assert.isTrue(utils.deepCompare({1: {name: "mhc", age: 28},
2: {name: "arb", age: 26}},
{1: {name: "mhc", age: 28},
2: {name: "arb", age: 26}}));
assert.isFalse(utils.deepCompare({1: {name: "mhc", age: 28},
2: {name: "arb", age: 26}},
{1: {name: "mhc", age: 28},
2: {name: "arb", age: 27}}));
assert.isFalse(utils.deepCompare({}, null));
assert.isFalse(utils.deepCompare({}, undefined));
});
it("should handle functions", function() {
// no two different function is equal really, they capture their
// context variables so even if they have same toString(), they
// won't have same functionality
var func = function(x) { return true; };
var func2 = function(x) { return true; };
assert.isTrue(utils.deepCompare(func, func));
assert.isFalse(utils.deepCompare(func, func2));
assert.isTrue(utils.deepCompare({ a: { b: func } }, { a: { b: func } }));
assert.isFalse(utils.deepCompare({ a: { b: func } }, { a: { b: func2 } }));
});
});
});