From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../matrix/lib/matrix-sdk/timeline-window.js | 462 +++++++++++++++++++++ 1 file changed, 462 insertions(+) create mode 100644 comm/chat/protocols/matrix/lib/matrix-sdk/timeline-window.js (limited to 'comm/chat/protocols/matrix/lib/matrix-sdk/timeline-window.js') diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/timeline-window.js b/comm/chat/protocols/matrix/lib/matrix-sdk/timeline-window.js new file mode 100644 index 0000000000..2c7365bdff --- /dev/null +++ b/comm/chat/protocols/matrix/lib/matrix-sdk/timeline-window.js @@ -0,0 +1,462 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.TimelineWindow = exports.TimelineIndex = void 0; +var _eventTimeline = require("./models/event-timeline"); +var _logger = require("./logger"); +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /* + Copyright 2016 - 2021 The Matrix.org Foundation C.I.C. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +/** + * @internal + */ +const DEBUG = false; + +/** + * @internal + */ +/* istanbul ignore next */ +const debuglog = DEBUG ? _logger.logger.log.bind(_logger.logger) : function () {}; + +/** + * the number of times we ask the server for more events before giving up + * + * @internal + */ +const DEFAULT_PAGINATE_LOOP_LIMIT = 5; +class TimelineWindow { + /** + * Construct a TimelineWindow. + * + *

This abstracts the separate timelines in a Matrix {@link Room} into a single iterable thing. + * It keeps track of the start and endpoints of the window, which can be advanced with the help + * of pagination requests. + * + *

Before the window is useful, it must be initialised by calling {@link TimelineWindow#load}. + * + *

Note that the window will not automatically extend itself when new events + * are received from /sync; you should arrange to call {@link TimelineWindow#paginate} + * on {@link RoomEvent.Timeline} events. + * + * @param client - MatrixClient to be used for context/pagination + * requests. + * + * @param timelineSet - The timelineSet to track + * + * @param opts - Configuration options for this window + */ + constructor(client, timelineSet, opts = {}) { + this.client = client; + this.timelineSet = timelineSet; + _defineProperty(this, "windowLimit", void 0); + // these will be TimelineIndex objects; they delineate the 'start' and + // 'end' of the window. + // + // start.index is inclusive; end.index is exclusive. + _defineProperty(this, "start", void 0); + _defineProperty(this, "end", void 0); + _defineProperty(this, "eventCount", 0); + this.windowLimit = opts.windowLimit || 1000; + } + + /** + * Initialise the window to point at a given event, or the live timeline + * + * @param initialEventId - If given, the window will contain the + * given event + * @param initialWindowSize - Size of the initial window + */ + load(initialEventId, initialWindowSize = 20) { + // given an EventTimeline, find the event we were looking for, and initialise our + // fields so that the event in question is in the middle of the window. + const initFields = timeline => { + if (!timeline) { + throw new Error("No timeline given to initFields"); + } + let eventIndex; + const events = timeline.getEvents(); + if (!initialEventId) { + // we were looking for the live timeline: initialise to the end + eventIndex = events.length; + } else { + eventIndex = events.findIndex(e => e.getId() === initialEventId); + if (eventIndex < 0) { + throw new Error("getEventTimeline result didn't include requested event"); + } + } + const endIndex = Math.min(events.length, eventIndex + Math.ceil(initialWindowSize / 2)); + const startIndex = Math.max(0, endIndex - initialWindowSize); + this.start = new TimelineIndex(timeline, startIndex - timeline.getBaseIndex()); + this.end = new TimelineIndex(timeline, endIndex - timeline.getBaseIndex()); + this.eventCount = endIndex - startIndex; + }; + + // We avoid delaying the resolution of the promise by a reactor tick if we already have the data we need, + // which is important to keep room-switching feeling snappy. + if (this.timelineSet.getTimelineForEvent(initialEventId)) { + initFields(this.timelineSet.getTimelineForEvent(initialEventId)); + return Promise.resolve(); + } else if (initialEventId) { + return this.client.getEventTimeline(this.timelineSet, initialEventId).then(initFields); + } else { + initFields(this.timelineSet.getLiveTimeline()); + return Promise.resolve(); + } + } + + /** + * Get the TimelineIndex of the window in the given direction. + * + * @param direction - EventTimeline.BACKWARDS to get the TimelineIndex + * at the start of the window; EventTimeline.FORWARDS to get the TimelineIndex at + * the end. + * + * @returns The requested timeline index if one exists, null + * otherwise. + */ + getTimelineIndex(direction) { + if (direction == _eventTimeline.EventTimeline.BACKWARDS) { + return this.start ?? null; + } else if (direction == _eventTimeline.EventTimeline.FORWARDS) { + return this.end ?? null; + } else { + throw new Error("Invalid direction '" + direction + "'"); + } + } + + /** + * Try to extend the window using events that are already in the underlying + * TimelineIndex. + * + * @param direction - EventTimeline.BACKWARDS to try extending it + * backwards; EventTimeline.FORWARDS to try extending it forwards. + * @param size - number of events to try to extend by. + * + * @returns true if the window was extended, false otherwise. + */ + extend(direction, size) { + const tl = this.getTimelineIndex(direction); + if (!tl) { + debuglog("TimelineWindow: no timeline yet"); + return false; + } + const count = direction == _eventTimeline.EventTimeline.BACKWARDS ? tl.retreat(size) : tl.advance(size); + if (count) { + this.eventCount += count; + debuglog("TimelineWindow: increased cap by " + count + " (now " + this.eventCount + ")"); + // remove some events from the other end, if necessary + const excess = this.eventCount - this.windowLimit; + if (excess > 0) { + this.unpaginate(excess, direction != _eventTimeline.EventTimeline.BACKWARDS); + } + return true; + } + return false; + } + + /** + * Check if this window can be extended + * + *

This returns true if we either have more events, or if we have a + * pagination token which means we can paginate in that direction. It does not + * necessarily mean that there are more events available in that direction at + * this time. + * + * @param direction - EventTimeline.BACKWARDS to check if we can + * paginate backwards; EventTimeline.FORWARDS to check if we can go forwards + * + * @returns true if we can paginate in the given direction + */ + canPaginate(direction) { + const tl = this.getTimelineIndex(direction); + if (!tl) { + debuglog("TimelineWindow: no timeline yet"); + return false; + } + if (direction == _eventTimeline.EventTimeline.BACKWARDS) { + if (tl.index > tl.minIndex()) { + return true; + } + } else { + if (tl.index < tl.maxIndex()) { + return true; + } + } + const hasNeighbouringTimeline = tl.timeline.getNeighbouringTimeline(direction); + const paginationToken = tl.timeline.getPaginationToken(direction); + return Boolean(hasNeighbouringTimeline) || Boolean(paginationToken); + } + + /** + * Attempt to extend the window + * + * @param direction - EventTimeline.BACKWARDS to extend the window + * backwards (towards older events); EventTimeline.FORWARDS to go forwards. + * + * @param size - number of events to try to extend by. If fewer than this + * number are immediately available, then we return immediately rather than + * making an API call. + * + * @param makeRequest - whether we should make API calls to + * fetch further events if we don't have any at all. (This has no effect if + * the room already knows about additional events in the relevant direction, + * even if there are fewer than 'size' of them, as we will just return those + * we already know about.) + * + * @param requestLimit - limit for the number of API requests we + * should make. + * + * @returns Promise which resolves to a boolean which is true if more events + * were successfully retrieved. + */ + async paginate(direction, size, makeRequest = true, requestLimit = DEFAULT_PAGINATE_LOOP_LIMIT) { + // Either wind back the message cap (if there are enough events in the + // timeline to do so), or fire off a pagination request. + const tl = this.getTimelineIndex(direction); + if (!tl) { + debuglog("TimelineWindow: no timeline yet"); + return false; + } + if (tl.pendingPaginate) { + return tl.pendingPaginate; + } + + // try moving the cap + if (this.extend(direction, size)) { + return true; + } + if (!makeRequest || requestLimit === 0) { + // todo: should we return something different to indicate that there + // might be more events out there, but we haven't found them yet? + return false; + } + + // try making a pagination request + const token = tl.timeline.getPaginationToken(direction); + if (!token) { + debuglog("TimelineWindow: no token"); + return false; + } + debuglog("TimelineWindow: starting request"); + const prom = this.client.paginateEventTimeline(tl.timeline, { + backwards: direction == _eventTimeline.EventTimeline.BACKWARDS, + limit: size + }).finally(function () { + tl.pendingPaginate = undefined; + }).then(r => { + debuglog("TimelineWindow: request completed with result " + r); + if (!r) { + return this.paginate(direction, size, false, 0); + } + + // recurse to advance the index into the results. + // + // If we don't get any new events, we want to make sure we keep asking + // the server for events for as long as we have a valid pagination + // token. In particular, we want to know if we've actually hit the + // start of the timeline, or if we just happened to know about all of + // the events thanks to https://matrix.org/jira/browse/SYN-645. + // + // On the other hand, we necessarily want to wait forever for the + // server to make its mind up about whether there are other events, + // because it gives a bad user experience + // (https://github.com/vector-im/vector-web/issues/1204). + return this.paginate(direction, size, true, requestLimit - 1); + }); + tl.pendingPaginate = prom; + return prom; + } + + /** + * Remove `delta` events from the start or end of the timeline. + * + * @param delta - number of events to remove from the timeline + * @param startOfTimeline - if events should be removed from the start + * of the timeline. + */ + unpaginate(delta, startOfTimeline) { + const tl = startOfTimeline ? this.start : this.end; + if (!tl) { + throw new Error(`Attempting to unpaginate startOfTimeline=${startOfTimeline} but don't have this direction`); + } + + // sanity-check the delta + if (delta > this.eventCount || delta < 0) { + throw new Error(`Attemting to unpaginate ${delta} events, but only have ${this.eventCount} in the timeline`); + } + while (delta > 0) { + const count = startOfTimeline ? tl.advance(delta) : tl.retreat(delta); + if (count <= 0) { + // sadness. This shouldn't be possible. + throw new Error("Unable to unpaginate any further, but still have " + this.eventCount + " events"); + } + delta -= count; + this.eventCount -= count; + debuglog("TimelineWindow.unpaginate: dropped " + count + " (now " + this.eventCount + ")"); + } + } + + /** + * Get a list of the events currently in the window + * + * @returns the events in the window + */ + getEvents() { + if (!this.start) { + // not yet loaded + return []; + } + const result = []; + + // iterate through each timeline between this.start and this.end + // (inclusive). + let timeline = this.start.timeline; + // eslint-disable-next-line no-constant-condition + while (timeline) { + const events = timeline.getEvents(); + + // For the first timeline in the chain, we want to start at + // this.start.index. For the last timeline in the chain, we want to + // stop before this.end.index. Otherwise, we want to copy all of the + // events in the timeline. + // + // (Note that both this.start.index and this.end.index are relative + // to their respective timelines' BaseIndex). + // + let startIndex = 0; + let endIndex = events.length; + if (timeline === this.start.timeline) { + startIndex = this.start.index + timeline.getBaseIndex(); + } + if (timeline === this.end?.timeline) { + endIndex = this.end.index + timeline.getBaseIndex(); + } + for (let i = startIndex; i < endIndex; i++) { + result.push(events[i]); + } + + // if we're not done, iterate to the next timeline. + if (timeline === this.end?.timeline) { + break; + } else { + timeline = timeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.FORWARDS); + } + } + return result; + } +} + +/** + * A thing which contains a timeline reference, and an index into it. + * @internal + */ +exports.TimelineWindow = TimelineWindow; +class TimelineIndex { + // index: the indexes are relative to BaseIndex, so could well be negative. + constructor(timeline, index) { + this.timeline = timeline; + this.index = index; + _defineProperty(this, "pendingPaginate", void 0); + } + + /** + * @returns the minimum possible value for the index in the current + * timeline + */ + minIndex() { + return this.timeline.getBaseIndex() * -1; + } + + /** + * @returns the maximum possible value for the index in the current + * timeline (exclusive - ie, it actually returns one more than the index + * of the last element). + */ + maxIndex() { + return this.timeline.getEvents().length - this.timeline.getBaseIndex(); + } + + /** + * Try move the index forward, or into the neighbouring timeline + * + * @param delta - number of events to advance by + * @returns number of events successfully advanced by + */ + advance(delta) { + if (!delta) { + return 0; + } + + // first try moving the index in the current timeline. See if there is room + // to do so. + let cappedDelta; + if (delta < 0) { + // we want to wind the index backwards. + // + // (this.minIndex() - this.index) is a negative number whose magnitude + // is the amount of room we have to wind back the index in the current + // timeline. We cap delta to this quantity. + cappedDelta = Math.max(delta, this.minIndex() - this.index); + if (cappedDelta < 0) { + this.index += cappedDelta; + return cappedDelta; + } + } else { + // we want to wind the index forwards. + // + // (this.maxIndex() - this.index) is a (positive) number whose magnitude + // is the amount of room we have to wind forward the index in the current + // timeline. We cap delta to this quantity. + cappedDelta = Math.min(delta, this.maxIndex() - this.index); + if (cappedDelta > 0) { + this.index += cappedDelta; + return cappedDelta; + } + } + + // the index is already at the start/end of the current timeline. + // + // next see if there is a neighbouring timeline to switch to. + const neighbour = this.timeline.getNeighbouringTimeline(delta < 0 ? _eventTimeline.EventTimeline.BACKWARDS : _eventTimeline.EventTimeline.FORWARDS); + if (neighbour) { + this.timeline = neighbour; + if (delta < 0) { + this.index = this.maxIndex(); + } else { + this.index = this.minIndex(); + } + debuglog("paginate: switched to new neighbour"); + + // recurse, using the next timeline + return this.advance(delta); + } + return 0; + } + + /** + * Try move the index backwards, or into the neighbouring timeline + * + * @param delta - number of events to retreat by + * @returns number of events successfully retreated by + */ + retreat(delta) { + return this.advance(delta * -1) * -1; + } +} +exports.TimelineIndex = TimelineIndex; \ No newline at end of file -- cgit v1.2.3