diff --git a/git-hooks/pre-commit b/git-hooks/pre-commit new file mode 100755 index 000000000..2bb0e2cad --- /dev/null +++ b/git-hooks/pre-commit @@ -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 diff --git a/lib/client.js b/lib/client.js index 8a78131e1..55416c7a5 100644 --- a/lib/client.js +++ b/lib/client.js @@ -908,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, @@ -1729,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; } @@ -2131,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; diff --git a/lib/models/event-timeline.js b/lib/models/event-timeline.js index 06300972e..4123849ac 100644 --- a/lib/models/event-timeline.js +++ b/lib/models/event-timeline.js @@ -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(); } /** @@ -290,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 diff --git a/lib/models/room.js b/lib/models/room.js index 9e1d58605..ece164ebd 100644 --- a/lib/models/room.js +++ b/lib/models/room.js @@ -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 @@ -416,8 +428,8 @@ Room.prototype.addTimeline = function() { * (oldest) instead of the end (newest) of the timeline. If true, the oldest * event will be the last 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 * @@ -427,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 : @@ -529,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; } @@ -545,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; @@ -601,69 +616,95 @@ 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 now = Date.now(); - for (var i = 0; i < events.length; i++) { - if (events[i].getType() === "m.room.redaction") { - var redactId = events[i].event.redacts; +Room.prototype._addLiveEvent = function(event, duplicateStrategy) { + if (event.getType() === "m.room.redaction") { + var redactId = event.event.redacts; - // 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); + // 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); - // 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. + // 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. + } + + if (event.getUnsigned().transaction_id) { + var existingEvent = this._txnToEvent[event.getUnsigned().transaction_id]; + if (existingEvent) { + // remote echo of an event we sent earlier + this._handleRemoteEcho(event, existingEvent); + return; + } + } + + 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 + ); + + if (!tlEvents[j].encryptedType) { + tlEvents[j] = event; + } + + // XXX: we need to fire an event when this happens. + break; + } } - - // 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. + } else { + debuglog("Room._addLiveEvent: ignoring duplicate event " + + event.getId()); } + return; + } - if (events[i].getUnsigned().transaction_id) { - var existingEvent = this._txnToEvent[events[i].getUnsigned().transaction_id]; - if (existingEvent) { - // remote echo of an event we sent earlier - this._handleRemoteEcho(events[i], existingEvent); - continue; - } - } + // TODO: pass through filter to see if this should be added to the timeline. + this._addEventToTimeline(event, this._liveTimeline, false); - 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); - } + // 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); - // 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 (events[i].sender) { - this.addReceipt(synthesizeReceipt( - events[i].sender.userId, events[i], "m.read" - ), true); + // also, any live events from a user should 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 comment it out for now. - // also, any live events from a user should 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 comment it out for now. - - // var user = this.currentState.getMember(events[i].sender.userId); - // user.lastActiveAgo = 0; - // user.lastPresenceTs = now; - } + // var user = this.currentState.getMember(events[i].sender.userId); + // user.lastActiveAgo = 0; + // user.lastPresenceTs = Date.now(); } }; @@ -866,20 +907,36 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) { * 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 only. + * * @throws If duplicateStrategy 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]); @@ -890,41 +947,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); } } }; diff --git a/lib/store/webstorage.js b/lib/store/webstorage.js index 5cd452033..76d83924e 100644 --- a/lib/store/webstorage.js +++ b/lib/store/webstorage.js @@ -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. diff --git a/lib/sync.js b/lib/sync.js index edc0ec78d..8ac1aaf1d 100644 --- a/lib/sync.js +++ b/lib/sync.js @@ -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 @@ -703,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); @@ -979,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); }; /** diff --git a/package.json b/package.json index 515f95be7..a9f16130f 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "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", diff --git a/spec/integ/matrix-client-event-emitter.spec.js b/spec/integ/matrix-client-event-emitter.spec.js index 52d3dbecc..82fbd1bd1 100644 --- a/spec/integ/matrix-client-event-emitter.spec.js +++ b/spec/integ/matrix-client-event-emitter.spec.js @@ -24,6 +24,7 @@ describe("MatrixClient events", function() { afterEach(function() { httpBackend.verifyNoOutstandingExpectation(); + client.stopClient(); }); describe("emissions", function() { diff --git a/spec/integ/matrix-client-event-timeline.spec.js b/spec/integ/matrix-client-event-timeline.spec.js index 799e2bd49..db94541fb 100644 --- a/spec/integ/matrix-client-event-timeline.spec.js +++ b/spec/integ/matrix-client-event-timeline.spec.js @@ -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() { diff --git a/spec/integ/matrix-client-methods.spec.js b/spec/integ/matrix-client-methods.spec.js index 945591c7c..fb6567efe 100644 --- a/spec/integ/matrix-client-methods.spec.js +++ b/spec/integ/matrix-client-methods.spec.js @@ -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 }) diff --git a/spec/integ/matrix-client-opts.spec.js b/spec/integ/matrix-client-opts.spec.js index 347e3acd3..ca5f5fd50 100644 --- a/spec/integ/matrix-client-opts.spec.js +++ b/spec/integ/matrix-client-opts.spec.js @@ -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, { diff --git a/spec/integ/matrix-client-room-timeline.spec.js b/spec/integ/matrix-client-room-timeline.spec.js index 8d5125f0c..c2ef9adb5 100644 --- a/spec/integ/matrix-client-room-timeline.spec.js +++ b/spec/integ/matrix-client-room-timeline.spec.js @@ -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); }); diff --git a/spec/integ/matrix-client-syncing.spec.js b/spec/integ/matrix-client-syncing.spec.js index c8d981d73..b6013f84a 100644 --- a/spec/integ/matrix-client-syncing.spec.js +++ b/spec/integ/matrix-client-syncing.spec.js @@ -32,6 +32,7 @@ describe("MatrixClient syncing", function() { afterEach(function() { httpBackend.verifyNoOutstandingExpectation(); + client.stopClient(); }); describe("startClient", function() { diff --git a/spec/mock-request.js b/spec/mock-request.js index 867e4eb1c..6d66e2861 100644 --- a/spec/mock-request.js +++ b/spec/mock-request.js @@ -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 = { diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index fd365d069..83d170109 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -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,39 +126,13 @@ 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]); }); - }); - describe("addEventsToTimeline", function() { - var events = [ - utils.mkMessage({ - room: roomId, user: userA, msg: "changing room name", event: true - }), - utils.mkEvent({ - type: "m.room.name", room: roomId, user: userA, event: true, - content: { name: "New Room Name" } - }) - ]; - - 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 be able to add events to the start", function() { - room.addEventsToTimeline(events, true); - 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", + it("should emit 'Room.timeline' events", function() { var callCount = 0; room.on("Room.timeline", function(event, emitRoom, toStart) { @@ -168,99 +142,10 @@ describe("Room", function() { expect(emitRoom).toEqual(room); expect(toStart).toBeFalsy(); }); - room.addEventsToTimeline(events); + room.addLiveEvents(events); expect(callCount).toEqual(2); }); - it("should emit 'Room.timeline' events when added to the start", - 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).toBe(true); - }); - room.addEventsToTimeline(events, true); - expect(callCount).toEqual(2); - }); - - it("should set event.sender for new and old events", function() { - var sentinel = { - userId: userA, - membership: "join", - name: "Alice" - }; - var oldSentinel = { - userId: userA, - membership: "join", - name: "Old Alice" - }; - room.currentState.getSentinelMember.andCallFake(function(uid) { - if (uid === userA) { - return sentinel; - } - return null; - }); - room.oldState.getSentinelMember.andCallFake(function(uid) { - if (uid === userA) { - return oldSentinel; - } - return null; - }); - - var newEv = utils.mkEvent({ - type: "m.room.name", room: roomId, user: userA, event: true, - content: { name: "New Room Name" } - }); - var oldEv = utils.mkEvent({ - type: "m.room.name", room: roomId, user: userA, event: true, - content: { name: "Old Room Name" } - }); - room.addEventsToTimeline([newEv]); - expect(newEv.sender).toEqual(sentinel); - room.addEventsToTimeline([oldEv], true); - expect(oldEv.sender).toEqual(oldSentinel); - }); - - it("should set event.target for new and old m.room.member events", - function() { - var sentinel = { - userId: userA, - membership: "join", - name: "Alice" - }; - var oldSentinel = { - userId: userA, - membership: "join", - name: "Old Alice" - }; - room.currentState.getSentinelMember.andCallFake(function(uid) { - if (uid === userA) { - return sentinel; - } - return null; - }); - room.oldState.getSentinelMember.andCallFake(function(uid) { - if (uid === userA) { - return oldSentinel; - } - return null; - }); - - var newEv = utils.mkMembership({ - room: roomId, mship: "invite", user: userB, skey: userA, event: true - }); - var oldEv = utils.mkMembership({ - room: roomId, mship: "ban", user: userB, skey: userA, event: true - }); - room.addEventsToTimeline([newEv]); - expect(newEv.target).toEqual(sentinel); - room.addEventsToTimeline([oldEv], true); - expect(oldEv.target).toEqual(oldSentinel); - }); - it("should call setStateEvents on the right RoomState with the right " + "forwardLooking value for new events", function() { var events = [ @@ -274,7 +159,7 @@ describe("Room", function() { } }) ]; - room.addEventsToTimeline(events); + room.addLiveEvents(events); expect(room.currentState.setStateEvents).toHaveBeenCalledWith( [events[0]] ); @@ -286,33 +171,6 @@ describe("Room", function() { 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 = [ - 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, true); - expect(room.oldState.setStateEvents).toHaveBeenCalledWith( - [events[0]] - ); - expect(room.oldState.setStateEvents).toHaveBeenCalledWith( - [events[1]] - ); - expect(events[0].forwardLooking).toBe(false); - 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, @@ -325,7 +183,7 @@ describe("Room", function() { } return null; }); - room.addEventsToTimeline(events); + room.addLiveEvents(events); expect(room.getEventReadUpTo(userA)).toEqual(events[1].getId()); }); @@ -370,13 +228,155 @@ describe("Room", function() { expect(room.timeline.length).toEqual(1); // then the remoteEvent - room.addEventsToTimeline([remoteEvent]); + room.addLiveEvents([remoteEvent]); expect(room.timeline.length).toEqual(1); expect(callCount).toEqual(2); }); }); + describe("addEventsToTimeline", function() { + var events = [ + utils.mkMessage({ + room: roomId, user: userA, msg: "changing room name", event: true + }), + utils.mkEvent({ + type: "m.room.name", room: roomId, user: userA, event: true, + content: { name: "New Room Name" } + }) + ]; + + 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.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 start", + 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).toBe(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, + membership: "join", + name: "Alice" + }; + var oldSentinel = { + userId: userA, + membership: "join", + name: "Old Alice" + }; + room.currentState.getSentinelMember.andCallFake(function(uid) { + if (uid === userA) { + return sentinel; + } + return null; + }); + room.oldState.getSentinelMember.andCallFake(function(uid) { + if (uid === userA) { + return oldSentinel; + } + return null; + }); + + var newEv = utils.mkEvent({ + type: "m.room.name", room: roomId, user: userA, event: true, + content: { name: "New Room Name" } + }); + var oldEv = utils.mkEvent({ + type: "m.room.name", room: roomId, user: userA, event: true, + content: { name: "Old Room Name" } + }); + room.addLiveEvents([newEv]); + expect(newEv.sender).toEqual(sentinel); + room.addEventsToTimeline([oldEv], true, room.getLiveTimeline()); + expect(oldEv.sender).toEqual(oldSentinel); + }); + + it("should set event.target for new and old m.room.member events", + function() { + var sentinel = { + userId: userA, + membership: "join", + name: "Alice" + }; + var oldSentinel = { + userId: userA, + membership: "join", + name: "Old Alice" + }; + room.currentState.getSentinelMember.andCallFake(function(uid) { + if (uid === userA) { + return sentinel; + } + return null; + }); + room.oldState.getSentinelMember.andCallFake(function(uid) { + if (uid === userA) { + return oldSentinel; + } + return null; + }); + + var newEv = utils.mkMembership({ + room: roomId, mship: "invite", user: userB, skey: userA, event: true + }); + var oldEv = utils.mkMembership({ + room: roomId, mship: "ban", user: userB, skey: userA, event: true + }); + room.addLiveEvents([newEv]); + expect(newEv.target).toEqual(sentinel); + 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 old 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, true, room.getLiveTimeline()); + expect(room.oldState.setStateEvents).toHaveBeenCalledWith( + [events[0]] + ); + expect(room.oldState.setStateEvents).toHaveBeenCalledWith( + [events[1]] + ); + expect(events[0].forwardLooking).toBe(false); + expect(events[1].forwardLooking).toBe(false); + expect(room.currentState.setStateEvents).not.toHaveBeenCalled(); + }); + }); + var resetTimelineTests = function(timelineSupport) { var events = [ utils.mkMessage({ @@ -397,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); @@ -410,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( @@ -443,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(); @@ -477,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())) @@ -496,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())) @@ -510,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())) @@ -521,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); @@ -1068,7 +1068,7 @@ describe("Room", function() { }), ]; - room.addEventsToTimeline(events); + room.addLiveEvents(events); var ts = 13787898424; // check it initialises correctly @@ -1159,9 +1159,9 @@ describe("Room", function() { var eventC = utils.mkMessage({ room: roomId, user: userA, msg: "remote 2", event: true }); - room.addEvents([eventA]); + room.addLiveEvents([eventA]); room.addPendingEvent(eventB, "TXN1"); - room.addEvents([eventC]); + room.addLiveEvents([eventC]); expect(room.timeline).toEqual( [eventA, eventC] ); @@ -1185,9 +1185,9 @@ describe("Room", function() { var eventC = utils.mkMessage({ room: roomId, user: userA, msg: "remote 2", event: true }); - room.addEvents([eventA]); + room.addLiveEvents([eventA]); room.addPendingEvent(eventB, "TXN1"); - room.addEvents([eventC]); + room.addLiveEvents([eventC]); expect(room.timeline).toEqual( [eventA, eventB, eventC] );