From e3b1d034f0286a061869770f7cd2bc1896ca187e Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Wed, 11 Aug 2021 13:33:25 -0700 Subject: [PATCH] Migrate TimelineEventStore.js to TypeScript --- src/matrix/storage/idb/Transaction.js | 2 +- ...ineEventStore.js => TimelineEventStore.ts} | 154 +++++++++++------- src/matrix/storage/types.ts | 28 ++++ 3 files changed, 121 insertions(+), 63 deletions(-) rename src/matrix/storage/idb/stores/{TimelineEventStore.js => TimelineEventStore.ts} (64%) create mode 100644 src/matrix/storage/types.ts diff --git a/src/matrix/storage/idb/Transaction.js b/src/matrix/storage/idb/Transaction.js index 72716fe5..a047a7a2 100644 --- a/src/matrix/storage/idb/Transaction.js +++ b/src/matrix/storage/idb/Transaction.js @@ -20,7 +20,7 @@ import {Store} from "./Store"; import {SessionStore} from "./stores/SessionStore"; import {RoomSummaryStore} from "./stores/RoomSummaryStore"; import {InviteStore} from "./stores/InviteStore"; -import {TimelineEventStore} from "./stores/TimelineEventStore.js"; +import {TimelineEventStore} from "./stores/TimelineEventStore"; import {TimelineRelationStore} from "./stores/TimelineRelationStore.js"; import {RoomStateStore} from "./stores/RoomStateStore.js"; import {RoomMemberStore} from "./stores/RoomMemberStore"; diff --git a/src/matrix/storage/idb/stores/TimelineEventStore.js b/src/matrix/storage/idb/stores/TimelineEventStore.ts similarity index 64% rename from src/matrix/storage/idb/stores/TimelineEventStore.js rename to src/matrix/storage/idb/stores/TimelineEventStore.ts index 8ff445f0..764b886b 100644 --- a/src/matrix/storage/idb/stores/TimelineEventStore.js +++ b/src/matrix/storage/idb/stores/TimelineEventStore.ts @@ -18,22 +18,49 @@ import {EventKey} from "../../../room/timeline/EventKey.js"; import { StorageError } from "../../common"; import { encodeUint32 } from "../utils"; import {KeyLimits} from "../../common"; +import {Store} from "../Store"; +import {RoomEvent, StateEvent} from "../../types"; -function encodeKey(roomId, fragmentId, eventIndex) { +interface Annotation { + count: number; + me: boolean; + firstTimestamp: number; +} + +interface StorageEntry { + roomId: string; + fragmentId: number; + eventIndex: number; + event: RoomEvent | StateEvent; + displayName?: string; + avatarUrl?: string; + annotations?: { [key : string]: Annotation }; + key: string; + eventIdKey: string; +} + +function encodeKey(roomId: string, fragmentId: number, eventIndex: number): string { return `${roomId}|${encodeUint32(fragmentId)}|${encodeUint32(eventIndex)}`; } -function encodeEventIdKey(roomId, eventId) { +function encodeEventIdKey(roomId: string, eventId: string): string { return `${roomId}|${eventId}`; } -function decodeEventIdKey(eventIdKey) { +function decodeEventIdKey(eventIdKey: string): { roomId: string, eventId: string } { const [roomId, eventId] = eventIdKey.split("|"); return {roomId, eventId}; } class Range { - constructor(IDBKeyRange, only, lower, upper, lowerOpen, upperOpen) { + private _IDBKeyRange: any; // TODO what's the appropriate representation here? + private _only?: EventKey; + private _lower?: EventKey; + private _upper?: EventKey; + private _lowerOpen: boolean; + private _upperOpen: boolean; + + constructor(IDBKeyRange: any, only?: EventKey, lower?: EventKey, upper?: EventKey, lowerOpen: boolean = false, upperOpen: boolean = false) { this._IDBKeyRange = IDBKeyRange; this._only = only; this._lower = lower; @@ -42,7 +69,7 @@ class Range { this._upperOpen = upperOpen; } - asIDBKeyRange(roomId) { + asIDBKeyRange(roomId: string): IDBKeyRange | undefined { try { // only if (this._only) { @@ -99,66 +126,68 @@ class Range { * @property {?Gap} gap if a gap entry, the gap */ export class TimelineEventStore { - constructor(timelineStore) { + private _timelineStore: Store; + + constructor(timelineStore: Store) { this._timelineStore = timelineStore; } /** Creates a range that only includes the given key - * @param {EventKey} eventKey the key - * @return {Range} the created range + * @param eventKey the key + * @return the created range */ - onlyRange(eventKey) { + onlyRange(eventKey: EventKey): Range { return new Range(this._timelineStore.IDBKeyRange, eventKey); } /** Creates a range that includes all keys before eventKey, and optionally also the key itself. - * @param {EventKey} eventKey the key - * @param {boolean} [open=false] whether the key is included (false) or excluded (true) from the range at the upper end. - * @return {Range} the created range + * @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 */ - upperBoundRange(eventKey, open=false) { + upperBoundRange(eventKey: EventKey, open=false): Range { return new Range(this._timelineStore.IDBKeyRange, undefined, undefined, eventKey, undefined, open); } /** Creates a range that includes all keys after eventKey, and optionally also the key itself. - * @param {EventKey} eventKey the key - * @param {boolean} [open=false] whether the key is included (false) or excluded (true) from the range at the lower end. - * @return {Range} the created range + * @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 */ - lowerBoundRange(eventKey, open=false) { + lowerBoundRange(eventKey: EventKey, open=false): Range { return new Range(this._timelineStore.IDBKeyRange, undefined, eventKey, undefined, open); } /** Creates a range that includes all keys between `lower` and `upper`, and optionally the given keys as well. - * @param {EventKey} lower the lower key - * @param {EventKey} upper the upper key - * @param {boolean} [lowerOpen=false] whether the lower key is included (false) or excluded (true) from the range. - * @param {boolean} [upperOpen=false] whether the upper key is included (false) or excluded (true) from the range. - * @return {Range} the created range + * @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 */ - boundRange(lower, upper, lowerOpen=false, upperOpen=false) { + boundRange(lower: EventKey, upper: EventKey, lowerOpen=false, upperOpen=false): Range { return new Range(this._timelineStore.IDBKeyRange, undefined, lower, upper, lowerOpen, upperOpen); } /** Looks up the last `amount` entries in the timeline for `roomId`. - * @param {string} roomId - * @param {number} fragmentId - * @param {number} amount - * @return {Promise} a promise resolving to an array with 0 or more entries, in ascending order. + * @param roomId + * @param fragmentId + * @param amount + * @return a promise resolving to an array with 0 or more entries, in ascending order. */ - async lastEvents(roomId, fragmentId, amount) { + async lastEvents(roomId: string, fragmentId: number, amount: number): Promise { const eventKey = EventKey.maxKey; eventKey.fragmentId = fragmentId; return this.eventsBefore(roomId, eventKey, amount); } /** Looks up the first `amount` entries in the timeline for `roomId`. - * @param {string} roomId - * @param {number} fragmentId - * @param {number} amount - * @return {Promise} a promise resolving to an array with 0 or more entries, in ascending order. + * @param roomId + * @param fragmentId + * @param amount + * @return a promise resolving to an array with 0 or more entries, in ascending order. */ - async firstEvents(roomId, fragmentId, amount) { + async firstEvents(roomId: string, fragmentId: number, amount: number): Promise { const eventKey = EventKey.minKey; eventKey.fragmentId = fragmentId; return this.eventsAfter(roomId, eventKey, amount); @@ -166,24 +195,24 @@ export class TimelineEventStore { /** Looks up `amount` entries after `eventKey` in the timeline for `roomId` within the same fragment. * The entry for `eventKey` is not included. - * @param {string} roomId - * @param {EventKey} eventKey - * @param {number} amount - * @return {Promise} a promise resolving to an array with 0 or more entries, in ascending order. + * @param roomId + * @param eventKey + * @param amount + * @return a promise resolving to an array with 0 or more entries, in ascending order. */ - eventsAfter(roomId, eventKey, amount) { + eventsAfter(roomId: string, eventKey: EventKey, amount: number): Promise { const idbRange = this.lowerBoundRange(eventKey, true).asIDBKeyRange(roomId); return this._timelineStore.selectLimit(idbRange, amount); } /** Looks up `amount` entries before `eventKey` in the timeline for `roomId` within the same fragment. * The entry for `eventKey` is not included. - * @param {string} roomId - * @param {EventKey} eventKey - * @param {number} amount - * @return {Promise} a promise resolving to an array with 0 or more entries, in ascending order. + * @param roomId + * @param eventKey + * @param amount + * @return a promise resolving to an array with 0 or more entries, in ascending order. */ - async eventsBefore(roomId, eventKey, amount) { + async eventsBefore(roomId: string, eventKey: EventKey, amount: number): Promise { const range = this.upperBoundRange(eventKey, true).asIDBKeyRange(roomId); const events = await this._timelineStore.selectLimitReverse(range, amount); events.reverse(); // because we fetched them backwards @@ -195,23 +224,23 @@ export class TimelineEventStore { * * The order in which results are returned might be different than `eventIds`. * Call the return value to obtain the next {id, event} pair. - * @param {string} roomId - * @param {string[]} eventIds - * @return {Function} + * @param roomId + * @param eventIds + * @return */ // performance comment from above refers to the fact that there *might* // 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. - async findFirstOccurringEventId(roomId, eventIds) { + async findFirstOccurringEventId(roomId: string, eventIds: string[]): Promise { const byEventId = this._timelineStore.index("byEventId"); const keys = eventIds.map(eventId => encodeEventIdKey(roomId, eventId)); const results = new Array(keys.length); - let firstFoundKey; + let firstFoundKey: string | undefined; // find first result that is found and has no undefined results before it - function firstFoundAndPrecedingResolved() { + function firstFoundAndPrecedingResolved(): string | undefined { for(let i = 0; i < results.length; ++i) { if (results[i] === undefined) { return; @@ -222,7 +251,8 @@ export class TimelineEventStore { } await byEventId.findExistingKeys(keys, false, (key, found) => { - const index = keys.indexOf(key); + // T[].search(T, number), but we want T[].search(R, number), so cast + const index = (keys as IDBValidKey[]).indexOf(key); results[index] = found; firstFoundKey = firstFoundAndPrecedingResolved(); return !!firstFoundKey; @@ -231,38 +261,38 @@ export class TimelineEventStore { } /** Inserts a new entry into the store. The combination of roomId and eventKey should not exist yet, or an error is thrown. - * @param {Entry} entry the entry to insert - * @return {Promise<>} a promise resolving to undefined if the operation was successful, or a StorageError if not. + * @param entry the entry to insert + * @return a promise resolving to undefined if the operation was successful, or a StorageError if not. * @throws {StorageError} ... */ - insert(entry) { + insert(entry: StorageEntry): Promise { entry.key = encodeKey(entry.roomId, entry.fragmentId, entry.eventIndex); entry.eventIdKey = encodeEventIdKey(entry.roomId, entry.event.event_id); // TODO: map error? or in idb/store? - this._timelineStore.add(entry); + return this._timelineStore.add(entry); } /** Updates the entry into the store with the given [roomId, eventKey] combination. * If not yet present, will insert. Might be slower than add. - * @param {Entry} entry the entry to update. - * @return {Promise<>} a promise resolving to undefined if the operation was successful, or a StorageError if not. + * @param entry the entry to update. + * @return a promise resolving to undefined if the operation was successful, or a StorageError if not. */ - update(entry) { - this._timelineStore.put(entry); + update(entry: StorageEntry): Promise { + return this._timelineStore.put(entry); } - get(roomId, eventKey) { + get(roomId: string, eventKey: EventKey): Promise { return this._timelineStore.get(encodeKey(roomId, eventKey.fragmentId, eventKey.eventIndex)); } - getByEventId(roomId, eventId) { + getByEventId(roomId: string, eventId: string): Promise { return this._timelineStore.index("byEventId").get(encodeEventIdKey(roomId, eventId)); } - removeAllForRoom(roomId) { + removeAllForRoom(roomId: string): Promise { const minKey = encodeKey(roomId, KeyLimits.minStorageKey, KeyLimits.minStorageKey); const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey, KeyLimits.maxStorageKey); const range = this._timelineStore.IDBKeyRange.bound(minKey, maxKey); - this._timelineStore.delete(range); + return this._timelineStore.delete(range); } } diff --git a/src/matrix/storage/types.ts b/src/matrix/storage/types.ts new file mode 100644 index 00000000..bc61e1f8 --- /dev/null +++ b/src/matrix/storage/types.ts @@ -0,0 +1,28 @@ +/* +Copyright 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. +*/ + +export type Content = { [key: string]: any } + +export interface RoomEvent { + content: Content; + type: string; + event_id: string; + sender: string; + origin_server_ts: number; + unsigned?: Content; +} + +export type StateEvent = RoomEvent & { prev_content?: Content, state_key: string }