2020-08-05 22:08:55 +05:30
|
|
|
/*
|
|
|
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2021-09-06 16:39:16 +05:30
|
|
|
import {EventKey} from "../../../room/timeline/EventKey";
|
2021-08-10 02:14:07 +05:30
|
|
|
import { StorageError } from "../../common";
|
2021-08-10 02:26:20 +05:30
|
|
|
import { encodeUint32 } from "../utils";
|
2021-08-10 02:14:07 +05:30
|
|
|
import {KeyLimits} from "../../common";
|
2021-08-12 02:03:25 +05:30
|
|
|
import {Store} from "../Store";
|
2021-09-01 00:40:36 +05:30
|
|
|
import {TimelineEvent, StateEvent} from "../../types";
|
2021-09-17 21:55:28 +05:30
|
|
|
import {LogItem} from "../../../../logging/LogItem.js";
|
2019-01-09 15:36:09 +05:30
|
|
|
|
2021-08-12 02:03:25 +05:30
|
|
|
interface Annotation {
|
|
|
|
count: number;
|
|
|
|
me: boolean;
|
|
|
|
firstTimestamp: number;
|
|
|
|
}
|
|
|
|
|
2021-09-06 16:21:28 +05:30
|
|
|
interface TimelineEventEntry {
|
2021-08-12 02:03:25 +05:30
|
|
|
roomId: string;
|
|
|
|
fragmentId: number;
|
|
|
|
eventIndex: number;
|
2021-09-01 00:40:36 +05:30
|
|
|
event: TimelineEvent | StateEvent;
|
2021-08-12 02:03:25 +05:30
|
|
|
displayName?: string;
|
|
|
|
avatarUrl?: string;
|
|
|
|
annotations?: { [key : string]: Annotation };
|
|
|
|
}
|
|
|
|
|
2021-09-06 16:21:28 +05:30
|
|
|
type TimelineEventStorageEntry = TimelineEventEntry & { key: string, eventIdKey: string };
|
|
|
|
|
2021-08-12 02:03:25 +05:30
|
|
|
function encodeKey(roomId: string, fragmentId: number, eventIndex: number): string {
|
2019-06-27 01:25:33 +05:30
|
|
|
return `${roomId}|${encodeUint32(fragmentId)}|${encodeUint32(eventIndex)}`;
|
|
|
|
}
|
|
|
|
|
2021-09-24 19:10:33 +05:30
|
|
|
function decodeKey(key: string): { roomId: string, eventKey: EventKey } {
|
2021-09-23 21:32:05 +05:30
|
|
|
const [roomId, fragmentId, eventIndex] = key.split("|");
|
2021-09-24 19:10:33 +05:30
|
|
|
return {roomId, eventKey: new EventKey(parseInt(fragmentId, 10), parseInt(eventIndex, 10))};
|
2021-09-23 21:32:05 +05:30
|
|
|
}
|
|
|
|
|
2021-08-12 02:03:25 +05:30
|
|
|
function encodeEventIdKey(roomId: string, eventId: string): string {
|
2019-06-27 01:25:33 +05:30
|
|
|
return `${roomId}|${eventId}`;
|
|
|
|
}
|
|
|
|
|
2021-08-12 02:03:25 +05:30
|
|
|
function decodeEventIdKey(eventIdKey: string): { roomId: string, eventId: string } {
|
2019-06-27 01:25:33 +05:30
|
|
|
const [roomId, eventId] = eventIdKey.split("|");
|
|
|
|
return {roomId, eventId};
|
|
|
|
}
|
|
|
|
|
2019-03-30 03:31:01 +05:30
|
|
|
class Range {
|
2021-09-01 00:46:16 +05:30
|
|
|
private _IDBKeyRange: typeof IDBKeyRange;
|
2021-08-12 02:03:25 +05:30
|
|
|
private _only?: EventKey;
|
|
|
|
private _lower?: EventKey;
|
|
|
|
private _upper?: EventKey;
|
|
|
|
private _lowerOpen: boolean;
|
|
|
|
private _upperOpen: boolean;
|
|
|
|
|
2021-09-01 00:46:16 +05:30
|
|
|
constructor(_IDBKeyRange: any, only?: EventKey, lower?: EventKey, upper?: EventKey, lowerOpen: boolean = false, upperOpen: boolean = false) {
|
|
|
|
this._IDBKeyRange = _IDBKeyRange;
|
2019-03-30 03:31:01 +05:30
|
|
|
this._only = only;
|
|
|
|
this._lower = lower;
|
|
|
|
this._upper = upper;
|
|
|
|
this._lowerOpen = lowerOpen;
|
|
|
|
this._upperOpen = upperOpen;
|
|
|
|
}
|
|
|
|
|
2021-08-12 02:03:25 +05:30
|
|
|
asIDBKeyRange(roomId: string): IDBKeyRange | undefined {
|
2019-06-27 01:30:50 +05:30
|
|
|
try {
|
|
|
|
// only
|
|
|
|
if (this._only) {
|
2021-06-02 16:01:13 +05:30
|
|
|
return this._IDBKeyRange.only(encodeKey(roomId, this._only.fragmentId, this._only.eventIndex));
|
2019-06-27 01:30:50 +05:30
|
|
|
}
|
|
|
|
// lowerBound
|
|
|
|
// also bound as we don't want to move into another roomId
|
|
|
|
if (this._lower && !this._upper) {
|
2021-06-02 16:01:13 +05:30
|
|
|
return this._IDBKeyRange.bound(
|
2019-06-27 01:30:50 +05:30
|
|
|
encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex),
|
2020-10-26 15:04:35 +05:30
|
|
|
encodeKey(roomId, this._lower.fragmentId, KeyLimits.maxStorageKey),
|
2019-06-27 01:30:50 +05:30
|
|
|
this._lowerOpen,
|
|
|
|
false
|
|
|
|
);
|
|
|
|
}
|
|
|
|
// upperBound
|
|
|
|
// also bound as we don't want to move into another roomId
|
|
|
|
if (!this._lower && this._upper) {
|
2021-06-02 16:01:13 +05:30
|
|
|
return this._IDBKeyRange.bound(
|
2020-10-26 15:04:35 +05:30
|
|
|
encodeKey(roomId, this._upper.fragmentId, KeyLimits.minStorageKey),
|
2019-06-27 01:30:50 +05:30
|
|
|
encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex),
|
|
|
|
false,
|
|
|
|
this._upperOpen
|
|
|
|
);
|
|
|
|
}
|
|
|
|
// bound
|
|
|
|
if (this._lower && this._upper) {
|
2021-06-02 16:01:13 +05:30
|
|
|
return this._IDBKeyRange.bound(
|
2019-06-27 01:30:50 +05:30
|
|
|
encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex),
|
|
|
|
encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex),
|
|
|
|
this._lowerOpen,
|
|
|
|
this._upperOpen
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} catch(err) {
|
|
|
|
throw new StorageError(`IDBKeyRange failed with data: ` + JSON.stringify(this), err);
|
2019-03-30 03:31:01 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/*
|
|
|
|
* @typedef {Object} Gap
|
|
|
|
* @property {?string} prev_batch the pagination token for this backwards facing gap
|
|
|
|
* @property {?string} next_batch the pagination token for this forwards facing gap
|
|
|
|
*
|
|
|
|
* @typedef {Object} Event
|
|
|
|
* @property {string} event_id the id of the event
|
|
|
|
* @property {string} type the
|
|
|
|
* @property {?string} state_key the state key of this state event
|
|
|
|
*
|
|
|
|
* @typedef {Object} Entry
|
|
|
|
* @property {string} roomId
|
2019-05-12 23:54:06 +05:30
|
|
|
* @property {EventKey} eventKey
|
2019-03-30 03:31:01 +05:30
|
|
|
* @property {?Event} event if an event entry, the event
|
|
|
|
* @property {?Gap} gap if a gap entry, the gap
|
|
|
|
*/
|
2020-04-21 00:56:39 +05:30
|
|
|
export class TimelineEventStore {
|
2021-09-06 16:21:28 +05:30
|
|
|
private _timelineStore: Store<TimelineEventStorageEntry>;
|
2021-08-12 02:03:25 +05:30
|
|
|
|
2021-09-06 16:21:28 +05:30
|
|
|
constructor(timelineStore: Store<TimelineEventStorageEntry>) {
|
2019-06-27 02:01:36 +05:30
|
|
|
this._timelineStore = timelineStore;
|
|
|
|
}
|
2019-01-09 15:36:09 +05:30
|
|
|
|
2019-03-30 03:31:01 +05:30
|
|
|
/** Creates a range that only includes the given key
|
2021-08-12 02:03:25 +05:30
|
|
|
* @param eventKey the key
|
|
|
|
* @return the created range
|
2019-03-30 03:31:01 +05:30
|
|
|
*/
|
2021-08-12 02:03:25 +05:30
|
|
|
onlyRange(eventKey: EventKey): Range {
|
2021-06-02 16:01:13 +05:30
|
|
|
return new Range(this._timelineStore.IDBKeyRange, eventKey);
|
2019-03-30 03:31:01 +05:30
|
|
|
}
|
|
|
|
|
2019-05-12 23:54:06 +05:30
|
|
|
/** Creates a range that includes all keys before eventKey, and optionally also the key itself.
|
2021-08-12 02:03:25 +05:30
|
|
|
* @param eventKey the key
|
|
|
|
* @param [open=false] whether the key is included (false) or excluded (true) from the range at the upper end.
|
|
|
|
* @return the created range
|
2019-03-30 03:31:01 +05:30
|
|
|
*/
|
2021-08-12 02:03:25 +05:30
|
|
|
upperBoundRange(eventKey: EventKey, open=false): Range {
|
2021-06-02 16:01:13 +05:30
|
|
|
return new Range(this._timelineStore.IDBKeyRange, undefined, undefined, eventKey, undefined, open);
|
2019-03-30 03:31:01 +05:30
|
|
|
}
|
|
|
|
|
2019-05-12 23:54:06 +05:30
|
|
|
/** Creates a range that includes all keys after eventKey, and optionally also the key itself.
|
2021-08-12 02:03:25 +05:30
|
|
|
* @param eventKey the key
|
|
|
|
* @param [open=false] whether the key is included (false) or excluded (true) from the range at the lower end.
|
|
|
|
* @return the created range
|
2019-03-30 03:31:01 +05:30
|
|
|
*/
|
2021-08-12 02:03:25 +05:30
|
|
|
lowerBoundRange(eventKey: EventKey, open=false): Range {
|
2021-06-02 16:01:13 +05:30
|
|
|
return new Range(this._timelineStore.IDBKeyRange, undefined, eventKey, undefined, open);
|
2019-03-30 03:31:01 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
/** Creates a range that includes all keys between `lower` and `upper`, and optionally the given keys as well.
|
2021-08-12 02:03:25 +05:30
|
|
|
* @param lower the lower key
|
|
|
|
* @param upper the upper key
|
|
|
|
* @param [lowerOpen=false] whether the lower key is included (false) or excluded (true) from the range.
|
|
|
|
* @param [upperOpen=false] whether the upper key is included (false) or excluded (true) from the range.
|
|
|
|
* @return the created range
|
2019-03-30 03:31:01 +05:30
|
|
|
*/
|
2021-08-12 02:03:25 +05:30
|
|
|
boundRange(lower: EventKey, upper: EventKey, lowerOpen=false, upperOpen=false): Range {
|
2021-06-02 16:01:13 +05:30
|
|
|
return new Range(this._timelineStore.IDBKeyRange, undefined, lower, upper, lowerOpen, upperOpen);
|
2019-03-30 03:31:01 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
/** Looks up the last `amount` entries in the timeline for `roomId`.
|
2021-08-12 02:03:25 +05:30
|
|
|
* @param roomId
|
|
|
|
* @param fragmentId
|
|
|
|
* @param amount
|
|
|
|
* @return a promise resolving to an array with 0 or more entries, in ascending order.
|
2019-03-30 03:31:01 +05:30
|
|
|
*/
|
2021-09-06 16:21:28 +05:30
|
|
|
async lastEvents(roomId: string, fragmentId: number, amount: number): Promise<TimelineEventEntry[]> {
|
2019-05-12 23:54:06 +05:30
|
|
|
const eventKey = EventKey.maxKey;
|
|
|
|
eventKey.fragmentId = fragmentId;
|
2019-06-27 02:01:36 +05:30
|
|
|
return this.eventsBefore(roomId, eventKey, amount);
|
|
|
|
}
|
2019-01-09 15:36:09 +05:30
|
|
|
|
2019-03-30 03:31:01 +05:30
|
|
|
/** Looks up the first `amount` entries in the timeline for `roomId`.
|
2021-08-12 02:03:25 +05:30
|
|
|
* @param roomId
|
|
|
|
* @param fragmentId
|
|
|
|
* @param amount
|
|
|
|
* @return a promise resolving to an array with 0 or more entries, in ascending order.
|
2019-03-30 03:31:01 +05:30
|
|
|
*/
|
2021-09-06 16:21:28 +05:30
|
|
|
async firstEvents(roomId: string, fragmentId: number, amount: number): Promise<TimelineEventEntry[]> {
|
2019-05-12 23:54:06 +05:30
|
|
|
const eventKey = EventKey.minKey;
|
|
|
|
eventKey.fragmentId = fragmentId;
|
2019-06-27 02:01:36 +05:30
|
|
|
return this.eventsAfter(roomId, eventKey, amount);
|
|
|
|
}
|
2019-01-09 15:36:09 +05:30
|
|
|
|
2019-05-12 23:54:06 +05:30
|
|
|
/** Looks up `amount` entries after `eventKey` in the timeline for `roomId` within the same fragment.
|
|
|
|
* The entry for `eventKey` is not included.
|
2021-08-12 02:03:25 +05:30
|
|
|
* @param roomId
|
|
|
|
* @param eventKey
|
|
|
|
* @param amount
|
|
|
|
* @return a promise resolving to an array with 0 or more entries, in ascending order.
|
2019-03-30 03:31:01 +05:30
|
|
|
*/
|
2021-09-06 16:21:28 +05:30
|
|
|
eventsAfter(roomId: string, eventKey: EventKey, amount: number): Promise<TimelineEventEntry[]> {
|
2019-05-12 23:54:06 +05:30
|
|
|
const idbRange = this.lowerBoundRange(eventKey, true).asIDBKeyRange(roomId);
|
2019-06-27 02:01:36 +05:30
|
|
|
return this._timelineStore.selectLimit(idbRange, amount);
|
|
|
|
}
|
2019-01-09 15:36:09 +05:30
|
|
|
|
2019-05-12 23:54:06 +05:30
|
|
|
/** Looks up `amount` entries before `eventKey` in the timeline for `roomId` within the same fragment.
|
|
|
|
* The entry for `eventKey` is not included.
|
2021-08-12 02:03:25 +05:30
|
|
|
* @param roomId
|
|
|
|
* @param eventKey
|
|
|
|
* @param amount
|
|
|
|
* @return a promise resolving to an array with 0 or more entries, in ascending order.
|
2019-03-30 03:31:01 +05:30
|
|
|
*/
|
2021-09-06 16:21:28 +05:30
|
|
|
async eventsBefore(roomId: string, eventKey: EventKey, amount: number): Promise<TimelineEventEntry[]> {
|
2019-05-12 23:54:06 +05:30
|
|
|
const range = this.upperBoundRange(eventKey, true).asIDBKeyRange(roomId);
|
2019-02-28 03:20:08 +05:30
|
|
|
const events = await this._timelineStore.selectLimitReverse(range, amount);
|
|
|
|
events.reverse(); // because we fetched them backwards
|
|
|
|
return events;
|
|
|
|
}
|
|
|
|
|
2021-09-23 21:32:05 +05:30
|
|
|
async getEventKeysForIds(roomId: string, eventIds: string[]): Promise<Map<string, EventKey>> {
|
|
|
|
const byEventId = this._timelineStore.index("byEventId");
|
|
|
|
const keys = eventIds.map(eventId => encodeEventIdKey(roomId, eventId));
|
|
|
|
const results = new Map();
|
|
|
|
await byEventId.findExistingKeys(keys, false, (indexKey, pk) => {
|
|
|
|
const {eventId} = decodeEventIdKey(indexKey as string);
|
2021-09-24 19:10:33 +05:30
|
|
|
const {eventKey} = decodeKey(pk as string);
|
|
|
|
results.set(eventId, eventKey);
|
2021-09-23 21:32:05 +05:30
|
|
|
return false;
|
|
|
|
});
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
2019-06-03 03:48:52 +05:30
|
|
|
/** Finds the first eventId that occurs in the store, if any.
|
2019-05-11 16:40:31 +05:30
|
|
|
* For optimal performance, `eventIds` should be in chronological order.
|
|
|
|
*
|
|
|
|
* The order in which results are returned might be different than `eventIds`.
|
|
|
|
* Call the return value to obtain the next {id, event} pair.
|
2021-08-12 02:03:25 +05:30
|
|
|
* @param roomId
|
|
|
|
* @param eventIds
|
|
|
|
* @return
|
2019-03-30 03:31:01 +05:30
|
|
|
*/
|
2019-06-03 03:41:12 +05:30
|
|
|
// performance comment from above refers to the fact that there *might*
|
2019-05-11 16:40:31 +05:30
|
|
|
// be a correlation between event_id sorting order and chronology.
|
|
|
|
// In that case we could avoid running over all eventIds, as the reported order by findExistingKeys
|
|
|
|
// would match the order of eventIds. That's why findLast is also passed as backwards to keysExist.
|
|
|
|
// also passing them in chronological order makes sense as that's how we'll receive them almost always.
|
2021-08-12 02:03:25 +05:30
|
|
|
async findFirstOccurringEventId(roomId: string, eventIds: string[]): Promise<string | undefined> {
|
2019-05-11 16:40:31 +05:30
|
|
|
const byEventId = this._timelineStore.index("byEventId");
|
2019-06-27 01:25:33 +05:30
|
|
|
const keys = eventIds.map(eventId => encodeEventIdKey(roomId, eventId));
|
2019-05-11 16:40:31 +05:30
|
|
|
const results = new Array(keys.length);
|
2021-08-12 02:03:25 +05:30
|
|
|
let firstFoundKey: string | undefined;
|
2019-03-09 05:11:06 +05:30
|
|
|
|
2019-05-11 16:40:31 +05:30
|
|
|
// find first result that is found and has no undefined results before it
|
2021-08-12 02:03:25 +05:30
|
|
|
function firstFoundAndPrecedingResolved(): string | undefined {
|
2019-06-03 03:48:52 +05:30
|
|
|
for(let i = 0; i < results.length; ++i) {
|
2019-05-11 16:40:31 +05:30
|
|
|
if (results[i] === undefined) {
|
|
|
|
return;
|
|
|
|
} else if(results[i] === true) {
|
|
|
|
return keys[i];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-03 03:48:52 +05:30
|
|
|
await byEventId.findExistingKeys(keys, false, (key, found) => {
|
2021-08-12 02:03:25 +05:30
|
|
|
// T[].search(T, number), but we want T[].search(R, number), so cast
|
|
|
|
const index = (keys as IDBValidKey[]).indexOf(key);
|
2019-05-11 16:40:31 +05:30
|
|
|
results[index] = found;
|
2019-06-03 03:41:12 +05:30
|
|
|
firstFoundKey = firstFoundAndPrecedingResolved();
|
|
|
|
return !!firstFoundKey;
|
2019-05-11 16:40:31 +05:30
|
|
|
});
|
2019-06-27 01:25:33 +05:30
|
|
|
return firstFoundKey && decodeEventIdKey(firstFoundKey).eventId;
|
2019-03-09 05:11:06 +05:30
|
|
|
}
|
|
|
|
|
2021-09-22 00:34:10 +05:30
|
|
|
/** Inserts a new entry into the store.
|
|
|
|
*
|
|
|
|
* If the event already exists in the store (either the eventKey or the event id
|
|
|
|
* are already known for the given roomId), this operation has no effect.
|
|
|
|
*
|
|
|
|
* Returns if the event was not yet known and the entry was written.
|
2019-03-30 03:31:01 +05:30
|
|
|
*/
|
2021-09-22 00:34:10 +05:30
|
|
|
tryInsert(entry: TimelineEventEntry, log: LogItem): Promise<boolean> {
|
2021-09-06 16:21:28 +05:30
|
|
|
(entry as TimelineEventStorageEntry).key = encodeKey(entry.roomId, entry.fragmentId, entry.eventIndex);
|
|
|
|
(entry as TimelineEventStorageEntry).eventIdKey = encodeEventIdKey(entry.roomId, entry.event.event_id);
|
2021-09-22 00:34:10 +05:30
|
|
|
return this._timelineStore.tryAdd(entry as TimelineEventStorageEntry, log);
|
2019-03-09 05:11:06 +05:30
|
|
|
}
|
|
|
|
|
2019-05-12 23:54:06 +05:30
|
|
|
/** Updates the entry into the store with the given [roomId, eventKey] combination.
|
2019-03-30 03:31:01 +05:30
|
|
|
* If not yet present, will insert. Might be slower than add.
|
2021-08-12 02:03:25 +05:30
|
|
|
* @param entry the entry to update.
|
2021-09-01 00:01:17 +05:30
|
|
|
* @return nothing. To wait for the operation to finish, await the transaction it's part of.
|
2019-03-30 03:31:01 +05:30
|
|
|
*/
|
2021-09-06 16:21:28 +05:30
|
|
|
update(entry: TimelineEventEntry): void {
|
|
|
|
this._timelineStore.put(entry as TimelineEventStorageEntry);
|
2019-02-28 03:20:08 +05:30
|
|
|
}
|
2019-02-04 02:47:24 +05:30
|
|
|
|
2021-09-06 16:21:28 +05:30
|
|
|
get(roomId: string, eventKey: EventKey): Promise<TimelineEventEntry | null> {
|
2019-06-27 01:25:33 +05:30
|
|
|
return this._timelineStore.get(encodeKey(roomId, eventKey.fragmentId, eventKey.eventIndex));
|
2019-03-30 03:31:01 +05:30
|
|
|
}
|
2019-05-12 23:54:06 +05:30
|
|
|
|
2021-09-06 16:21:28 +05:30
|
|
|
getByEventId(roomId: string, eventId: string): Promise<TimelineEventEntry | null> {
|
2019-06-27 01:25:33 +05:30
|
|
|
return this._timelineStore.index("byEventId").get(encodeEventIdKey(roomId, eventId));
|
2019-05-12 23:54:06 +05:30
|
|
|
}
|
2021-05-12 19:08:11 +05:30
|
|
|
|
2021-09-01 00:01:17 +05:30
|
|
|
removeAllForRoom(roomId: string): void {
|
2021-05-12 19:08:11 +05:30
|
|
|
const minKey = encodeKey(roomId, KeyLimits.minStorageKey, KeyLimits.minStorageKey);
|
|
|
|
const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey, KeyLimits.maxStorageKey);
|
2021-06-02 16:01:13 +05:30
|
|
|
const range = this._timelineStore.IDBKeyRange.bound(minKey, maxKey);
|
2021-09-01 00:01:17 +05:30
|
|
|
this._timelineStore.delete(range);
|
2021-05-12 19:08:11 +05:30
|
|
|
}
|
2019-01-09 15:36:09 +05:30
|
|
|
}
|