UNPKG

83.2 kBJavaScriptView Raw
1/*
2Copyright 2015, 2016 OpenMarket Ltd
3Copyright 2017 Vector Creations Ltd
4Copyright 2018 New Vector Ltd
5
6Licensed under the Apache License, Version 2.0 (the "License");
7you may not use this file except in compliance with the License.
8You may obtain a copy of the License at
9
10 http://www.apache.org/licenses/LICENSE-2.0
11
12Unless required by applicable law or agreed to in writing, software
13distributed under the License is distributed on an "AS IS" BASIS,
14WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15See the License for the specific language governing permissions and
16limitations under the License.
17*/
18"use strict";
19
20/*
21 * TODO:
22 * This class mainly serves to take all the syncing logic out of client.js and
23 * into a separate file. It's all very fluid, and this class gut wrenches a lot
24 * of MatrixClient props (e.g. _http). Given we want to support WebSockets as
25 * an alternative syncing API, we may want to have a proper syncing interface
26 * for HTTP and WS at some point.
27 */
28
29var _stringify = require("babel-runtime/core-js/json/stringify");
30
31var _stringify2 = _interopRequireDefault(_stringify);
32
33var _keys = require("babel-runtime/core-js/object/keys");
34
35var _keys2 = _interopRequireDefault(_keys);
36
37var _getIterator2 = require("babel-runtime/core-js/get-iterator");
38
39var _getIterator3 = _interopRequireDefault(_getIterator2);
40
41var _regenerator = require("babel-runtime/regenerator");
42
43var _regenerator2 = _interopRequireDefault(_regenerator);
44
45var _bluebird = require("bluebird");
46
47var _bluebird2 = _interopRequireDefault(_bluebird);
48
49var _errors = require("./errors");
50
51function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
52
53var User = require("./models/user");
54var Room = require("./models/room");
55var Group = require('./models/group');
56var utils = require("./utils");
57var Filter = require("./filter");
58var EventTimeline = require("./models/event-timeline");
59
60var DEBUG = true;
61
62// /sync requests allow you to set a timeout= but the request may continue
63// beyond that and wedge forever, so we need to track how long we are willing
64// to keep open the connection. This constant is *ADDED* to the timeout= value
65// to determine the max time we're willing to wait.
66var BUFFER_PERIOD_MS = 80 * 1000;
67
68// Number of consecutive failed syncs that will lead to a syncState of ERROR as opposed
69// to RECONNECTING. This is needed to inform the client of server issues when the
70// keepAlive is successful but the server /sync fails.
71var FAILED_SYNC_ERROR_THRESHOLD = 3;
72
73function getFilterName(userId, suffix) {
74 // scope this on the user ID because people may login on many accounts
75 // and they all need to be stored!
76 return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
77}
78
79function debuglog() {
80 var _console;
81
82 if (!DEBUG) {
83 return;
84 }
85 (_console = console).log.apply(_console, arguments);
86}
87
88/**
89 * <b>Internal class - unstable.</b>
90 * Construct an entity which is able to sync with a homeserver.
91 * @constructor
92 * @param {MatrixClient} client The matrix client instance to use.
93 * @param {Object} opts Config options
94 * @param {module:crypto=} opts.crypto Crypto manager
95 * @param {Function=} opts.canResetEntireTimeline A function which is called
96 * with a room ID and returns a boolean. It should return 'true' if the SDK can
97 * SAFELY remove events from this room. It may not be safe to remove events if
98 * there are other references to the timelines for this room.
99 * Default: returns false.
100 * @param {Boolean=} opts.disablePresence True to perform syncing without automatically
101 * updating presence.
102 */
103function SyncApi(client, opts) {
104 this.client = client;
105 opts = opts || {};
106 opts.initialSyncLimit = opts.initialSyncLimit === undefined ? 8 : opts.initialSyncLimit;
107 opts.resolveInvitesToProfiles = opts.resolveInvitesToProfiles || false;
108 opts.pollTimeout = opts.pollTimeout || 30 * 1000;
109 opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
110 if (!opts.canResetEntireTimeline) {
111 opts.canResetEntireTimeline = function (roomId) {
112 return false;
113 };
114 }
115 this.opts = opts;
116 this._peekRoomId = null;
117 this._currentSyncRequest = null;
118 this._syncState = null;
119 this._syncStateData = null; // additional data (eg. error object for failed sync)
120 this._catchingUp = false;
121 this._running = false;
122 this._keepAliveTimer = null;
123 this._connectionReturnedDefer = null;
124 this._notifEvents = []; // accumulator of sync events in the current sync response
125 this._failedSyncCount = 0; // Number of consecutive failed /sync requests
126 this._storeIsInvalid = false; // flag set if the store needs to be cleared before we can start
127
128 if (client.getNotifTimelineSet()) {
129 client.reEmitter.reEmit(client.getNotifTimelineSet(), ["Room.timeline", "Room.timelineReset"]);
130 }
131}
132
133/**
134 * @param {string} roomId
135 * @return {Room}
136 */
137SyncApi.prototype.createRoom = function (roomId) {
138 var client = this.client;
139 var room = new Room(roomId, client, client.getUserId(), {
140 lazyLoadMembers: this.opts.lazyLoadMembers,
141 pendingEventOrdering: this.opts.pendingEventOrdering,
142 timelineSupport: client.timelineSupport
143 });
144 client.reEmitter.reEmit(room, ["Room.name", "Room.timeline", "Room.redaction", "Room.receipt", "Room.tags", "Room.timelineReset", "Room.localEchoUpdated", "Room.accountData", "Room.myMembership"]);
145 this._registerStateListeners(room);
146 return room;
147};
148
149/**
150 * @param {string} groupId
151 * @return {Group}
152 */
153SyncApi.prototype.createGroup = function (groupId) {
154 var client = this.client;
155 var group = new Group(groupId);
156 client.reEmitter.reEmit(group, ["Group.profile", "Group.myMembership"]);
157 client.store.storeGroup(group);
158 return group;
159};
160
161/**
162 * @param {Room} room
163 * @private
164 */
165SyncApi.prototype._registerStateListeners = function (room) {
166 var client = this.client;
167 // we need to also re-emit room state and room member events, so hook it up
168 // to the client now. We need to add a listener for RoomState.members in
169 // order to hook them correctly. (TODO: find a better way?)
170 client.reEmitter.reEmit(room.currentState, ["RoomState.events", "RoomState.members", "RoomState.newMember"]);
171 room.currentState.on("RoomState.newMember", function (event, state, member) {
172 member.user = client.getUser(member.userId);
173 client.reEmitter.reEmit(member, ["RoomMember.name", "RoomMember.typing", "RoomMember.powerLevel", "RoomMember.membership"]);
174 });
175};
176
177/**
178 * @param {Room} room
179 * @private
180 */
181SyncApi.prototype._deregisterStateListeners = function (room) {
182 // could do with a better way of achieving this.
183 room.currentState.removeAllListeners("RoomState.events");
184 room.currentState.removeAllListeners("RoomState.members");
185 room.currentState.removeAllListeners("RoomState.newMember");
186};
187
188/**
189 * Sync rooms the user has left.
190 * @return {Promise} Resolved when they've been added to the store.
191 */
192SyncApi.prototype.syncLeftRooms = function () {
193 var client = this.client;
194 var self = this;
195
196 // grab a filter with limit=1 and include_leave=true
197 var filter = new Filter(this.client.credentials.userId);
198 filter.setTimelineLimit(1);
199 filter.setIncludeLeaveRooms(true);
200
201 var localTimeoutMs = this.opts.pollTimeout + BUFFER_PERIOD_MS;
202 var qps = {
203 timeout: 0 // don't want to block since this is a single isolated req
204 };
205
206 return client.getOrCreateFilter(getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter).then(function (filterId) {
207 qps.filter = filterId;
208 return client._http.authedRequest(undefined, "GET", "/sync", qps, undefined, localTimeoutMs);
209 }).then(function (data) {
210 var leaveRooms = [];
211 if (data.rooms && data.rooms.leave) {
212 leaveRooms = self._mapSyncResponseToRoomArray(data.rooms.leave);
213 }
214 var rooms = [];
215 leaveRooms.forEach(function (leaveObj) {
216 var room = leaveObj.room;
217 rooms.push(room);
218 if (!leaveObj.isBrandNewRoom) {
219 // the intention behind syncLeftRooms is to add in rooms which were
220 // *omitted* from the initial /sync. Rooms the user were joined to
221 // but then left whilst the app is running will appear in this list
222 // and we do not want to bother with them since they will have the
223 // current state already (and may get dupe messages if we add
224 // yet more timeline events!), so skip them.
225 // NB: When we persist rooms to localStorage this will be more
226 // complicated...
227 return;
228 }
229 leaveObj.timeline = leaveObj.timeline || {};
230 var timelineEvents = self._mapSyncEventsFormat(leaveObj.timeline, room);
231 var stateEvents = self._mapSyncEventsFormat(leaveObj.state, room);
232
233 // set the back-pagination token. Do this *before* adding any
234 // events so that clients can start back-paginating.
235 room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch, EventTimeline.BACKWARDS);
236
237 self._processRoomEvents(room, stateEvents, timelineEvents);
238
239 room.recalculate();
240 client.store.storeRoom(room);
241 client.emit("Room", room);
242
243 self._processEventsForNotifs(room, timelineEvents);
244 });
245 return rooms;
246 });
247};
248
249/**
250 * Peek into a room. This will result in the room in question being synced so it
251 * is accessible via getRooms(). Live updates for the room will be provided.
252 * @param {string} roomId The room ID to peek into.
253 * @return {Promise} A promise which resolves once the room has been added to the
254 * store.
255 */
256SyncApi.prototype.peek = function (roomId) {
257 var self = this;
258 var client = this.client;
259 this._peekRoomId = roomId;
260 return this.client.roomInitialSync(roomId, 20).then(function (response) {
261 // make sure things are init'd
262 response.messages = response.messages || {};
263 response.messages.chunk = response.messages.chunk || [];
264 response.state = response.state || [];
265
266 var peekRoom = self.createRoom(roomId);
267
268 // FIXME: Mostly duplicated from _processRoomEvents but not entirely
269 // because "state" in this API is at the BEGINNING of the chunk
270 var oldStateEvents = utils.map(utils.deepCopy(response.state), client.getEventMapper());
271 var stateEvents = utils.map(response.state, client.getEventMapper());
272 var messages = utils.map(response.messages.chunk, client.getEventMapper());
273
274 // XXX: copypasted from /sync until we kill off this
275 // minging v1 API stuff)
276 // handle presence events (User objects)
277 if (response.presence && utils.isArray(response.presence)) {
278 response.presence.map(client.getEventMapper()).forEach(function (presenceEvent) {
279 var user = client.store.getUser(presenceEvent.getContent().user_id);
280 if (user) {
281 user.setPresenceEvent(presenceEvent);
282 } else {
283 user = createNewUser(client, presenceEvent.getContent().user_id);
284 user.setPresenceEvent(presenceEvent);
285 client.store.storeUser(user);
286 }
287 client.emit("event", presenceEvent);
288 });
289 }
290
291 // set the pagination token before adding the events in case people
292 // fire off pagination requests in response to the Room.timeline
293 // events.
294 if (response.messages.start) {
295 peekRoom.oldState.paginationToken = response.messages.start;
296 }
297
298 // set the state of the room to as it was after the timeline executes
299 peekRoom.oldState.setStateEvents(oldStateEvents);
300 peekRoom.currentState.setStateEvents(stateEvents);
301
302 self._resolveInvites(peekRoom);
303 peekRoom.recalculate();
304
305 // roll backwards to diverge old state. addEventsToTimeline
306 // will overwrite the pagination token, so make sure it overwrites
307 // it with the right thing.
308 peekRoom.addEventsToTimeline(messages.reverse(), true, peekRoom.getLiveTimeline(), response.messages.start);
309
310 client.store.storeRoom(peekRoom);
311 client.emit("Room", peekRoom);
312
313 self._peekPoll(peekRoom);
314 return peekRoom;
315 });
316};
317
318/**
319 * Stop polling for updates in the peeked room. NOPs if there is no room being
320 * peeked.
321 */
322SyncApi.prototype.stopPeeking = function () {
323 this._peekRoomId = null;
324};
325
326/**
327 * Do a peek room poll.
328 * @param {Room} peekRoom
329 * @param {string} token from= token
330 */
331SyncApi.prototype._peekPoll = function (peekRoom, token) {
332 if (this._peekRoomId !== peekRoom.roomId) {
333 debuglog("Stopped peeking in room %s", peekRoom.roomId);
334 return;
335 }
336
337 var self = this;
338 // FIXME: gut wrenching; hard-coded timeout values
339 this.client._http.authedRequest(undefined, "GET", "/events", {
340 room_id: peekRoom.roomId,
341 timeout: 30 * 1000,
342 from: token
343 }, undefined, 50 * 1000).done(function (res) {
344 if (self._peekRoomId !== peekRoom.roomId) {
345 debuglog("Stopped peeking in room %s", peekRoom.roomId);
346 return;
347 }
348 // We have a problem that we get presence both from /events and /sync
349 // however, /sync only returns presence for users in rooms
350 // you're actually joined to.
351 // in order to be sure to get presence for all of the users in the
352 // peeked room, we handle presence explicitly here. This may result
353 // in duplicate presence events firing for some users, which is a
354 // performance drain, but such is life.
355 // XXX: copypasted from /sync until we can kill this minging v1 stuff.
356
357 res.chunk.filter(function (e) {
358 return e.type === "m.presence";
359 }).map(self.client.getEventMapper()).forEach(function (presenceEvent) {
360 var user = self.client.store.getUser(presenceEvent.getContent().user_id);
361 if (user) {
362 user.setPresenceEvent(presenceEvent);
363 } else {
364 user = createNewUser(self.client, presenceEvent.getContent().user_id);
365 user.setPresenceEvent(presenceEvent);
366 self.client.store.storeUser(user);
367 }
368 self.client.emit("event", presenceEvent);
369 });
370
371 // strip out events which aren't for the given room_id (e.g presence)
372 var events = res.chunk.filter(function (e) {
373 return e.room_id === peekRoom.roomId;
374 }).map(self.client.getEventMapper());
375
376 peekRoom.addLiveEvents(events);
377 self._peekPoll(peekRoom, res.end);
378 }, function (err) {
379 console.error("[%s] Peek poll failed: %s", peekRoom.roomId, err);
380 setTimeout(function () {
381 self._peekPoll(peekRoom, token);
382 }, 30 * 1000);
383 });
384};
385
386/**
387 * Returns the current state of this sync object
388 * @see module:client~MatrixClient#event:"sync"
389 * @return {?String}
390 */
391SyncApi.prototype.getSyncState = function () {
392 return this._syncState;
393};
394
395/**
396 * Returns the additional data object associated with
397 * the current sync state, or null if there is no
398 * such data.
399 * Sync errors, if available, are put in the 'error' key of
400 * this object.
401 * @return {?Object}
402 */
403SyncApi.prototype.getSyncStateData = function () {
404 return this._syncStateData;
405};
406
407SyncApi.prototype.recoverFromSyncStartupError = function () {
408 var _ref = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee(savedSyncPromise, err) {
409 var keepaliveProm;
410 return _regenerator2.default.wrap(function _callee$(_context) {
411 while (1) {
412 switch (_context.prev = _context.next) {
413 case 0:
414 _context.next = 2;
415 return (0, _bluebird.resolve)(savedSyncPromise);
416
417 case 2:
418 keepaliveProm = this._startKeepAlives();
419
420 this._updateSyncState("ERROR", { error: err });
421 _context.next = 6;
422 return (0, _bluebird.resolve)(keepaliveProm);
423
424 case 6:
425 case "end":
426 return _context.stop();
427 }
428 }
429 }, _callee, this);
430 }));
431
432 return function (_x, _x2) {
433 return _ref.apply(this, arguments);
434 };
435}();
436
437/**
438 * Is the lazy loading option different than in previous session?
439 * @param {bool} lazyLoadMembers current options for lazy loading
440 * @return {bool} whether or not the option has changed compared to the previous session */
441SyncApi.prototype._wasLazyLoadingToggled = function () {
442 var _ref2 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee2(lazyLoadMembers) {
443 var lazyLoadMembersBefore, isStoreNewlyCreated, prevClientOptions;
444 return _regenerator2.default.wrap(function _callee2$(_context2) {
445 while (1) {
446 switch (_context2.prev = _context2.next) {
447 case 0:
448 lazyLoadMembers = !!lazyLoadMembers;
449 // assume it was turned off before
450 // if we don't know any better
451 lazyLoadMembersBefore = false;
452 _context2.next = 4;
453 return (0, _bluebird.resolve)(this.client.store.isNewlyCreated());
454
455 case 4:
456 isStoreNewlyCreated = _context2.sent;
457
458 if (isStoreNewlyCreated) {
459 _context2.next = 11;
460 break;
461 }
462
463 _context2.next = 8;
464 return (0, _bluebird.resolve)(this.client.store.getClientOptions());
465
466 case 8:
467 prevClientOptions = _context2.sent;
468
469 if (prevClientOptions) {
470 lazyLoadMembersBefore = !!prevClientOptions.lazyLoadMembers;
471 }
472 return _context2.abrupt("return", lazyLoadMembersBefore !== lazyLoadMembers);
473
474 case 11:
475 return _context2.abrupt("return", false);
476
477 case 12:
478 case "end":
479 return _context2.stop();
480 }
481 }
482 }, _callee2, this);
483 }));
484
485 return function (_x3) {
486 return _ref2.apply(this, arguments);
487 };
488}();
489
490/**
491 * Main entry point
492 */
493SyncApi.prototype.sync = function () {
494 var _this = this;
495
496 // We need to do one-off checks before we can begin the /sync loop.
497 // These are:
498 // 1) We need to get push rules so we can check if events should bing as we get
499 // them from /sync.
500 // 2) We need to get/create a filter which we can use for /sync.
501 // 3) We need to check the lazy loading option matches what was used in the
502 // stored sync. If it doesn't, we can't use the stored sync.
503
504 var getPushRules = function () {
505 var _ref3 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee3() {
506 var result;
507 return _regenerator2.default.wrap(function _callee3$(_context3) {
508 while (1) {
509 switch (_context3.prev = _context3.next) {
510 case 0:
511 _context3.prev = 0;
512 _context3.next = 3;
513 return (0, _bluebird.resolve)(client.getPushRules());
514
515 case 3:
516 result = _context3.sent;
517
518 debuglog("Got push rules");
519
520 client.pushRules = result;
521 _context3.next = 14;
522 break;
523
524 case 8:
525 _context3.prev = 8;
526 _context3.t0 = _context3["catch"](0);
527 _context3.next = 12;
528 return (0, _bluebird.resolve)(self.recoverFromSyncStartupError(savedSyncPromise, _context3.t0));
529
530 case 12:
531 getPushRules();
532 return _context3.abrupt("return");
533
534 case 14:
535 checkLazyLoadStatus(); // advance to the next stage
536
537 case 15:
538 case "end":
539 return _context3.stop();
540 }
541 }
542 }, _callee3, this, [[0, 8]]);
543 }));
544
545 return function getPushRules() {
546 return _ref3.apply(this, arguments);
547 };
548 }();
549
550 var getFilter = function () {
551 var _ref5 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee5() {
552 var filter, filterId;
553 return _regenerator2.default.wrap(function _callee5$(_context5) {
554 while (1) {
555 switch (_context5.prev = _context5.next) {
556 case 0:
557 filter = void 0;
558
559 if (self.opts.filter) {
560 filter = self.opts.filter;
561 } else {
562 filter = new Filter(client.credentials.userId);
563 filter.setTimelineLimit(self.opts.initialSyncLimit);
564 }
565
566 filterId = void 0;
567 _context5.prev = 3;
568 _context5.next = 6;
569 return (0, _bluebird.resolve)(client.getOrCreateFilter(getFilterName(client.credentials.userId), filter));
570
571 case 6:
572 filterId = _context5.sent;
573 _context5.next = 15;
574 break;
575
576 case 9:
577 _context5.prev = 9;
578 _context5.t0 = _context5["catch"](3);
579 _context5.next = 13;
580 return (0, _bluebird.resolve)(self.recoverFromSyncStartupError(savedSyncPromise, _context5.t0));
581
582 case 13:
583 getFilter();
584 return _context5.abrupt("return");
585
586 case 15:
587 // reset the notifications timeline to prepare it to paginate from
588 // the current point in time.
589 // The right solution would be to tie /sync pagination tokens into
590 // /notifications API somehow.
591 client.resetNotifTimelineSet();
592
593 if (self._currentSyncRequest === null) {
594 // Send this first sync request here so we can then wait for the saved
595 // sync data to finish processing before we process the results of this one.
596 console.log("Sending first sync request...");
597 self._currentSyncRequest = self._doSyncRequest({ filterId: filterId }, savedSyncToken);
598 }
599
600 // Now wait for the saved sync to finish...
601 _context5.next = 19;
602 return (0, _bluebird.resolve)(savedSyncPromise);
603
604 case 19:
605 self._sync({ filterId: filterId });
606
607 case 20:
608 case "end":
609 return _context5.stop();
610 }
611 }
612 }, _callee5, this, [[3, 9]]);
613 }));
614
615 return function getFilter() {
616 return _ref5.apply(this, arguments);
617 };
618 }();
619
620 var client = this.client;
621 var self = this;
622
623 this._running = true;
624
625 if (global.document) {
626 this._onOnlineBound = this._onOnline.bind(this);
627 global.document.addEventListener("online", this._onOnlineBound, false);
628 }
629
630 var savedSyncPromise = _bluebird2.default.resolve();
631 var savedSyncToken = null;
632
633 var checkLazyLoadStatus = function () {
634 var _ref4 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee4() {
635 var supported, shouldClear, reason, error;
636 return _regenerator2.default.wrap(function _callee4$(_context4) {
637 while (1) {
638 switch (_context4.prev = _context4.next) {
639 case 0:
640 if (_this.opts.lazyLoadMembers && client.isGuest()) {
641 _this.opts.lazyLoadMembers = false;
642 }
643
644 if (!_this.opts.lazyLoadMembers) {
645 _context4.next = 13;
646 break;
647 }
648
649 _context4.next = 4;
650 return (0, _bluebird.resolve)(client.doesServerSupportLazyLoading());
651
652 case 4:
653 supported = _context4.sent;
654
655 if (!supported) {
656 _context4.next = 11;
657 break;
658 }
659
660 _context4.next = 8;
661 return (0, _bluebird.resolve)(client.createFilter(Filter.LAZY_LOADING_SYNC_FILTER));
662
663 case 8:
664 _this.opts.filter = _context4.sent;
665 _context4.next = 13;
666 break;
667
668 case 11:
669 console.log("LL: lazy loading requested but not supported " + "by server, so disabling");
670 _this.opts.lazyLoadMembers = false;
671
672 case 13:
673 _context4.next = 15;
674 return (0, _bluebird.resolve)(_this._wasLazyLoadingToggled(_this.opts.lazyLoadMembers));
675
676 case 15:
677 shouldClear = _context4.sent;
678
679 if (!shouldClear) {
680 _context4.next = 23;
681 break;
682 }
683
684 _this._storeIsInvalid = true;
685 reason = _errors.InvalidStoreError.TOGGLED_LAZY_LOADING;
686 error = new _errors.InvalidStoreError(reason, !!_this.opts.lazyLoadMembers);
687
688 _this._updateSyncState("ERROR", { error: error });
689 // bail out of the sync loop now: the app needs to respond to this error.
690 // we leave the state as 'ERROR' which isn't great since this normally means
691 // we're retrying. The client must be stopped before clearing the stores anyway
692 // so the app should stop the client, clear the store and start it again.
693 console.warn("InvalidStoreError: store is not usable: stopping sync.");
694 return _context4.abrupt("return");
695
696 case 23:
697 if (_this.opts.lazyLoadMembers && _this.opts.crypto) {
698 _this.opts.crypto.enableLazyLoading();
699 }
700 _context4.next = 26;
701 return (0, _bluebird.resolve)(_this.client._storeClientOptions());
702
703 case 26:
704
705 getFilter(); // Now get the filter and start syncing
706
707 case 27:
708 case "end":
709 return _context4.stop();
710 }
711 }
712 }, _callee4, _this);
713 }));
714
715 return function checkLazyLoadStatus() {
716 return _ref4.apply(this, arguments);
717 };
718 }();
719
720 if (client.isGuest()) {
721 // no push rules for guests, no access to POST filter for guests.
722 self._sync({});
723 } else {
724 // Pull the saved sync token out first, before the worker starts sending
725 // all the sync data which could take a while. This will let us send our
726 // first incremental sync request before we've processed our saved data.
727 savedSyncPromise = client.store.getSavedSyncToken().then(function (tok) {
728 savedSyncToken = tok;
729 return client.store.getSavedSync();
730 }).then(function (savedSync) {
731 if (savedSync) {
732 return self._syncFromCache(savedSync);
733 }
734 });
735 // Now start the first incremental sync request: this can also
736 // take a while so if we set it going now, we can wait for it
737 // to finish while we process our saved sync data.
738 getPushRules();
739 }
740};
741
742/**
743 * Stops the sync object from syncing.
744 */
745SyncApi.prototype.stop = function () {
746 debuglog("SyncApi.stop");
747 if (global.document) {
748 global.document.removeEventListener("online", this._onOnlineBound, false);
749 this._onOnlineBound = undefined;
750 }
751 this._running = false;
752 if (this._currentSyncRequest) {
753 this._currentSyncRequest.abort();
754 }
755 if (this._keepAliveTimer) {
756 clearTimeout(this._keepAliveTimer);
757 this._keepAliveTimer = null;
758 }
759};
760
761/**
762 * Retry a backed off syncing request immediately. This should only be used when
763 * the user <b>explicitly</b> attempts to retry their lost connection.
764 * @return {boolean} True if this resulted in a request being retried.
765 */
766SyncApi.prototype.retryImmediately = function () {
767 if (!this._connectionReturnedDefer) {
768 return false;
769 }
770 this._startKeepAlives(0);
771 return true;
772};
773/**
774 * Process a single set of cached sync data.
775 * @param {Object} savedSync a saved sync that was persisted by a store. This
776 * should have been acquired via client.store.getSavedSync().
777 */
778SyncApi.prototype._syncFromCache = function () {
779 var _ref6 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee6(savedSync) {
780 var nextSyncToken, syncEventData, data;
781 return _regenerator2.default.wrap(function _callee6$(_context6) {
782 while (1) {
783 switch (_context6.prev = _context6.next) {
784 case 0:
785 debuglog("sync(): not doing HTTP hit, instead returning stored /sync data");
786
787 nextSyncToken = savedSync.nextBatch;
788
789 // Set sync token for future incremental syncing
790
791 this.client.store.setSyncToken(nextSyncToken);
792
793 // No previous sync, set old token to null
794 syncEventData = {
795 oldSyncToken: null,
796 nextSyncToken: nextSyncToken,
797 catchingUp: false
798 };
799 data = {
800 next_batch: nextSyncToken,
801 rooms: savedSync.roomsData,
802 groups: savedSync.groupsData,
803 account_data: {
804 events: savedSync.accountData
805 }
806 };
807 _context6.prev = 5;
808 _context6.next = 8;
809 return (0, _bluebird.resolve)(this._processSyncResponse(syncEventData, data));
810
811 case 8:
812 _context6.next = 13;
813 break;
814
815 case 10:
816 _context6.prev = 10;
817 _context6.t0 = _context6["catch"](5);
818
819 console.error("Error processing cached sync", _context6.t0.stack || _context6.t0);
820
821 case 13:
822
823 // Don't emit a prepared if we've bailed because the store is invalid:
824 // in this case the client will not be usable until stopped & restarted
825 // so this would be useless and misleading.
826 if (!this._storeIsInvalid) {
827 this._updateSyncState("PREPARED", syncEventData);
828 }
829
830 case 14:
831 case "end":
832 return _context6.stop();
833 }
834 }
835 }, _callee6, this, [[5, 10]]);
836 }));
837
838 return function (_x4) {
839 return _ref6.apply(this, arguments);
840 };
841}();
842
843/**
844 * Invoke me to do /sync calls
845 * @param {Object} syncOptions
846 * @param {string} syncOptions.filterId
847 * @param {boolean} syncOptions.hasSyncedBefore
848 */
849SyncApi.prototype._sync = function () {
850 var _ref7 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee7(syncOptions) {
851 var client, syncToken, data, syncEventData;
852 return _regenerator2.default.wrap(function _callee7$(_context7) {
853 while (1) {
854 switch (_context7.prev = _context7.next) {
855 case 0:
856 client = this.client;
857
858 if (this._running) {
859 _context7.next = 6;
860 break;
861 }
862
863 debuglog("Sync no longer running: exiting.");
864 if (this._connectionReturnedDefer) {
865 this._connectionReturnedDefer.reject();
866 this._connectionReturnedDefer = null;
867 }
868 this._updateSyncState("STOPPED");
869 return _context7.abrupt("return");
870
871 case 6:
872 syncToken = client.store.getSyncToken();
873 data = void 0;
874 _context7.prev = 8;
875
876 //debuglog('Starting sync since=' + syncToken);
877 if (this._currentSyncRequest === null) {
878 this._currentSyncRequest = this._doSyncRequest(syncOptions, syncToken);
879 }
880 _context7.next = 12;
881 return (0, _bluebird.resolve)(this._currentSyncRequest);
882
883 case 12:
884 data = _context7.sent;
885 _context7.next = 19;
886 break;
887
888 case 15:
889 _context7.prev = 15;
890 _context7.t0 = _context7["catch"](8);
891
892 this._onSyncError(_context7.t0, syncOptions);
893 return _context7.abrupt("return");
894
895 case 19:
896 _context7.prev = 19;
897
898 this._currentSyncRequest = null;
899 return _context7.finish(19);
900
901 case 22:
902
903 //debuglog('Completed sync, next_batch=' + data.next_batch);
904
905 // set the sync token NOW *before* processing the events. We do this so
906 // if something barfs on an event we can skip it rather than constantly
907 // polling with the same token.
908 client.store.setSyncToken(data.next_batch);
909
910 // Reset after a successful sync
911 this._failedSyncCount = 0;
912
913 _context7.next = 26;
914 return (0, _bluebird.resolve)(client.store.setSyncData(data));
915
916 case 26:
917 syncEventData = {
918 oldSyncToken: syncToken,
919 nextSyncToken: data.next_batch,
920 catchingUp: this._catchingUp
921 };
922
923 if (!this.opts.crypto) {
924 _context7.next = 30;
925 break;
926 }
927
928 _context7.next = 30;
929 return (0, _bluebird.resolve)(this.opts.crypto.onSyncWillProcess(syncEventData));
930
931 case 30:
932 _context7.prev = 30;
933 _context7.next = 33;
934 return (0, _bluebird.resolve)(this._processSyncResponse(syncEventData, data));
935
936 case 33:
937 _context7.next = 38;
938 break;
939
940 case 35:
941 _context7.prev = 35;
942 _context7.t1 = _context7["catch"](30);
943
944 // log the exception with stack if we have it, else fall back
945 // to the plain description
946 console.error("Caught /sync error", _context7.t1.stack || _context7.t1);
947
948 case 38:
949
950 // update this as it may have changed
951 syncEventData.catchingUp = this._catchingUp;
952
953 // emit synced events
954 if (!syncOptions.hasSyncedBefore) {
955 this._updateSyncState("PREPARED", syncEventData);
956 syncOptions.hasSyncedBefore = true;
957 }
958
959 // tell the crypto module to do its processing. It may block (to do a
960 // /keys/changes request).
961
962 if (!this.opts.crypto) {
963 _context7.next = 43;
964 break;
965 }
966
967 _context7.next = 43;
968 return (0, _bluebird.resolve)(this.opts.crypto.onSyncCompleted(syncEventData));
969
970 case 43:
971
972 // keep emitting SYNCING -> SYNCING for clients who want to do bulk updates
973 this._updateSyncState("SYNCING", syncEventData);
974
975 if (!client.store.wantsSave()) {
976 _context7.next = 49;
977 break;
978 }
979
980 if (!this.opts.crypto) {
981 _context7.next = 48;
982 break;
983 }
984
985 _context7.next = 48;
986 return (0, _bluebird.resolve)(this.opts.crypto.saveDeviceList(0));
987
988 case 48:
989
990 // tell databases that everything is now in a consistent state and can be saved.
991 client.store.save();
992
993 case 49:
994
995 // Begin next sync
996 this._sync(syncOptions);
997
998 case 50:
999 case "end":
1000 return _context7.stop();
1001 }
1002 }
1003 }, _callee7, this, [[8, 15, 19, 22], [30, 35]]);
1004 }));
1005
1006 return function (_x5) {
1007 return _ref7.apply(this, arguments);
1008 };
1009}();
1010
1011SyncApi.prototype._doSyncRequest = function (syncOptions, syncToken) {
1012 var qps = this._getSyncParams(syncOptions, syncToken);
1013 return this.client._http.authedRequest(undefined, "GET", "/sync", qps, undefined, qps.timeout + BUFFER_PERIOD_MS);
1014};
1015
1016SyncApi.prototype._getSyncParams = function (syncOptions, syncToken) {
1017 var pollTimeout = this.opts.pollTimeout;
1018
1019 if (this.getSyncState() !== 'SYNCING' || this._catchingUp) {
1020 // unless we are happily syncing already, we want the server to return
1021 // as quickly as possible, even if there are no events queued. This
1022 // serves two purposes:
1023 //
1024 // * When the connection dies, we want to know asap when it comes back,
1025 // so that we can hide the error from the user. (We don't want to
1026 // have to wait for an event or a timeout).
1027 //
1028 // * We want to know if the server has any to_device messages queued up
1029 // for us. We do that by calling it with a zero timeout until it
1030 // doesn't give us any more to_device messages.
1031 this._catchingUp = true;
1032 pollTimeout = 0;
1033 }
1034
1035 var filterId = syncOptions.filterId;
1036 if (this.client.isGuest() && !filterId) {
1037 filterId = this._getGuestFilter();
1038 }
1039
1040 var qps = {
1041 filter: filterId,
1042 timeout: pollTimeout
1043 };
1044
1045 if (this.opts.disablePresence) {
1046 qps.set_presence = "offline";
1047 }
1048
1049 if (syncToken) {
1050 qps.since = syncToken;
1051 } else {
1052 // use a cachebuster for initialsyncs, to make sure that
1053 // we don't get a stale sync
1054 // (https://github.com/vector-im/vector-web/issues/1354)
1055 qps._cacheBuster = Date.now();
1056 }
1057
1058 if (this.getSyncState() == 'ERROR' || this.getSyncState() == 'RECONNECTING') {
1059 // we think the connection is dead. If it comes back up, we won't know
1060 // about it till /sync returns. If the timeout= is high, this could
1061 // be a long time. Set it to 0 when doing retries so we don't have to wait
1062 // for an event or a timeout before emiting the SYNCING event.
1063 qps.timeout = 0;
1064 }
1065
1066 return qps;
1067};
1068
1069SyncApi.prototype._onSyncError = function (err, syncOptions) {
1070 var _this2 = this;
1071
1072 if (!this._running) {
1073 debuglog("Sync no longer running: exiting");
1074 if (this._connectionReturnedDefer) {
1075 this._connectionReturnedDefer.reject();
1076 this._connectionReturnedDefer = null;
1077 }
1078 this._updateSyncState("STOPPED");
1079 return;
1080 }
1081
1082 console.error("/sync error %s", err);
1083 console.error(err);
1084
1085 this._failedSyncCount++;
1086 console.log('Number of consecutive failed sync requests:', this._failedSyncCount);
1087
1088 debuglog("Starting keep-alive");
1089 // Note that we do *not* mark the sync connection as
1090 // lost yet: we only do this if a keepalive poke
1091 // fails, since long lived HTTP connections will
1092 // go away sometimes and we shouldn't treat this as
1093 // erroneous. We set the state to 'reconnecting'
1094 // instead, so that clients can observe this state
1095 // if they wish.
1096 this._startKeepAlives().then(function (connDidFail) {
1097 // Only emit CATCHUP if we detected a connectivity error: if we didn't,
1098 // it's quite likely the sync will fail again for the same reason and we
1099 // want to stay in ERROR rather than keep flip-flopping between ERROR
1100 // and CATCHUP.
1101 if (connDidFail && _this2.getSyncState() === 'ERROR') {
1102 _this2._updateSyncState("CATCHUP", {
1103 oldSyncToken: null,
1104 nextSyncToken: null,
1105 catchingUp: true
1106 });
1107 }
1108 _this2._sync(syncOptions);
1109 });
1110
1111 this._currentSyncRequest = null;
1112 // Transition from RECONNECTING to ERROR after a given number of failed syncs
1113 this._updateSyncState(this._failedSyncCount >= FAILED_SYNC_ERROR_THRESHOLD ? "ERROR" : "RECONNECTING", { error: err });
1114};
1115
1116/**
1117 * Process data returned from a sync response and propagate it
1118 * into the model objects
1119 *
1120 * @param {Object} syncEventData Object containing sync tokens associated with this sync
1121 * @param {Object} data The response from /sync
1122 */
1123SyncApi.prototype._processSyncResponse = function () {
1124 var _ref8 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee10(syncEventData, data) {
1125 var client, self, events, inviteRooms, joinRooms, leaveRooms, currentCount;
1126 return _regenerator2.default.wrap(function _callee10$(_context10) {
1127 while (1) {
1128 switch (_context10.prev = _context10.next) {
1129 case 0:
1130 client = this.client;
1131 self = this;
1132
1133 // data looks like:
1134 // {
1135 // next_batch: $token,
1136 // presence: { events: [] },
1137 // account_data: { events: [] },
1138 // device_lists: { changed: ["@user:server", ... ]},
1139 // to_device: { events: [] },
1140 // device_one_time_keys_count: { signed_curve25519: 42 },
1141 // rooms: {
1142 // invite: {
1143 // $roomid: {
1144 // invite_state: { events: [] }
1145 // }
1146 // },
1147 // join: {
1148 // $roomid: {
1149 // state: { events: [] },
1150 // timeline: { events: [], prev_batch: $token, limited: true },
1151 // ephemeral: { events: [] },
1152 // summary: {
1153 // m.heroes: [ $user_id ],
1154 // m.joined_member_count: $count,
1155 // m.invited_member_count: $count
1156 // },
1157 // account_data: { events: [] },
1158 // unread_notifications: {
1159 // highlight_count: 0,
1160 // notification_count: 0,
1161 // }
1162 // }
1163 // },
1164 // leave: {
1165 // $roomid: {
1166 // state: { events: [] },
1167 // timeline: { events: [], prev_batch: $token }
1168 // }
1169 // }
1170 // },
1171 // groups: {
1172 // invite: {
1173 // $groupId: {
1174 // inviter: $inviter,
1175 // profile: {
1176 // avatar_url: $avatarUrl,
1177 // name: $groupName,
1178 // },
1179 // },
1180 // },
1181 // join: {},
1182 // leave: {},
1183 // },
1184 // }
1185
1186 // TODO-arch:
1187 // - Each event we pass through needs to be emitted via 'event', can we
1188 // do this in one place?
1189 // - The isBrandNewRoom boilerplate is boilerplatey.
1190
1191 // handle presence events (User objects)
1192
1193 if (data.presence && utils.isArray(data.presence.events)) {
1194 data.presence.events.map(client.getEventMapper()).forEach(function (presenceEvent) {
1195 var user = client.store.getUser(presenceEvent.getSender());
1196 if (user) {
1197 user.setPresenceEvent(presenceEvent);
1198 } else {
1199 user = createNewUser(client, presenceEvent.getSender());
1200 user.setPresenceEvent(presenceEvent);
1201 client.store.storeUser(user);
1202 }
1203 client.emit("event", presenceEvent);
1204 });
1205 }
1206
1207 // handle non-room account_data
1208 if (data.account_data && utils.isArray(data.account_data.events)) {
1209 events = data.account_data.events.map(client.getEventMapper());
1210
1211 client.store.storeAccountDataEvents(events);
1212 events.forEach(function (accountDataEvent) {
1213 // Honour push rules that come down the sync stream but also
1214 // honour push rules that were previously cached. Base rules
1215 // will be updated when we recieve push rules via getPushRules
1216 // (see SyncApi.prototype.sync) before syncing over the network.
1217 if (accountDataEvent.getType() == 'm.push_rules') {
1218 client.pushRules = accountDataEvent.getContent();
1219 }
1220 client.emit("accountData", accountDataEvent);
1221 return accountDataEvent;
1222 });
1223 }
1224
1225 // handle to-device events
1226 if (data.to_device && utils.isArray(data.to_device.events) && data.to_device.events.length > 0) {
1227 data.to_device.events.map(client.getEventMapper()).forEach(function (toDeviceEvent) {
1228 var content = toDeviceEvent.getContent();
1229 if (toDeviceEvent.getType() == "m.room.message" && content.msgtype == "m.bad.encrypted") {
1230 // the mapper already logged a warning.
1231 console.log('Ignoring undecryptable to-device event from ' + toDeviceEvent.getSender());
1232 return;
1233 }
1234
1235 client.emit("toDeviceEvent", toDeviceEvent);
1236 });
1237 } else {
1238 // no more to-device events: we can stop polling with a short timeout.
1239 this._catchingUp = false;
1240 }
1241
1242 if (data.groups) {
1243 if (data.groups.invite) {
1244 this._processGroupSyncEntry(data.groups.invite, 'invite');
1245 }
1246
1247 if (data.groups.join) {
1248 this._processGroupSyncEntry(data.groups.join, 'join');
1249 }
1250
1251 if (data.groups.leave) {
1252 this._processGroupSyncEntry(data.groups.leave, 'leave');
1253 }
1254 }
1255
1256 // the returned json structure is a bit crap, so make it into a
1257 // nicer form (array) after applying sanity to make sure we don't fail
1258 // on missing keys (on the off chance)
1259 inviteRooms = [];
1260 joinRooms = [];
1261 leaveRooms = [];
1262
1263
1264 if (data.rooms) {
1265 if (data.rooms.invite) {
1266 inviteRooms = this._mapSyncResponseToRoomArray(data.rooms.invite);
1267 }
1268 if (data.rooms.join) {
1269 joinRooms = this._mapSyncResponseToRoomArray(data.rooms.join);
1270 }
1271 if (data.rooms.leave) {
1272 leaveRooms = this._mapSyncResponseToRoomArray(data.rooms.leave);
1273 }
1274 }
1275
1276 this._notifEvents = [];
1277
1278 // Handle invites
1279 inviteRooms.forEach(function (inviteObj) {
1280 var room = inviteObj.room;
1281 var stateEvents = self._mapSyncEventsFormat(inviteObj.invite_state, room);
1282
1283 room.updateMyMembership("invite");
1284 self._processRoomEvents(room, stateEvents);
1285 if (inviteObj.isBrandNewRoom) {
1286 room.recalculate();
1287 client.store.storeRoom(room);
1288 client.emit("Room", room);
1289 }
1290 stateEvents.forEach(function (e) {
1291 client.emit("event", e);
1292 });
1293 });
1294
1295 // Handle joins
1296 _context10.next = 14;
1297 return (0, _bluebird.resolve)(_bluebird2.default.mapSeries(joinRooms, function () {
1298 var _ref9 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee9(joinObj) {
1299 var processRoomEvent = function () {
1300 var _ref10 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee8(e) {
1301 return _regenerator2.default.wrap(function _callee8$(_context8) {
1302 while (1) {
1303 switch (_context8.prev = _context8.next) {
1304 case 0:
1305 client.emit("event", e);
1306
1307 if (!(e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto)) {
1308 _context8.next = 4;
1309 break;
1310 }
1311
1312 _context8.next = 4;
1313 return (0, _bluebird.resolve)(self.opts.crypto.onCryptoEvent(e));
1314
1315 case 4:
1316 case "end":
1317 return _context8.stop();
1318 }
1319 }
1320 }, _callee8, this);
1321 }));
1322
1323 return function processRoomEvent(_x9) {
1324 return _ref10.apply(this, arguments);
1325 };
1326 }();
1327
1328 var room, stateEvents, timelineEvents, ephemeralEvents, accountDataEvents, limited, i, eventId;
1329 return _regenerator2.default.wrap(function _callee9$(_context9) {
1330 while (1) {
1331 switch (_context9.prev = _context9.next) {
1332 case 0:
1333 room = joinObj.room;
1334 stateEvents = self._mapSyncEventsFormat(joinObj.state, room);
1335 timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room);
1336 ephemeralEvents = self._mapSyncEventsFormat(joinObj.ephemeral);
1337 accountDataEvents = self._mapSyncEventsFormat(joinObj.account_data);
1338
1339 // we do this first so it's correct when any of the events fire
1340
1341 if (joinObj.unread_notifications) {
1342 room.setUnreadNotificationCount('total', joinObj.unread_notifications.notification_count);
1343 room.setUnreadNotificationCount('highlight', joinObj.unread_notifications.highlight_count);
1344 }
1345
1346 room.updateMyMembership("join");
1347
1348 joinObj.timeline = joinObj.timeline || {};
1349
1350 if (!joinObj.isBrandNewRoom) {
1351 _context9.next = 12;
1352 break;
1353 }
1354
1355 // set the back-pagination token. Do this *before* adding any
1356 // events so that clients can start back-paginating.
1357 room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
1358 _context9.next = 26;
1359 break;
1360
1361 case 12:
1362 if (!joinObj.timeline.limited) {
1363 _context9.next = 26;
1364 break;
1365 }
1366
1367 limited = true;
1368
1369 // we've got a limited sync, so we *probably* have a gap in the
1370 // timeline, so should reset. But we might have been peeking or
1371 // paginating and already have some of the events, in which
1372 // case we just want to append any subsequent events to the end
1373 // of the existing timeline.
1374 //
1375 // This is particularly important in the case that we already have
1376 // *all* of the events in the timeline - in that case, if we reset
1377 // the timeline, we'll end up with an entirely empty timeline,
1378 // which we'll try to paginate but not get any new events (which
1379 // will stop us linking the empty timeline into the chain).
1380 //
1381
1382 i = timelineEvents.length - 1;
1383
1384 case 15:
1385 if (!(i >= 0)) {
1386 _context9.next = 25;
1387 break;
1388 }
1389
1390 eventId = timelineEvents[i].getId();
1391
1392 if (!room.getTimelineForEvent(eventId)) {
1393 _context9.next = 22;
1394 break;
1395 }
1396
1397 debuglog("Already have event " + eventId + " in limited " + "sync - not resetting");
1398 limited = false;
1399
1400 // we might still be missing some of the events before i;
1401 // we don't want to be adding them to the end of the
1402 // timeline because that would put them out of order.
1403 timelineEvents.splice(0, i);
1404
1405 // XXX: there's a problem here if the skipped part of the
1406 // timeline modifies the state set in stateEvents, because
1407 // we'll end up using the state from stateEvents rather
1408 // than the later state from timelineEvents. We probably
1409 // need to wind stateEvents forward over the events we're
1410 // skipping.
1411
1412 return _context9.abrupt("break", 25);
1413
1414 case 22:
1415 i--;
1416 _context9.next = 15;
1417 break;
1418
1419 case 25:
1420
1421 if (limited) {
1422 self._deregisterStateListeners(room);
1423 room.resetLiveTimeline(joinObj.timeline.prev_batch, self.opts.canResetEntireTimeline(room.roomId) ? null : syncEventData.oldSyncToken);
1424
1425 // We have to assume any gap in any timeline is
1426 // reason to stop incrementally tracking notifications and
1427 // reset the timeline.
1428 client.resetNotifTimelineSet();
1429
1430 self._registerStateListeners(room);
1431 }
1432
1433 case 26:
1434
1435 self._processRoomEvents(room, stateEvents, timelineEvents);
1436
1437 // set summary after processing events,
1438 // because it will trigger a name calculation
1439 // which needs the room state to be up to date
1440 if (joinObj.summary) {
1441 room.setSummary(joinObj.summary);
1442 }
1443
1444 // XXX: should we be adding ephemeralEvents to the timeline?
1445 // It feels like that for symmetry with room.addAccountData()
1446 // there should be a room.addEphemeralEvents() or similar.
1447 room.addLiveEvents(ephemeralEvents);
1448
1449 // we deliberately don't add accountData to the timeline
1450 room.addAccountData(accountDataEvents);
1451
1452 room.recalculate();
1453 if (joinObj.isBrandNewRoom) {
1454 client.store.storeRoom(room);
1455 client.emit("Room", room);
1456 }
1457
1458 self._processEventsForNotifs(room, timelineEvents);
1459
1460 _context9.next = 35;
1461 return (0, _bluebird.resolve)(_bluebird2.default.mapSeries(stateEvents, processRoomEvent));
1462
1463 case 35:
1464 _context9.next = 37;
1465 return (0, _bluebird.resolve)(_bluebird2.default.mapSeries(timelineEvents, processRoomEvent));
1466
1467 case 37:
1468 ephemeralEvents.forEach(function (e) {
1469 client.emit("event", e);
1470 });
1471 accountDataEvents.forEach(function (e) {
1472 client.emit("event", e);
1473 });
1474
1475 case 39:
1476 case "end":
1477 return _context9.stop();
1478 }
1479 }
1480 }, _callee9, this);
1481 }));
1482
1483 return function (_x8) {
1484 return _ref9.apply(this, arguments);
1485 };
1486 }()));
1487
1488 case 14:
1489
1490 // Handle leaves (e.g. kicked rooms)
1491 leaveRooms.forEach(function (leaveObj) {
1492 var room = leaveObj.room;
1493 var stateEvents = self._mapSyncEventsFormat(leaveObj.state, room);
1494 var timelineEvents = self._mapSyncEventsFormat(leaveObj.timeline, room);
1495 var accountDataEvents = self._mapSyncEventsFormat(leaveObj.account_data);
1496
1497 room.updateMyMembership("leave");
1498
1499 self._processRoomEvents(room, stateEvents, timelineEvents);
1500 room.addAccountData(accountDataEvents);
1501
1502 room.recalculate();
1503 if (leaveObj.isBrandNewRoom) {
1504 client.store.storeRoom(room);
1505 client.emit("Room", room);
1506 }
1507
1508 self._processEventsForNotifs(room, timelineEvents);
1509
1510 stateEvents.forEach(function (e) {
1511 client.emit("event", e);
1512 });
1513 timelineEvents.forEach(function (e) {
1514 client.emit("event", e);
1515 });
1516 accountDataEvents.forEach(function (e) {
1517 client.emit("event", e);
1518 });
1519 });
1520
1521 // update the notification timeline, if appropriate.
1522 // we only do this for live events, as otherwise we can't order them sanely
1523 // in the timeline relative to ones paginated in by /notifications.
1524 // XXX: we could fix this by making EventTimeline support chronological
1525 // ordering... but it doesn't, right now.
1526 if (syncEventData.oldSyncToken && this._notifEvents.length) {
1527 this._notifEvents.sort(function (a, b) {
1528 return a.getTs() - b.getTs();
1529 });
1530 this._notifEvents.forEach(function (event) {
1531 client.getNotifTimelineSet().addLiveEvent(event);
1532 });
1533 }
1534
1535 // Handle device list updates
1536
1537 if (!data.device_lists) {
1538 _context10.next = 22;
1539 break;
1540 }
1541
1542 if (!this.opts.crypto) {
1543 _context10.next = 22;
1544 break;
1545 }
1546
1547 _context10.next = 20;
1548 return (0, _bluebird.resolve)(this.opts.crypto.handleDeviceListChanges(syncEventData, data.device_lists));
1549
1550 case 20:
1551 _context10.next = 22;
1552 break;
1553
1554 case 22:
1555
1556 // Handle one_time_keys_count
1557 if (this.opts.crypto && data.device_one_time_keys_count) {
1558 currentCount = data.device_one_time_keys_count.signed_curve25519 || 0;
1559
1560 this.opts.crypto.updateOneTimeKeyCount(currentCount);
1561 }
1562
1563 case 23:
1564 case "end":
1565 return _context10.stop();
1566 }
1567 }
1568 }, _callee10, this);
1569 }));
1570
1571 return function (_x6, _x7) {
1572 return _ref8.apply(this, arguments);
1573 };
1574}();
1575
1576/**
1577 * Starts polling the connectivity check endpoint
1578 * @param {number} delay How long to delay until the first poll.
1579 * defaults to a short, randomised interval (to prevent
1580 * tightlooping if /versions succeeds but /sync etc. fail).
1581 * @return {promise} which resolves once the connection returns
1582 */
1583SyncApi.prototype._startKeepAlives = function (delay) {
1584 if (delay === undefined) {
1585 delay = 2000 + Math.floor(Math.random() * 5000);
1586 }
1587
1588 if (this._keepAliveTimer !== null) {
1589 clearTimeout(this._keepAliveTimer);
1590 }
1591 var self = this;
1592 if (delay > 0) {
1593 self._keepAliveTimer = setTimeout(self._pokeKeepAlive.bind(self), delay);
1594 } else {
1595 self._pokeKeepAlive();
1596 }
1597 if (!this._connectionReturnedDefer) {
1598 this._connectionReturnedDefer = _bluebird2.default.defer();
1599 }
1600 return this._connectionReturnedDefer.promise;
1601};
1602
1603/**
1604 * Make a dummy call to /_matrix/client/versions, to see if the HS is
1605 * reachable.
1606 *
1607 * On failure, schedules a call back to itself. On success, resolves
1608 * this._connectionReturnedDefer.
1609 *
1610 * @param {bool} connDidFail True if a connectivity failure has been detected. Optional.
1611 */
1612SyncApi.prototype._pokeKeepAlive = function (connDidFail) {
1613 if (connDidFail === undefined) connDidFail = false;
1614 var self = this;
1615 function success() {
1616 clearTimeout(self._keepAliveTimer);
1617 if (self._connectionReturnedDefer) {
1618 self._connectionReturnedDefer.resolve(connDidFail);
1619 self._connectionReturnedDefer = null;
1620 }
1621 }
1622
1623 this.client._http.request(undefined, // callback
1624 "GET", "/_matrix/client/versions", undefined, // queryParams
1625 undefined, // data
1626 {
1627 prefix: '',
1628 localTimeoutMs: 15 * 1000
1629 }).done(function () {
1630 success();
1631 }, function (err) {
1632 if (err.httpStatus == 400 || err.httpStatus == 404) {
1633 // treat this as a success because the server probably just doesn't
1634 // support /versions: point is, we're getting a response.
1635 // We wait a short time though, just in case somehow the server
1636 // is in a mode where it 400s /versions responses and sync etc.
1637 // responses fail, this will mean we don't hammer in a loop.
1638 self._keepAliveTimer = setTimeout(success, 2000);
1639 } else {
1640 connDidFail = true;
1641 self._keepAliveTimer = setTimeout(self._pokeKeepAlive.bind(self, connDidFail), 5000 + Math.floor(Math.random() * 5000));
1642 // A keepalive has failed, so we emit the
1643 // error state (whether or not this is the
1644 // first failure).
1645 // Note we do this after setting the timer:
1646 // this lets the unit tests advance the mock
1647 // clock when they get the error.
1648 self._updateSyncState("ERROR", { error: err });
1649 }
1650 });
1651};
1652
1653/**
1654 * @param {Object} groupsSection Groups section object, eg. response.groups.invite
1655 * @param {string} sectionName Which section this is ('invite', 'join' or 'leave')
1656 */
1657SyncApi.prototype._processGroupSyncEntry = function (groupsSection, sectionName) {
1658 // Processes entries from 'groups' section of the sync stream
1659 var _iteratorNormalCompletion = true;
1660 var _didIteratorError = false;
1661 var _iteratorError = undefined;
1662
1663 try {
1664 for (var _iterator = (0, _getIterator3.default)((0, _keys2.default)(groupsSection)), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
1665 var groupId = _step.value;
1666
1667 var groupInfo = groupsSection[groupId];
1668 var group = this.client.store.getGroup(groupId);
1669 var isBrandNew = group === null;
1670 if (group === null) {
1671 group = this.createGroup(groupId);
1672 }
1673 if (groupInfo.profile) {
1674 group.setProfile(groupInfo.profile.name, groupInfo.profile.avatar_url);
1675 }
1676 if (groupInfo.inviter) {
1677 group.setInviter({ userId: groupInfo.inviter });
1678 }
1679 group.setMyMembership(sectionName);
1680 if (isBrandNew) {
1681 // Now we've filled in all the fields, emit the Group event
1682 this.client.emit("Group", group);
1683 }
1684 }
1685 } catch (err) {
1686 _didIteratorError = true;
1687 _iteratorError = err;
1688 } finally {
1689 try {
1690 if (!_iteratorNormalCompletion && _iterator.return) {
1691 _iterator.return();
1692 }
1693 } finally {
1694 if (_didIteratorError) {
1695 throw _iteratorError;
1696 }
1697 }
1698 }
1699};
1700
1701/**
1702 * @param {Object} obj
1703 * @return {Object[]}
1704 */
1705SyncApi.prototype._mapSyncResponseToRoomArray = function (obj) {
1706 // Maps { roomid: {stuff}, roomid: {stuff} }
1707 // to
1708 // [{stuff+Room+isBrandNewRoom}, {stuff+Room+isBrandNewRoom}]
1709 var client = this.client;
1710 var self = this;
1711 return utils.keys(obj).map(function (roomId) {
1712 var arrObj = obj[roomId];
1713 var room = client.store.getRoom(roomId);
1714 var isBrandNewRoom = false;
1715 if (!room) {
1716 room = self.createRoom(roomId);
1717 isBrandNewRoom = true;
1718 }
1719 arrObj.room = room;
1720 arrObj.isBrandNewRoom = isBrandNewRoom;
1721 return arrObj;
1722 });
1723};
1724
1725/**
1726 * @param {Object} obj
1727 * @param {Room} room
1728 * @return {MatrixEvent[]}
1729 */
1730SyncApi.prototype._mapSyncEventsFormat = function (obj, room) {
1731 if (!obj || !utils.isArray(obj.events)) {
1732 return [];
1733 }
1734 var mapper = this.client.getEventMapper();
1735 return obj.events.map(function (e) {
1736 if (room) {
1737 e.room_id = room.roomId;
1738 }
1739 return mapper(e);
1740 });
1741};
1742
1743/**
1744 * @param {Room} room
1745 */
1746SyncApi.prototype._resolveInvites = function (room) {
1747 if (!room || !this.opts.resolveInvitesToProfiles) {
1748 return;
1749 }
1750 var client = this.client;
1751 // For each invited room member we want to give them a displayname/avatar url
1752 // if they have one (the m.room.member invites don't contain this).
1753 room.getMembersWithMembership("invite").forEach(function (member) {
1754 if (member._requestedProfileInfo) {
1755 return;
1756 }
1757 member._requestedProfileInfo = true;
1758 // try to get a cached copy first.
1759 var user = client.getUser(member.userId);
1760 var promise = void 0;
1761 if (user) {
1762 promise = _bluebird2.default.resolve({
1763 avatar_url: user.avatarUrl,
1764 displayname: user.displayName
1765 });
1766 } else {
1767 promise = client.getProfileInfo(member.userId);
1768 }
1769 promise.done(function (info) {
1770 // slightly naughty by doctoring the invite event but this means all
1771 // the code paths remain the same between invite/join display name stuff
1772 // which is a worthy trade-off for some minor pollution.
1773 var inviteEvent = member.events.member;
1774 if (inviteEvent.getContent().membership !== "invite") {
1775 // between resolving and now they have since joined, so don't clobber
1776 return;
1777 }
1778 inviteEvent.getContent().avatar_url = info.avatar_url;
1779 inviteEvent.getContent().displayname = info.displayname;
1780 // fire listeners
1781 member.setMembershipEvent(inviteEvent, room.currentState);
1782 }, function (err) {
1783 // OH WELL.
1784 });
1785 });
1786};
1787
1788/**
1789 * @param {Room} room
1790 * @param {MatrixEvent[]} stateEventList A list of state events. This is the state
1791 * at the *START* of the timeline list if it is supplied.
1792 * @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index
1793 * is earlier in time. Higher index is later.
1794 */
1795SyncApi.prototype._processRoomEvents = function (room, stateEventList, timelineEventList) {
1796 // If there are no events in the timeline yet, initialise it with
1797 // the given state events
1798 var liveTimeline = room.getLiveTimeline();
1799 var timelineWasEmpty = liveTimeline.getEvents().length == 0;
1800 if (timelineWasEmpty) {
1801 // Passing these events into initialiseState will freeze them, so we need
1802 // to compute and cache the push actions for them now, otherwise sync dies
1803 // with an attempt to assign to read only property.
1804 // XXX: This is pretty horrible and is assuming all sorts of behaviour from
1805 // these functions that it shouldn't be. We should probably either store the
1806 // push actions cache elsewhere so we can freeze MatrixEvents, or otherwise
1807 // find some solution where MatrixEvents are immutable but allow for a cache
1808 // field.
1809 var _iteratorNormalCompletion2 = true;
1810 var _didIteratorError2 = false;
1811 var _iteratorError2 = undefined;
1812
1813 try {
1814 for (var _iterator2 = (0, _getIterator3.default)(stateEventList), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
1815 var ev = _step2.value;
1816
1817 this.client.getPushActionsForEvent(ev);
1818 }
1819 } catch (err) {
1820 _didIteratorError2 = true;
1821 _iteratorError2 = err;
1822 } finally {
1823 try {
1824 if (!_iteratorNormalCompletion2 && _iterator2.return) {
1825 _iterator2.return();
1826 }
1827 } finally {
1828 if (_didIteratorError2) {
1829 throw _iteratorError2;
1830 }
1831 }
1832 }
1833
1834 liveTimeline.initialiseState(stateEventList);
1835 }
1836
1837 this._resolveInvites(room);
1838
1839 // recalculate the room name at this point as adding events to the timeline
1840 // may make notifications appear which should have the right name.
1841 // XXX: This looks suspect: we'll end up recalculating the room once here
1842 // and then again after adding events (_processSyncResponse calls it after
1843 // calling us) even if no state events were added. It also means that if
1844 // one of the room events in timelineEventList is something that needs
1845 // a recalculation (like m.room.name) we won't recalculate until we've
1846 // finished adding all the events, which will cause the notification to have
1847 // the old room name rather than the new one.
1848 room.recalculate();
1849
1850 // If the timeline wasn't empty, we process the state events here: they're
1851 // defined as updates to the state before the start of the timeline, so this
1852 // starts to roll the state forward.
1853 // XXX: That's what we *should* do, but this can happen if we were previously
1854 // peeking in a room, in which case we obviously do *not* want to add the
1855 // state events here onto the end of the timeline. Historically, the js-sdk
1856 // has just set these new state events on the old and new state. This seems
1857 // very wrong because there could be events in the timeline that diverge the
1858 // state, in which case this is going to leave things out of sync. However,
1859 // for now I think it;s best to behave the same as the code has done previously.
1860 if (!timelineWasEmpty) {
1861 // XXX: As above, don't do this...
1862 //room.addLiveEvents(stateEventList || []);
1863 // Do this instead...
1864 room.oldState.setStateEvents(stateEventList || []);
1865 room.currentState.setStateEvents(stateEventList || []);
1866 }
1867 // execute the timeline events. This will continue to diverge the current state
1868 // if the timeline has any state events in it.
1869 // This also needs to be done before running push rules on the events as they need
1870 // to be decorated with sender etc.
1871 room.addLiveEvents(timelineEventList || []);
1872};
1873
1874/**
1875 * Takes a list of timelineEvents and adds and adds to _notifEvents
1876 * as appropriate.
1877 * This must be called after the room the events belong to has been stored.
1878 *
1879 * @param {Room} room
1880 * @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index
1881 * is earlier in time. Higher index is later.
1882 */
1883SyncApi.prototype._processEventsForNotifs = function (room, timelineEventList) {
1884 // gather our notifications into this._notifEvents
1885 if (this.client.getNotifTimelineSet()) {
1886 for (var i = 0; i < timelineEventList.length; i++) {
1887 var pushActions = this.client.getPushActionsForEvent(timelineEventList[i]);
1888 if (pushActions && pushActions.notify && pushActions.tweaks && pushActions.tweaks.highlight) {
1889 this._notifEvents.push(timelineEventList[i]);
1890 }
1891 }
1892 }
1893};
1894
1895/**
1896 * @return {string}
1897 */
1898SyncApi.prototype._getGuestFilter = function () {
1899 var guestRooms = this.client._guestRooms; // FIXME: horrible gut-wrenching
1900 if (!guestRooms) {
1901 return "{}";
1902 }
1903 // we just need to specify the filter inline if we're a guest because guests
1904 // can't create filters.
1905 return (0, _stringify2.default)({
1906 room: {
1907 timeline: {
1908 limit: 20
1909 }
1910 }
1911 });
1912};
1913
1914/**
1915 * Sets the sync state and emits an event to say so
1916 * @param {String} newState The new state string
1917 * @param {Object} data Object of additional data to emit in the event
1918 */
1919SyncApi.prototype._updateSyncState = function (newState, data) {
1920 var old = this._syncState;
1921 this._syncState = newState;
1922 this._syncStateData = data;
1923 this.client.emit("sync", this._syncState, old, data);
1924};
1925
1926/**
1927 * Event handler for the 'online' event
1928 * This event is generally unreliable and precise behaviour
1929 * varies between browsers, so we poll for connectivity too,
1930 * but this might help us reconnect a little faster.
1931 */
1932SyncApi.prototype._onOnline = function () {
1933 debuglog("Browser thinks we are back online");
1934 this._startKeepAlives(0);
1935};
1936
1937function createNewUser(client, userId) {
1938 var user = new User(userId);
1939 client.reEmitter.reEmit(user, ["User.avatarUrl", "User.displayName", "User.presence", "User.currentlyActive", "User.lastPresenceTs"]);
1940 return user;
1941}
1942
1943/** */
1944module.exports = SyncApi;
1945//# sourceMappingURL=sync.js.map
\No newline at end of file