UNPKG

84.3 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 var user;
1302 return _regenerator2.default.wrap(function _callee8$(_context8) {
1303 while (1) {
1304 switch (_context8.prev = _context8.next) {
1305 case 0:
1306 client.emit("event", e);
1307
1308 if (!(e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto)) {
1309 _context8.next = 4;
1310 break;
1311 }
1312
1313 _context8.next = 4;
1314 return (0, _bluebird.resolve)(self.opts.crypto.onCryptoEvent(e));
1315
1316 case 4:
1317 if (e.isState() && e.getType() === "im.vector.user_status") {
1318 user = client.store.getUser(e.getStateKey());
1319
1320 if (user) {
1321 user._unstable_updateStatusMessage(e);
1322 } else {
1323 user = createNewUser(client, e.getStateKey());
1324 user._unstable_updateStatusMessage(e);
1325 client.store.storeUser(user);
1326 }
1327 }
1328
1329 case 5:
1330 case "end":
1331 return _context8.stop();
1332 }
1333 }
1334 }, _callee8, this);
1335 }));
1336
1337 return function processRoomEvent(_x9) {
1338 return _ref10.apply(this, arguments);
1339 };
1340 }();
1341
1342 var room, stateEvents, timelineEvents, ephemeralEvents, accountDataEvents, limited, i, eventId;
1343 return _regenerator2.default.wrap(function _callee9$(_context9) {
1344 while (1) {
1345 switch (_context9.prev = _context9.next) {
1346 case 0:
1347 room = joinObj.room;
1348 stateEvents = self._mapSyncEventsFormat(joinObj.state, room);
1349 timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room);
1350 ephemeralEvents = self._mapSyncEventsFormat(joinObj.ephemeral);
1351 accountDataEvents = self._mapSyncEventsFormat(joinObj.account_data);
1352
1353 // we do this first so it's correct when any of the events fire
1354
1355 if (joinObj.unread_notifications) {
1356 room.setUnreadNotificationCount('total', joinObj.unread_notifications.notification_count);
1357 room.setUnreadNotificationCount('highlight', joinObj.unread_notifications.highlight_count);
1358 }
1359
1360 room.updateMyMembership("join");
1361
1362 joinObj.timeline = joinObj.timeline || {};
1363
1364 if (!joinObj.isBrandNewRoom) {
1365 _context9.next = 12;
1366 break;
1367 }
1368
1369 // set the back-pagination token. Do this *before* adding any
1370 // events so that clients can start back-paginating.
1371 room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
1372 _context9.next = 26;
1373 break;
1374
1375 case 12:
1376 if (!joinObj.timeline.limited) {
1377 _context9.next = 26;
1378 break;
1379 }
1380
1381 limited = true;
1382
1383 // we've got a limited sync, so we *probably* have a gap in the
1384 // timeline, so should reset. But we might have been peeking or
1385 // paginating and already have some of the events, in which
1386 // case we just want to append any subsequent events to the end
1387 // of the existing timeline.
1388 //
1389 // This is particularly important in the case that we already have
1390 // *all* of the events in the timeline - in that case, if we reset
1391 // the timeline, we'll end up with an entirely empty timeline,
1392 // which we'll try to paginate but not get any new events (which
1393 // will stop us linking the empty timeline into the chain).
1394 //
1395
1396 i = timelineEvents.length - 1;
1397
1398 case 15:
1399 if (!(i >= 0)) {
1400 _context9.next = 25;
1401 break;
1402 }
1403
1404 eventId = timelineEvents[i].getId();
1405
1406 if (!room.getTimelineForEvent(eventId)) {
1407 _context9.next = 22;
1408 break;
1409 }
1410
1411 debuglog("Already have event " + eventId + " in limited " + "sync - not resetting");
1412 limited = false;
1413
1414 // we might still be missing some of the events before i;
1415 // we don't want to be adding them to the end of the
1416 // timeline because that would put them out of order.
1417 timelineEvents.splice(0, i);
1418
1419 // XXX: there's a problem here if the skipped part of the
1420 // timeline modifies the state set in stateEvents, because
1421 // we'll end up using the state from stateEvents rather
1422 // than the later state from timelineEvents. We probably
1423 // need to wind stateEvents forward over the events we're
1424 // skipping.
1425
1426 return _context9.abrupt("break", 25);
1427
1428 case 22:
1429 i--;
1430 _context9.next = 15;
1431 break;
1432
1433 case 25:
1434
1435 if (limited) {
1436 self._deregisterStateListeners(room);
1437 room.resetLiveTimeline(joinObj.timeline.prev_batch, self.opts.canResetEntireTimeline(room.roomId) ? null : syncEventData.oldSyncToken);
1438
1439 // We have to assume any gap in any timeline is
1440 // reason to stop incrementally tracking notifications and
1441 // reset the timeline.
1442 client.resetNotifTimelineSet();
1443
1444 self._registerStateListeners(room);
1445 }
1446
1447 case 26:
1448
1449 self._processRoomEvents(room, stateEvents, timelineEvents);
1450
1451 // set summary after processing events,
1452 // because it will trigger a name calculation
1453 // which needs the room state to be up to date
1454 if (joinObj.summary) {
1455 room.setSummary(joinObj.summary);
1456 }
1457
1458 // XXX: should we be adding ephemeralEvents to the timeline?
1459 // It feels like that for symmetry with room.addAccountData()
1460 // there should be a room.addEphemeralEvents() or similar.
1461 room.addLiveEvents(ephemeralEvents);
1462
1463 // we deliberately don't add accountData to the timeline
1464 room.addAccountData(accountDataEvents);
1465
1466 room.recalculate();
1467 if (joinObj.isBrandNewRoom) {
1468 client.store.storeRoom(room);
1469 client.emit("Room", room);
1470 }
1471
1472 self._processEventsForNotifs(room, timelineEvents);
1473
1474 _context9.next = 35;
1475 return (0, _bluebird.resolve)(_bluebird2.default.mapSeries(stateEvents, processRoomEvent));
1476
1477 case 35:
1478 _context9.next = 37;
1479 return (0, _bluebird.resolve)(_bluebird2.default.mapSeries(timelineEvents, processRoomEvent));
1480
1481 case 37:
1482 ephemeralEvents.forEach(function (e) {
1483 client.emit("event", e);
1484 });
1485 accountDataEvents.forEach(function (e) {
1486 client.emit("event", e);
1487 });
1488
1489 case 39:
1490 case "end":
1491 return _context9.stop();
1492 }
1493 }
1494 }, _callee9, this);
1495 }));
1496
1497 return function (_x8) {
1498 return _ref9.apply(this, arguments);
1499 };
1500 }()));
1501
1502 case 14:
1503
1504 // Handle leaves (e.g. kicked rooms)
1505 leaveRooms.forEach(function (leaveObj) {
1506 var room = leaveObj.room;
1507 var stateEvents = self._mapSyncEventsFormat(leaveObj.state, room);
1508 var timelineEvents = self._mapSyncEventsFormat(leaveObj.timeline, room);
1509 var accountDataEvents = self._mapSyncEventsFormat(leaveObj.account_data);
1510
1511 room.updateMyMembership("leave");
1512
1513 self._processRoomEvents(room, stateEvents, timelineEvents);
1514 room.addAccountData(accountDataEvents);
1515
1516 room.recalculate();
1517 if (leaveObj.isBrandNewRoom) {
1518 client.store.storeRoom(room);
1519 client.emit("Room", room);
1520 }
1521
1522 self._processEventsForNotifs(room, timelineEvents);
1523
1524 stateEvents.forEach(function (e) {
1525 client.emit("event", e);
1526 });
1527 timelineEvents.forEach(function (e) {
1528 client.emit("event", e);
1529 });
1530 accountDataEvents.forEach(function (e) {
1531 client.emit("event", e);
1532 });
1533 });
1534
1535 // update the notification timeline, if appropriate.
1536 // we only do this for live events, as otherwise we can't order them sanely
1537 // in the timeline relative to ones paginated in by /notifications.
1538 // XXX: we could fix this by making EventTimeline support chronological
1539 // ordering... but it doesn't, right now.
1540 if (syncEventData.oldSyncToken && this._notifEvents.length) {
1541 this._notifEvents.sort(function (a, b) {
1542 return a.getTs() - b.getTs();
1543 });
1544 this._notifEvents.forEach(function (event) {
1545 client.getNotifTimelineSet().addLiveEvent(event);
1546 });
1547 }
1548
1549 // Handle device list updates
1550
1551 if (!data.device_lists) {
1552 _context10.next = 22;
1553 break;
1554 }
1555
1556 if (!this.opts.crypto) {
1557 _context10.next = 22;
1558 break;
1559 }
1560
1561 _context10.next = 20;
1562 return (0, _bluebird.resolve)(this.opts.crypto.handleDeviceListChanges(syncEventData, data.device_lists));
1563
1564 case 20:
1565 _context10.next = 22;
1566 break;
1567
1568 case 22:
1569
1570 // Handle one_time_keys_count
1571 if (this.opts.crypto && data.device_one_time_keys_count) {
1572 currentCount = data.device_one_time_keys_count.signed_curve25519 || 0;
1573
1574 this.opts.crypto.updateOneTimeKeyCount(currentCount);
1575 }
1576
1577 case 23:
1578 case "end":
1579 return _context10.stop();
1580 }
1581 }
1582 }, _callee10, this);
1583 }));
1584
1585 return function (_x6, _x7) {
1586 return _ref8.apply(this, arguments);
1587 };
1588}();
1589
1590/**
1591 * Starts polling the connectivity check endpoint
1592 * @param {number} delay How long to delay until the first poll.
1593 * defaults to a short, randomised interval (to prevent
1594 * tightlooping if /versions succeeds but /sync etc. fail).
1595 * @return {promise} which resolves once the connection returns
1596 */
1597SyncApi.prototype._startKeepAlives = function (delay) {
1598 if (delay === undefined) {
1599 delay = 2000 + Math.floor(Math.random() * 5000);
1600 }
1601
1602 if (this._keepAliveTimer !== null) {
1603 clearTimeout(this._keepAliveTimer);
1604 }
1605 var self = this;
1606 if (delay > 0) {
1607 self._keepAliveTimer = setTimeout(self._pokeKeepAlive.bind(self), delay);
1608 } else {
1609 self._pokeKeepAlive();
1610 }
1611 if (!this._connectionReturnedDefer) {
1612 this._connectionReturnedDefer = _bluebird2.default.defer();
1613 }
1614 return this._connectionReturnedDefer.promise;
1615};
1616
1617/**
1618 * Make a dummy call to /_matrix/client/versions, to see if the HS is
1619 * reachable.
1620 *
1621 * On failure, schedules a call back to itself. On success, resolves
1622 * this._connectionReturnedDefer.
1623 *
1624 * @param {bool} connDidFail True if a connectivity failure has been detected. Optional.
1625 */
1626SyncApi.prototype._pokeKeepAlive = function (connDidFail) {
1627 if (connDidFail === undefined) connDidFail = false;
1628 var self = this;
1629 function success() {
1630 clearTimeout(self._keepAliveTimer);
1631 if (self._connectionReturnedDefer) {
1632 self._connectionReturnedDefer.resolve(connDidFail);
1633 self._connectionReturnedDefer = null;
1634 }
1635 }
1636
1637 this.client._http.request(undefined, // callback
1638 "GET", "/_matrix/client/versions", undefined, // queryParams
1639 undefined, // data
1640 {
1641 prefix: '',
1642 localTimeoutMs: 15 * 1000
1643 }).done(function () {
1644 success();
1645 }, function (err) {
1646 if (err.httpStatus == 400 || err.httpStatus == 404) {
1647 // treat this as a success because the server probably just doesn't
1648 // support /versions: point is, we're getting a response.
1649 // We wait a short time though, just in case somehow the server
1650 // is in a mode where it 400s /versions responses and sync etc.
1651 // responses fail, this will mean we don't hammer in a loop.
1652 self._keepAliveTimer = setTimeout(success, 2000);
1653 } else {
1654 connDidFail = true;
1655 self._keepAliveTimer = setTimeout(self._pokeKeepAlive.bind(self, connDidFail), 5000 + Math.floor(Math.random() * 5000));
1656 // A keepalive has failed, so we emit the
1657 // error state (whether or not this is the
1658 // first failure).
1659 // Note we do this after setting the timer:
1660 // this lets the unit tests advance the mock
1661 // clock when they get the error.
1662 self._updateSyncState("ERROR", { error: err });
1663 }
1664 });
1665};
1666
1667/**
1668 * @param {Object} groupsSection Groups section object, eg. response.groups.invite
1669 * @param {string} sectionName Which section this is ('invite', 'join' or 'leave')
1670 */
1671SyncApi.prototype._processGroupSyncEntry = function (groupsSection, sectionName) {
1672 // Processes entries from 'groups' section of the sync stream
1673 var _iteratorNormalCompletion = true;
1674 var _didIteratorError = false;
1675 var _iteratorError = undefined;
1676
1677 try {
1678 for (var _iterator = (0, _getIterator3.default)((0, _keys2.default)(groupsSection)), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
1679 var groupId = _step.value;
1680
1681 var groupInfo = groupsSection[groupId];
1682 var group = this.client.store.getGroup(groupId);
1683 var isBrandNew = group === null;
1684 if (group === null) {
1685 group = this.createGroup(groupId);
1686 }
1687 if (groupInfo.profile) {
1688 group.setProfile(groupInfo.profile.name, groupInfo.profile.avatar_url);
1689 }
1690 if (groupInfo.inviter) {
1691 group.setInviter({ userId: groupInfo.inviter });
1692 }
1693 group.setMyMembership(sectionName);
1694 if (isBrandNew) {
1695 // Now we've filled in all the fields, emit the Group event
1696 this.client.emit("Group", group);
1697 }
1698 }
1699 } catch (err) {
1700 _didIteratorError = true;
1701 _iteratorError = err;
1702 } finally {
1703 try {
1704 if (!_iteratorNormalCompletion && _iterator.return) {
1705 _iterator.return();
1706 }
1707 } finally {
1708 if (_didIteratorError) {
1709 throw _iteratorError;
1710 }
1711 }
1712 }
1713};
1714
1715/**
1716 * @param {Object} obj
1717 * @return {Object[]}
1718 */
1719SyncApi.prototype._mapSyncResponseToRoomArray = function (obj) {
1720 // Maps { roomid: {stuff}, roomid: {stuff} }
1721 // to
1722 // [{stuff+Room+isBrandNewRoom}, {stuff+Room+isBrandNewRoom}]
1723 var client = this.client;
1724 var self = this;
1725 return utils.keys(obj).map(function (roomId) {
1726 var arrObj = obj[roomId];
1727 var room = client.store.getRoom(roomId);
1728 var isBrandNewRoom = false;
1729 if (!room) {
1730 room = self.createRoom(roomId);
1731 isBrandNewRoom = true;
1732 }
1733 arrObj.room = room;
1734 arrObj.isBrandNewRoom = isBrandNewRoom;
1735 return arrObj;
1736 });
1737};
1738
1739/**
1740 * @param {Object} obj
1741 * @param {Room} room
1742 * @return {MatrixEvent[]}
1743 */
1744SyncApi.prototype._mapSyncEventsFormat = function (obj, room) {
1745 if (!obj || !utils.isArray(obj.events)) {
1746 return [];
1747 }
1748 var mapper = this.client.getEventMapper();
1749 return obj.events.map(function (e) {
1750 if (room) {
1751 e.room_id = room.roomId;
1752 }
1753 return mapper(e);
1754 });
1755};
1756
1757/**
1758 * @param {Room} room
1759 */
1760SyncApi.prototype._resolveInvites = function (room) {
1761 if (!room || !this.opts.resolveInvitesToProfiles) {
1762 return;
1763 }
1764 var client = this.client;
1765 // For each invited room member we want to give them a displayname/avatar url
1766 // if they have one (the m.room.member invites don't contain this).
1767 room.getMembersWithMembership("invite").forEach(function (member) {
1768 if (member._requestedProfileInfo) {
1769 return;
1770 }
1771 member._requestedProfileInfo = true;
1772 // try to get a cached copy first.
1773 var user = client.getUser(member.userId);
1774 var promise = void 0;
1775 if (user) {
1776 promise = _bluebird2.default.resolve({
1777 avatar_url: user.avatarUrl,
1778 displayname: user.displayName
1779 });
1780 } else {
1781 promise = client.getProfileInfo(member.userId);
1782 }
1783 promise.done(function (info) {
1784 // slightly naughty by doctoring the invite event but this means all
1785 // the code paths remain the same between invite/join display name stuff
1786 // which is a worthy trade-off for some minor pollution.
1787 var inviteEvent = member.events.member;
1788 if (inviteEvent.getContent().membership !== "invite") {
1789 // between resolving and now they have since joined, so don't clobber
1790 return;
1791 }
1792 inviteEvent.getContent().avatar_url = info.avatar_url;
1793 inviteEvent.getContent().displayname = info.displayname;
1794 // fire listeners
1795 member.setMembershipEvent(inviteEvent, room.currentState);
1796 }, function (err) {
1797 // OH WELL.
1798 });
1799 });
1800};
1801
1802/**
1803 * @param {Room} room
1804 * @param {MatrixEvent[]} stateEventList A list of state events. This is the state
1805 * at the *START* of the timeline list if it is supplied.
1806 * @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index
1807 * is earlier in time. Higher index is later.
1808 */
1809SyncApi.prototype._processRoomEvents = function (room, stateEventList, timelineEventList) {
1810 // If there are no events in the timeline yet, initialise it with
1811 // the given state events
1812 var liveTimeline = room.getLiveTimeline();
1813 var timelineWasEmpty = liveTimeline.getEvents().length == 0;
1814 if (timelineWasEmpty) {
1815 // Passing these events into initialiseState will freeze them, so we need
1816 // to compute and cache the push actions for them now, otherwise sync dies
1817 // with an attempt to assign to read only property.
1818 // XXX: This is pretty horrible and is assuming all sorts of behaviour from
1819 // these functions that it shouldn't be. We should probably either store the
1820 // push actions cache elsewhere so we can freeze MatrixEvents, or otherwise
1821 // find some solution where MatrixEvents are immutable but allow for a cache
1822 // field.
1823 var _iteratorNormalCompletion2 = true;
1824 var _didIteratorError2 = false;
1825 var _iteratorError2 = undefined;
1826
1827 try {
1828 for (var _iterator2 = (0, _getIterator3.default)(stateEventList), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
1829 var ev = _step2.value;
1830
1831 this.client.getPushActionsForEvent(ev);
1832 }
1833 } catch (err) {
1834 _didIteratorError2 = true;
1835 _iteratorError2 = err;
1836 } finally {
1837 try {
1838 if (!_iteratorNormalCompletion2 && _iterator2.return) {
1839 _iterator2.return();
1840 }
1841 } finally {
1842 if (_didIteratorError2) {
1843 throw _iteratorError2;
1844 }
1845 }
1846 }
1847
1848 liveTimeline.initialiseState(stateEventList);
1849 }
1850
1851 this._resolveInvites(room);
1852
1853 // recalculate the room name at this point as adding events to the timeline
1854 // may make notifications appear which should have the right name.
1855 // XXX: This looks suspect: we'll end up recalculating the room once here
1856 // and then again after adding events (_processSyncResponse calls it after
1857 // calling us) even if no state events were added. It also means that if
1858 // one of the room events in timelineEventList is something that needs
1859 // a recalculation (like m.room.name) we won't recalculate until we've
1860 // finished adding all the events, which will cause the notification to have
1861 // the old room name rather than the new one.
1862 room.recalculate();
1863
1864 // If the timeline wasn't empty, we process the state events here: they're
1865 // defined as updates to the state before the start of the timeline, so this
1866 // starts to roll the state forward.
1867 // XXX: That's what we *should* do, but this can happen if we were previously
1868 // peeking in a room, in which case we obviously do *not* want to add the
1869 // state events here onto the end of the timeline. Historically, the js-sdk
1870 // has just set these new state events on the old and new state. This seems
1871 // very wrong because there could be events in the timeline that diverge the
1872 // state, in which case this is going to leave things out of sync. However,
1873 // for now I think it;s best to behave the same as the code has done previously.
1874 if (!timelineWasEmpty) {
1875 // XXX: As above, don't do this...
1876 //room.addLiveEvents(stateEventList || []);
1877 // Do this instead...
1878 room.oldState.setStateEvents(stateEventList || []);
1879 room.currentState.setStateEvents(stateEventList || []);
1880 }
1881 // execute the timeline events. This will continue to diverge the current state
1882 // if the timeline has any state events in it.
1883 // This also needs to be done before running push rules on the events as they need
1884 // to be decorated with sender etc.
1885 room.addLiveEvents(timelineEventList || []);
1886};
1887
1888/**
1889 * Takes a list of timelineEvents and adds and adds to _notifEvents
1890 * as appropriate.
1891 * This must be called after the room the events belong to has been stored.
1892 *
1893 * @param {Room} room
1894 * @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index
1895 * is earlier in time. Higher index is later.
1896 */
1897SyncApi.prototype._processEventsForNotifs = function (room, timelineEventList) {
1898 // gather our notifications into this._notifEvents
1899 if (this.client.getNotifTimelineSet()) {
1900 for (var i = 0; i < timelineEventList.length; i++) {
1901 var pushActions = this.client.getPushActionsForEvent(timelineEventList[i]);
1902 if (pushActions && pushActions.notify && pushActions.tweaks && pushActions.tweaks.highlight) {
1903 this._notifEvents.push(timelineEventList[i]);
1904 }
1905 }
1906 }
1907};
1908
1909/**
1910 * @return {string}
1911 */
1912SyncApi.prototype._getGuestFilter = function () {
1913 var guestRooms = this.client._guestRooms; // FIXME: horrible gut-wrenching
1914 if (!guestRooms) {
1915 return "{}";
1916 }
1917 // we just need to specify the filter inline if we're a guest because guests
1918 // can't create filters.
1919 return (0, _stringify2.default)({
1920 room: {
1921 timeline: {
1922 limit: 20
1923 }
1924 }
1925 });
1926};
1927
1928/**
1929 * Sets the sync state and emits an event to say so
1930 * @param {String} newState The new state string
1931 * @param {Object} data Object of additional data to emit in the event
1932 */
1933SyncApi.prototype._updateSyncState = function (newState, data) {
1934 var old = this._syncState;
1935 this._syncState = newState;
1936 this._syncStateData = data;
1937 this.client.emit("sync", this._syncState, old, data);
1938};
1939
1940/**
1941 * Event handler for the 'online' event
1942 * This event is generally unreliable and precise behaviour
1943 * varies between browsers, so we poll for connectivity too,
1944 * but this might help us reconnect a little faster.
1945 */
1946SyncApi.prototype._onOnline = function () {
1947 debuglog("Browser thinks we are back online");
1948 this._startKeepAlives(0);
1949};
1950
1951function createNewUser(client, userId) {
1952 var user = new User(userId);
1953 client.reEmitter.reEmit(user, ["User.avatarUrl", "User.displayName", "User.presence", "User.currentlyActive", "User.lastPresenceTs"]);
1954 return user;
1955}
1956
1957/** */
1958module.exports = SyncApi;
1959//# sourceMappingURL=sync.js.map
\No newline at end of file