UNPKG

16.5 kBJavaScriptView Raw
1/*
2Copyright 2016 OpenMarket Ltd
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16"use strict";
17
18/** @module timeline-window */
19
20var _bluebird = require("bluebird");
21
22var _bluebird2 = _interopRequireDefault(_bluebird);
23
24function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
25
26var EventTimeline = require("./models/event-timeline");
27
28/**
29 * @private
30 */
31var DEBUG = false;
32
33/**
34 * @private
35 */
36var debuglog = DEBUG ? console.log.bind(console) : function () {};
37
38/**
39 * the number of times we ask the server for more events before giving up
40 *
41 * @private
42 */
43var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
44
45/**
46 * Construct a TimelineWindow.
47 *
48 * <p>This abstracts the separate timelines in a Matrix {@link
49 * module:models/room|Room} into a single iterable thing. It keeps track of
50 * the start and endpoints of the window, which can be advanced with the help
51 * of pagination requests.
52 *
53 * <p>Before the window is useful, it must be initialised by calling {@link
54 * module:timeline-window~TimelineWindow#load|load}.
55 *
56 * <p>Note that the window will not automatically extend itself when new events
57 * are received from /sync; you should arrange to call {@link
58 * module:timeline-window~TimelineWindow#paginate|paginate} on {@link
59 * module:client~MatrixClient.event:"Room.timeline"|Room.timeline} events.
60 *
61 * @param {MatrixClient} client MatrixClient to be used for context/pagination
62 * requests.
63 *
64 * @param {EventTimelineSet} timelineSet The timelineSet to track
65 *
66 * @param {Object} [opts] Configuration options for this window
67 *
68 * @param {number} [opts.windowLimit = 1000] maximum number of events to keep
69 * in the window. If more events are retrieved via pagination requests,
70 * excess events will be dropped from the other end of the window.
71 *
72 * @constructor
73 */
74function TimelineWindow(client, timelineSet, opts) {
75 opts = opts || {};
76 this._client = client;
77 this._timelineSet = timelineSet;
78
79 // these will be TimelineIndex objects; they delineate the 'start' and
80 // 'end' of the window.
81 //
82 // _start.index is inclusive; _end.index is exclusive.
83 this._start = null;
84 this._end = null;
85
86 this._eventCount = 0;
87 this._windowLimit = opts.windowLimit || 1000;
88}
89
90/**
91 * Initialise the window to point at a given event, or the live timeline
92 *
93 * @param {string} [initialEventId] If given, the window will contain the
94 * given event
95 * @param {number} [initialWindowSize = 20] Size of the initial window
96 *
97 * @return {module:client.Promise}
98 */
99TimelineWindow.prototype.load = function (initialEventId, initialWindowSize) {
100 var self = this;
101 initialWindowSize = initialWindowSize || 20;
102
103 // given an EventTimeline, find the event we were looking for, and initialise our
104 // fields so that the event in question is in the middle of the window.
105 var initFields = function initFields(timeline) {
106 var eventIndex = void 0;
107
108 var events = timeline.getEvents();
109
110 if (!initialEventId) {
111 // we were looking for the live timeline: initialise to the end
112 eventIndex = events.length;
113 } else {
114 for (var i = 0; i < events.length; i++) {
115 if (events[i].getId() == initialEventId) {
116 eventIndex = i;
117 break;
118 }
119 }
120
121 if (eventIndex === undefined) {
122 throw new Error("getEventTimeline result didn't include requested event");
123 }
124 }
125
126 var endIndex = Math.min(events.length, eventIndex + Math.ceil(initialWindowSize / 2));
127 var startIndex = Math.max(0, endIndex - initialWindowSize);
128 self._start = new TimelineIndex(timeline, startIndex - timeline.getBaseIndex());
129 self._end = new TimelineIndex(timeline, endIndex - timeline.getBaseIndex());
130 self._eventCount = endIndex - startIndex;
131 };
132
133 // We avoid delaying the resolution of the promise by a reactor tick if
134 // we already have the data we need, which is important to keep room-switching
135 // feeling snappy.
136 //
137 if (initialEventId) {
138 var prom = this._client.getEventTimeline(this._timelineSet, initialEventId);
139
140 if (prom.isFulfilled()) {
141 initFields(prom.value());
142 return _bluebird2.default.resolve();
143 } else {
144 return prom.then(initFields);
145 }
146 } else {
147 var tl = this._timelineSet.getLiveTimeline();
148 initFields(tl);
149 return _bluebird2.default.resolve();
150 }
151};
152
153/**
154 * Check if this window can be extended
155 *
156 * <p>This returns true if we either have more events, or if we have a
157 * pagination token which means we can paginate in that direction. It does not
158 * necessarily mean that there are more events available in that direction at
159 * this time.
160 *
161 * @param {string} direction EventTimeline.BACKWARDS to check if we can
162 * paginate backwards; EventTimeline.FORWARDS to check if we can go forwards
163 *
164 * @return {boolean} true if we can paginate in the given direction
165 */
166TimelineWindow.prototype.canPaginate = function (direction) {
167 var tl = void 0;
168 if (direction == EventTimeline.BACKWARDS) {
169 tl = this._start;
170 } else if (direction == EventTimeline.FORWARDS) {
171 tl = this._end;
172 } else {
173 throw new Error("Invalid direction '" + direction + "'");
174 }
175
176 if (!tl) {
177 debuglog("TimelineWindow: no timeline yet");
178 return false;
179 }
180
181 if (direction == EventTimeline.BACKWARDS) {
182 if (tl.index > tl.minIndex()) {
183 return true;
184 }
185 } else {
186 if (tl.index < tl.maxIndex()) {
187 return true;
188 }
189 }
190
191 return Boolean(tl.timeline.getNeighbouringTimeline(direction) || tl.timeline.getPaginationToken(direction));
192};
193
194/**
195 * Attempt to extend the window
196 *
197 * @param {string} direction EventTimeline.BACKWARDS to extend the window
198 * backwards (towards older events); EventTimeline.FORWARDS to go forwards.
199 *
200 * @param {number} size number of events to try to extend by. If fewer than this
201 * number are immediately available, then we return immediately rather than
202 * making an API call.
203 *
204 * @param {boolean} [makeRequest = true] whether we should make API calls to
205 * fetch further events if we don't have any at all. (This has no effect if
206 * the room already knows about additional events in the relevant direction,
207 * even if there are fewer than 'size' of them, as we will just return those
208 * we already know about.)
209 *
210 * @param {number} [requestLimit = 5] limit for the number of API requests we
211 * should make.
212 *
213 * @return {module:client.Promise} Resolves to a boolean which is true if more events
214 * were successfully retrieved.
215 */
216TimelineWindow.prototype.paginate = function (direction, size, makeRequest, requestLimit) {
217 // Either wind back the message cap (if there are enough events in the
218 // timeline to do so), or fire off a pagination request.
219
220 if (makeRequest === undefined) {
221 makeRequest = true;
222 }
223
224 if (requestLimit === undefined) {
225 requestLimit = DEFAULT_PAGINATE_LOOP_LIMIT;
226 }
227
228 var tl = void 0;
229 if (direction == EventTimeline.BACKWARDS) {
230 tl = this._start;
231 } else if (direction == EventTimeline.FORWARDS) {
232 tl = this._end;
233 } else {
234 throw new Error("Invalid direction '" + direction + "'");
235 }
236
237 if (!tl) {
238 debuglog("TimelineWindow: no timeline yet");
239 return _bluebird2.default.resolve(false);
240 }
241
242 if (tl.pendingPaginate) {
243 return tl.pendingPaginate;
244 }
245
246 // try moving the cap
247 var count = direction == EventTimeline.BACKWARDS ? tl.retreat(size) : tl.advance(size);
248
249 if (count) {
250 this._eventCount += count;
251 debuglog("TimelineWindow: increased cap by " + count + " (now " + this._eventCount + ")");
252 // remove some events from the other end, if necessary
253 var excess = this._eventCount - this._windowLimit;
254 if (excess > 0) {
255 this.unpaginate(excess, direction != EventTimeline.BACKWARDS);
256 }
257 return _bluebird2.default.resolve(true);
258 }
259
260 if (!makeRequest || requestLimit === 0) {
261 // todo: should we return something different to indicate that there
262 // might be more events out there, but we haven't found them yet?
263 return _bluebird2.default.resolve(false);
264 }
265
266 // try making a pagination request
267 var token = tl.timeline.getPaginationToken(direction);
268 if (!token) {
269 debuglog("TimelineWindow: no token");
270 return _bluebird2.default.resolve(false);
271 }
272
273 debuglog("TimelineWindow: starting request");
274 var self = this;
275
276 var prom = this._client.paginateEventTimeline(tl.timeline, {
277 backwards: direction == EventTimeline.BACKWARDS,
278 limit: size
279 }).finally(function () {
280 tl.pendingPaginate = null;
281 }).then(function (r) {
282 debuglog("TimelineWindow: request completed with result " + r);
283 if (!r) {
284 // end of timeline
285 return false;
286 }
287
288 // recurse to advance the index into the results.
289 //
290 // If we don't get any new events, we want to make sure we keep asking
291 // the server for events for as long as we have a valid pagination
292 // token. In particular, we want to know if we've actually hit the
293 // start of the timeline, or if we just happened to know about all of
294 // the events thanks to https://matrix.org/jira/browse/SYN-645.
295 //
296 // On the other hand, we necessarily want to wait forever for the
297 // server to make its mind up about whether there are other events,
298 // because it gives a bad user experience
299 // (https://github.com/vector-im/vector-web/issues/1204).
300 return self.paginate(direction, size, true, requestLimit - 1);
301 });
302 tl.pendingPaginate = prom;
303 return prom;
304};
305
306/**
307 * Remove `delta` events from the start or end of the timeline.
308 *
309 * @param {number} delta number of events to remove from the timeline
310 * @param {boolean} startOfTimeline if events should be removed from the start
311 * of the timeline.
312 */
313TimelineWindow.prototype.unpaginate = function (delta, startOfTimeline) {
314 var tl = startOfTimeline ? this._start : this._end;
315
316 // sanity-check the delta
317 if (delta > this._eventCount || delta < 0) {
318 throw new Error("Attemting to unpaginate " + delta + " events, but " + "only have " + this._eventCount + " in the timeline");
319 }
320
321 while (delta > 0) {
322 var count = startOfTimeline ? tl.advance(delta) : tl.retreat(delta);
323 if (count <= 0) {
324 // sadness. This shouldn't be possible.
325 throw new Error("Unable to unpaginate any further, but still have " + this._eventCount + " events");
326 }
327
328 delta -= count;
329 this._eventCount -= count;
330 debuglog("TimelineWindow.unpaginate: dropped " + count + " (now " + this._eventCount + ")");
331 }
332};
333
334/**
335 * Get a list of the events currently in the window
336 *
337 * @return {MatrixEvent[]} the events in the window
338 */
339TimelineWindow.prototype.getEvents = function () {
340 if (!this._start) {
341 // not yet loaded
342 return [];
343 }
344
345 var result = [];
346
347 // iterate through each timeline between this._start and this._end
348 // (inclusive).
349 var timeline = this._start.timeline;
350 while (true) {
351 var events = timeline.getEvents();
352
353 // For the first timeline in the chain, we want to start at
354 // this._start.index. For the last timeline in the chain, we want to
355 // stop before this._end.index. Otherwise, we want to copy all of the
356 // events in the timeline.
357 //
358 // (Note that both this._start.index and this._end.index are relative
359 // to their respective timelines' BaseIndex).
360 //
361 var startIndex = 0,
362 endIndex = events.length;
363 if (timeline === this._start.timeline) {
364 startIndex = this._start.index + timeline.getBaseIndex();
365 }
366 if (timeline === this._end.timeline) {
367 endIndex = this._end.index + timeline.getBaseIndex();
368 }
369
370 for (var i = startIndex; i < endIndex; i++) {
371 result.push(events[i]);
372 }
373
374 // if we're not done, iterate to the next timeline.
375 if (timeline === this._end.timeline) {
376 break;
377 } else {
378 timeline = timeline.getNeighbouringTimeline(EventTimeline.FORWARDS);
379 }
380 }
381
382 return result;
383};
384
385/**
386 * a thing which contains a timeline reference, and an index into it.
387 *
388 * @constructor
389 * @param {EventTimeline} timeline
390 * @param {number} index
391 * @private
392 */
393function TimelineIndex(timeline, index) {
394 this.timeline = timeline;
395
396 // the indexes are relative to BaseIndex, so could well be negative.
397 this.index = index;
398}
399
400/**
401 * @return {number} the minimum possible value for the index in the current
402 * timeline
403 */
404TimelineIndex.prototype.minIndex = function () {
405 return this.timeline.getBaseIndex() * -1;
406};
407
408/**
409 * @return {number} the maximum possible value for the index in the current
410 * timeline (exclusive - ie, it actually returns one more than the index
411 * of the last element).
412 */
413TimelineIndex.prototype.maxIndex = function () {
414 return this.timeline.getEvents().length - this.timeline.getBaseIndex();
415};
416
417/**
418 * Try move the index forward, or into the neighbouring timeline
419 *
420 * @param {number} delta number of events to advance by
421 * @return {number} number of events successfully advanced by
422 */
423TimelineIndex.prototype.advance = function (delta) {
424 if (!delta) {
425 return 0;
426 }
427
428 // first try moving the index in the current timeline. See if there is room
429 // to do so.
430 var cappedDelta = void 0;
431 if (delta < 0) {
432 // we want to wind the index backwards.
433 //
434 // (this.minIndex() - this.index) is a negative number whose magnitude
435 // is the amount of room we have to wind back the index in the current
436 // timeline. We cap delta to this quantity.
437 cappedDelta = Math.max(delta, this.minIndex() - this.index);
438 if (cappedDelta < 0) {
439 this.index += cappedDelta;
440 return cappedDelta;
441 }
442 } else {
443 // we want to wind the index forwards.
444 //
445 // (this.maxIndex() - this.index) is a (positive) number whose magnitude
446 // is the amount of room we have to wind forward the index in the current
447 // timeline. We cap delta to this quantity.
448 cappedDelta = Math.min(delta, this.maxIndex() - this.index);
449 if (cappedDelta > 0) {
450 this.index += cappedDelta;
451 return cappedDelta;
452 }
453 }
454
455 // the index is already at the start/end of the current timeline.
456 //
457 // next see if there is a neighbouring timeline to switch to.
458 var neighbour = this.timeline.getNeighbouringTimeline(delta < 0 ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS);
459 if (neighbour) {
460 this.timeline = neighbour;
461 if (delta < 0) {
462 this.index = this.maxIndex();
463 } else {
464 this.index = this.minIndex();
465 }
466
467 debuglog("paginate: switched to new neighbour");
468
469 // recurse, using the next timeline
470 return this.advance(delta);
471 }
472
473 return 0;
474};
475
476/**
477 * Try move the index backwards, or into the neighbouring timeline
478 *
479 * @param {number} delta number of events to retreat by
480 * @return {number} number of events successfully retreated by
481 */
482TimelineIndex.prototype.retreat = function (delta) {
483 return this.advance(delta * -1) * -1;
484};
485
486/**
487 * The TimelineWindow class.
488 */
489module.exports.TimelineWindow = TimelineWindow;
490
491/**
492 * The TimelineIndex class. exported here for unit testing.
493 */
494module.exports.TimelineIndex = TimelineIndex;
495//# sourceMappingURL=timeline-window.js.map
\No newline at end of file