Merge pull request #473 from vector-im/snowpack-ts-storage-2

Snowpack + Typescript conversion (Part 2)
This commit is contained in:
Bruno Windels 2021-09-06 11:06:46 +02:00 committed by GitHub
commit 5d8e66a3f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 262 additions and 149 deletions

View file

@ -148,7 +148,7 @@ export class Store<T> extends QueryTarget<T> {
return new QueryTarget<T>(new QueryTargetWrapper<T>(this._idbStore.index(indexName))); return new QueryTarget<T>(new QueryTargetWrapper<T>(this._idbStore.index(indexName)));
} }
put(value: T): Promise<IDBValidKey> { put(value: T): void {
// If this request fails, the error will bubble up to the transaction and abort it, // If this request fails, the error will bubble up to the transaction and abort it,
// which is the behaviour we want. Therefore, it is ok to not create a promise for this // which is the behaviour we want. Therefore, it is ok to not create a promise for this
// request and await it. // request and await it.
@ -159,12 +159,12 @@ export class Store<T> extends QueryTarget<T> {
// //
// Note that this can still throw synchronously, like it does for TransactionInactiveError, // Note that this can still throw synchronously, like it does for TransactionInactiveError,
// see https://www.w3.org/TR/IndexedDB-2/#transaction-lifetime-concept // see https://www.w3.org/TR/IndexedDB-2/#transaction-lifetime-concept
return reqAsPromise(this._idbStore.put(value)); this._idbStore.put(value);
} }
add(value: T): Promise<IDBValidKey> { add(value: T): void {
// ok to not monitor result of request, see comment in `put`. // ok to not monitor result of request, see comment in `put`.
return reqAsPromise(this._idbStore.add(value)); this._idbStore.add(value);
} }
delete(keyOrKeyRange: IDBValidKey | IDBKeyRange): Promise<undefined> { delete(keyOrKeyRange: IDBValidKey | IDBKeyRange): Promise<undefined> {

View file

@ -17,13 +17,13 @@ limitations under the License.
import {txnAsPromise} from "./utils"; import {txnAsPromise} from "./utils";
import {StorageError} from "../common"; import {StorageError} from "../common";
import {Store} from "./Store"; import {Store} from "./Store";
import {SessionStore} from "./stores/SessionStore.js"; import {SessionStore} from "./stores/SessionStore";
import {RoomSummaryStore} from "./stores/RoomSummaryStore.js"; import {RoomSummaryStore} from "./stores/RoomSummaryStore";
import {InviteStore} from "./stores/InviteStore.js"; import {InviteStore} from "./stores/InviteStore";
import {TimelineEventStore} from "./stores/TimelineEventStore.js"; import {TimelineEventStore} from "./stores/TimelineEventStore";
import {TimelineRelationStore} from "./stores/TimelineRelationStore.js"; import {TimelineRelationStore} from "./stores/TimelineRelationStore";
import {RoomStateStore} from "./stores/RoomStateStore.js"; import {RoomStateStore} from "./stores/RoomStateStore.js";
import {RoomMemberStore} from "./stores/RoomMemberStore.js"; import {RoomMemberStore} from "./stores/RoomMemberStore";
import {TimelineFragmentStore} from "./stores/TimelineFragmentStore.js"; import {TimelineFragmentStore} from "./stores/TimelineFragmentStore.js";
import {PendingEventStore} from "./stores/PendingEventStore.js"; import {PendingEventStore} from "./stores/PendingEventStore.js";
import {UserIdentityStore} from "./stores/UserIdentityStore.js"; import {UserIdentityStore} from "./stores/UserIdentityStore.js";

View file

@ -1,10 +1,10 @@
import {iterateCursor, reqAsPromise} from "./utils"; import {iterateCursor, reqAsPromise} from "./utils";
import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../room/members/RoomMember.js"; import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../room/members/RoomMember.js";
import {addRoomToIdentity} from "../../e2ee/DeviceTracker.js"; import {addRoomToIdentity} from "../../e2ee/DeviceTracker.js";
import {RoomMemberStore} from "./stores/RoomMemberStore.js"; import {RoomMemberStore} from "./stores/RoomMemberStore";
import {SessionStore} from "./stores/SessionStore.js"; import {SessionStore} from "./stores/SessionStore";
import {encodeScopeTypeKey} from "./stores/OperationStore.js"; import {encodeScopeTypeKey} from "./stores/OperationStore.js";
import {MAX_UNICODE} from "./stores/common.js"; import {MAX_UNICODE} from "./stores/common";
// FUNCTIONS SHOULD ONLY BE APPENDED!! // FUNCTIONS SHOULD ONLY BE APPENDED!!
// the index in the array is the database version // the index in the array is the database version

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {MAX_UNICODE, MIN_UNICODE} from "./common.js"; import {MAX_UNICODE, MIN_UNICODE} from "./common";
function encodeKey(userId, deviceId) { function encodeKey(userId, deviceId) {
return `${userId}|${deviceId}`; return `${userId}|${deviceId}`;

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {MIN_UNICODE, MAX_UNICODE} from "./common.js"; import {MIN_UNICODE, MAX_UNICODE} from "./common";
function encodeKey(roomId, sessionId, messageIndex) { function encodeKey(roomId, sessionId, messageIndex) {
return `${roomId}|${sessionId}|${messageIndex}`; return `${roomId}|${sessionId}|${messageIndex}`;

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {MIN_UNICODE, MAX_UNICODE} from "./common.js"; import {MIN_UNICODE, MAX_UNICODE} from "./common";
function encodeKey(roomId, senderKey, sessionId) { function encodeKey(roomId, senderKey, sessionId) {
return `${roomId}|${senderKey}|${sessionId}`; return `${roomId}|${senderKey}|${sessionId}`;

View file

@ -0,0 +1,51 @@
/*
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.
*/
import {Store} from "../Store";
import {MemberData} from "./RoomMemberStore";
// TODO: Move to Invite when that's TypeScript.
export interface InviteData {
roomId: string;
isEncrypted: boolean;
isDirectMessage: boolean;
name?: string;
avatarUrl?: string;
avatarColorId: number;
canonicalAlias?: string;
timestamp: number;
joinRule: string;
inviter?: MemberData;
}
export class InviteStore {
private _inviteStore: Store<InviteData>;
constructor(inviteStore: Store<InviteData>) {
this._inviteStore = inviteStore;
}
getAll(): Promise<InviteData[]> {
return this._inviteStore.selectAll();
}
set(invite: InviteData): void {
this._inviteStore.put(invite);
}
remove(roomId: string): void {
this._inviteStore.delete(roomId);
}
}

View file

@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {MIN_UNICODE, MAX_UNICODE} from "./common.js"; import {MIN_UNICODE, MAX_UNICODE} from "./common";
export function encodeScopeTypeKey(scope, type) { export function encodeScopeTypeKey(scope, type) {
return `${scope}|${type}`; return `${scope}|${type}`;

View file

@ -15,44 +15,59 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {MAX_UNICODE} from "./common.js"; import {MAX_UNICODE} from "./common";
import {Store} from "../Store";
function encodeKey(roomId, userId) { function encodeKey(roomId: string, userId: string) {
return `${roomId}|${userId}`; return `${roomId}|${userId}`;
} }
function decodeKey(key) { function decodeKey(key: string): { roomId: string, userId: string } {
const [roomId, userId] = key.split("|"); const [roomId, userId] = key.split("|");
return {roomId, userId}; return {roomId, userId};
} }
// TODO: Move to RoomMember when that's TypeScript.
export interface MemberData {
roomId: string;
userId: string;
avatarUrl: string;
displayName: string;
membership: "join" | "leave" | "invite" | "ban";
}
type MemberStorageEntry = MemberData & { key: string }
// no historical members // no historical members
export class RoomMemberStore { export class RoomMemberStore {
constructor(roomMembersStore) { private _roomMembersStore: Store<MemberStorageEntry>;
constructor(roomMembersStore: Store<MemberStorageEntry>) {
this._roomMembersStore = roomMembersStore; this._roomMembersStore = roomMembersStore;
} }
get(roomId, userId) { get(roomId: string, userId: string): Promise<MemberStorageEntry | null> {
return this._roomMembersStore.get(encodeKey(roomId, userId)); return this._roomMembersStore.get(encodeKey(roomId, userId));
} }
async set(member) { set(member: MemberData): void {
member.key = encodeKey(member.roomId, member.userId); // Object.assign would be more typesafe, but small objects
return this._roomMembersStore.put(member); (member as any).key = encodeKey(member.roomId, member.userId);
this._roomMembersStore.put(member as MemberStorageEntry);
} }
getAll(roomId) { getAll(roomId: string): Promise<MemberData[]> {
const range = this._roomMembersStore.IDBKeyRange.lowerBound(encodeKey(roomId, "")); const range = this._roomMembersStore.IDBKeyRange.lowerBound(encodeKey(roomId, ""));
return this._roomMembersStore.selectWhile(range, member => { return this._roomMembersStore.selectWhile(range, member => {
return member.roomId === roomId; return member.roomId === roomId;
}); });
} }
async getAllUserIds(roomId) { async getAllUserIds(roomId: string): Promise<string[]> {
const userIds = []; const userIds: string[] = [];
const range = this._roomMembersStore.IDBKeyRange.lowerBound(encodeKey(roomId, "")); const range = this._roomMembersStore.IDBKeyRange.lowerBound(encodeKey(roomId, ""));
await this._roomMembersStore.iterateKeys(range, key => { await this._roomMembersStore.iterateKeys(range, key => {
const decodedKey = decodeKey(key); const decodedKey = decodeKey(key as string);
// prevent running into the next room // prevent running into the next room
if (decodedKey.roomId === roomId) { if (decodedKey.roomId === roomId) {
userIds.push(decodedKey.userId); userIds.push(decodedKey.userId);
@ -63,7 +78,7 @@ export class RoomMemberStore {
return userIds; return userIds;
} }
removeAllForRoom(roomId) { removeAllForRoom(roomId: string): void {
// exclude both keys as they are theoretical min and max, // exclude both keys as they are theoretical min and max,
// but we should't have a match for just the room id, or room id with max // but we should't have a match for just the room id, or room id with max
const range = this._roomMembersStore.IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true); const range = this._roomMembersStore.IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true);

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {MAX_UNICODE} from "./common.js"; import {MAX_UNICODE} from "./common";
function encodeKey(roomId, eventType, stateKey) { function encodeKey(roomId, eventType, stateKey) {
return `${roomId}|${eventType}|${stateKey}`; return `${roomId}|${eventType}|${stateKey}`;

View file

@ -27,31 +27,35 @@ store contains:
inviteCount inviteCount
joinCount joinCount
*/ */
import {Store} from "../Store";
import {SummaryData} from "../../../room/RoomSummary";
/** Used for both roomSummary and archivedRoomSummary stores */ /** Used for both roomSummary and archivedRoomSummary stores */
export class RoomSummaryStore { export class RoomSummaryStore {
constructor(summaryStore) { private _summaryStore: Store<SummaryData>;
constructor(summaryStore: Store<SummaryData>) {
this._summaryStore = summaryStore; this._summaryStore = summaryStore;
} }
getAll() { getAll(): Promise<SummaryData[]> {
return this._summaryStore.selectAll(); return this._summaryStore.selectAll();
} }
set(summary) { set(summary: SummaryData): void {
return this._summaryStore.put(summary); this._summaryStore.put(summary);
} }
get(roomId) { get(roomId: string): Promise<SummaryData | null> {
return this._summaryStore.get(roomId); return this._summaryStore.get(roomId);
} }
async has(roomId) { async has(roomId: string): Promise<boolean> {
const fetchedKey = await this._summaryStore.getKey(roomId); const fetchedKey = await this._summaryStore.getKey(roomId);
return roomId === fetchedKey; return roomId === fetchedKey;
} }
remove(roomId) { remove(roomId: string): Promise<undefined> {
return this._summaryStore.delete(roomId); return this._summaryStore.delete(roomId);
} }
} }

View file

@ -13,28 +13,36 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {Store} from "../Store";
export interface SessionEntry {
key: string;
value: any;
}
export class SessionStore { export class SessionStore {
constructor(sessionStore) { private _sessionStore: Store<SessionEntry>
constructor(sessionStore: Store<SessionEntry>) {
this._sessionStore = sessionStore; this._sessionStore = sessionStore;
} }
async get(key) { async get(key: string): Promise<any> {
const entry = await this._sessionStore.get(key); const entry = await this._sessionStore.get(key);
if (entry) { if (entry) {
return entry.value; return entry.value;
} }
} }
set(key, value) { set(key: string, value: any): void {
this._sessionStore.put({key, value}); this._sessionStore.put({key, value});
} }
add(key, value) { add(key: string, value: any): void {
this._sessionStore.add({key, value}); this._sessionStore.add({key, value});
} }
remove(key) { remove(key: string): void {
this._sessionStore.delete(key); this._sessionStore.delete(key);
} }
} }

View file

@ -18,23 +18,50 @@ import {EventKey} from "../../../room/timeline/EventKey.js";
import { StorageError } from "../../common"; import { StorageError } from "../../common";
import { encodeUint32 } from "../utils"; import { encodeUint32 } from "../utils";
import {KeyLimits} from "../../common"; import {KeyLimits} from "../../common";
import {Store} from "../Store";
import {TimelineEvent, 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: TimelineEvent | 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)}`; return `${roomId}|${encodeUint32(fragmentId)}|${encodeUint32(eventIndex)}`;
} }
function encodeEventIdKey(roomId, eventId) { function encodeEventIdKey(roomId: string, eventId: string): string {
return `${roomId}|${eventId}`; return `${roomId}|${eventId}`;
} }
function decodeEventIdKey(eventIdKey) { function decodeEventIdKey(eventIdKey: string): { roomId: string, eventId: string } {
const [roomId, eventId] = eventIdKey.split("|"); const [roomId, eventId] = eventIdKey.split("|");
return {roomId, eventId}; return {roomId, eventId};
} }
class Range { class Range {
constructor(IDBKeyRange, only, lower, upper, lowerOpen, upperOpen) { private _IDBKeyRange: typeof IDBKeyRange;
this._IDBKeyRange = IDBKeyRange; 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._only = only;
this._lower = lower; this._lower = lower;
this._upper = upper; this._upper = upper;
@ -42,7 +69,7 @@ class Range {
this._upperOpen = upperOpen; this._upperOpen = upperOpen;
} }
asIDBKeyRange(roomId) { asIDBKeyRange(roomId: string): IDBKeyRange | undefined {
try { try {
// only // only
if (this._only) { if (this._only) {
@ -99,66 +126,68 @@ class Range {
* @property {?Gap} gap if a gap entry, the gap * @property {?Gap} gap if a gap entry, the gap
*/ */
export class TimelineEventStore { export class TimelineEventStore {
constructor(timelineStore) { private _timelineStore: Store<StorageEntry>;
constructor(timelineStore: Store<StorageEntry>) {
this._timelineStore = timelineStore; this._timelineStore = timelineStore;
} }
/** Creates a range that only includes the given key /** Creates a range that only includes the given key
* @param {EventKey} eventKey the key * @param eventKey the key
* @return {Range} the created range * @return the created range
*/ */
onlyRange(eventKey) { onlyRange(eventKey: EventKey): Range {
return new Range(this._timelineStore.IDBKeyRange, eventKey); return new Range(this._timelineStore.IDBKeyRange, eventKey);
} }
/** Creates a range that includes all keys before eventKey, and optionally also the key itself. /** Creates a range that includes all keys before eventKey, and optionally also the key itself.
* @param {EventKey} eventKey the key * @param eventKey the key
* @param {boolean} [open=false] whether the key is included (false) or excluded (true) from the range at the upper end. * @param [open=false] whether the key is included (false) or excluded (true) from the range at the upper end.
* @return {Range} the created range * @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); 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. /** Creates a range that includes all keys after eventKey, and optionally also the key itself.
* @param {EventKey} eventKey the key * @param eventKey the key
* @param {boolean} [open=false] whether the key is included (false) or excluded (true) from the range at the lower end. * @param [open=false] whether the key is included (false) or excluded (true) from the range at the lower end.
* @return {Range} the created range * @return the created range
*/ */
lowerBoundRange(eventKey, open=false) { lowerBoundRange(eventKey: EventKey, open=false): Range {
return new Range(this._timelineStore.IDBKeyRange, undefined, eventKey, undefined, open); 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. /** 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 lower the lower key
* @param {EventKey} upper the upper key * @param upper the upper key
* @param {boolean} [lowerOpen=false] whether the lower key is included (false) or excluded (true) from the range. * @param [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. * @param [upperOpen=false] whether the upper key is included (false) or excluded (true) from the range.
* @return {Range} the created 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); return new Range(this._timelineStore.IDBKeyRange, undefined, lower, upper, lowerOpen, upperOpen);
} }
/** Looks up the last `amount` entries in the timeline for `roomId`. /** Looks up the last `amount` entries in the timeline for `roomId`.
* @param {string} roomId * @param roomId
* @param {number} fragmentId * @param fragmentId
* @param {number} amount * @param amount
* @return {Promise<Entry[]>} a promise resolving to an array with 0 or more entries, in ascending order. * @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<StorageEntry[]> {
const eventKey = EventKey.maxKey; const eventKey = EventKey.maxKey;
eventKey.fragmentId = fragmentId; eventKey.fragmentId = fragmentId;
return this.eventsBefore(roomId, eventKey, amount); return this.eventsBefore(roomId, eventKey, amount);
} }
/** Looks up the first `amount` entries in the timeline for `roomId`. /** Looks up the first `amount` entries in the timeline for `roomId`.
* @param {string} roomId * @param roomId
* @param {number} fragmentId * @param fragmentId
* @param {number} amount * @param amount
* @return {Promise<Entry[]>} a promise resolving to an array with 0 or more entries, in ascending order. * @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<StorageEntry[]> {
const eventKey = EventKey.minKey; const eventKey = EventKey.minKey;
eventKey.fragmentId = fragmentId; eventKey.fragmentId = fragmentId;
return this.eventsAfter(roomId, eventKey, amount); 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. /** Looks up `amount` entries after `eventKey` in the timeline for `roomId` within the same fragment.
* The entry for `eventKey` is not included. * The entry for `eventKey` is not included.
* @param {string} roomId * @param roomId
* @param {EventKey} eventKey * @param eventKey
* @param {number} amount * @param amount
* @return {Promise<Entry[]>} a promise resolving to an array with 0 or more entries, in ascending order. * @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<StorageEntry[]> {
const idbRange = this.lowerBoundRange(eventKey, true).asIDBKeyRange(roomId); const idbRange = this.lowerBoundRange(eventKey, true).asIDBKeyRange(roomId);
return this._timelineStore.selectLimit(idbRange, amount); return this._timelineStore.selectLimit(idbRange, amount);
} }
/** Looks up `amount` entries before `eventKey` in the timeline for `roomId` within the same fragment. /** Looks up `amount` entries before `eventKey` in the timeline for `roomId` within the same fragment.
* The entry for `eventKey` is not included. * The entry for `eventKey` is not included.
* @param {string} roomId * @param roomId
* @param {EventKey} eventKey * @param eventKey
* @param {number} amount * @param amount
* @return {Promise<Entry[]>} a promise resolving to an array with 0 or more entries, in ascending order. * @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<StorageEntry[]> {
const range = this.upperBoundRange(eventKey, true).asIDBKeyRange(roomId); const range = this.upperBoundRange(eventKey, true).asIDBKeyRange(roomId);
const events = await this._timelineStore.selectLimitReverse(range, amount); const events = await this._timelineStore.selectLimitReverse(range, amount);
events.reverse(); // because we fetched them backwards 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`. * The order in which results are returned might be different than `eventIds`.
* Call the return value to obtain the next {id, event} pair. * Call the return value to obtain the next {id, event} pair.
* @param {string} roomId * @param roomId
* @param {string[]} eventIds * @param eventIds
* @return {Function<Promise>} * @return
*/ */
// performance comment from above refers to the fact that there *might* // performance comment from above refers to the fact that there *might*
// be a correlation between event_id sorting order and chronology. // 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 // 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. // 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. // 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<string | undefined> {
const byEventId = this._timelineStore.index("byEventId"); const byEventId = this._timelineStore.index("byEventId");
const keys = eventIds.map(eventId => encodeEventIdKey(roomId, eventId)); const keys = eventIds.map(eventId => encodeEventIdKey(roomId, eventId));
const results = new Array(keys.length); 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 // 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) { for(let i = 0; i < results.length; ++i) {
if (results[i] === undefined) { if (results[i] === undefined) {
return; return;
@ -222,7 +251,8 @@ export class TimelineEventStore {
} }
await byEventId.findExistingKeys(keys, false, (key, found) => { 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; results[index] = found;
firstFoundKey = firstFoundAndPrecedingResolved(); firstFoundKey = firstFoundAndPrecedingResolved();
return !!firstFoundKey; return !!firstFoundKey;
@ -231,11 +261,11 @@ 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. /** 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 * @param entry the entry to insert
* @return {Promise<>} a promise resolving to undefined if the operation was successful, or a StorageError if not. * @return nothing. To wait for the operation to finish, await the transaction it's part of.
* @throws {StorageError} ... * @throws {StorageError} ...
*/ */
insert(entry) { insert(entry: StorageEntry): void {
entry.key = encodeKey(entry.roomId, entry.fragmentId, entry.eventIndex); entry.key = encodeKey(entry.roomId, entry.fragmentId, entry.eventIndex);
entry.eventIdKey = encodeEventIdKey(entry.roomId, entry.event.event_id); entry.eventIdKey = encodeEventIdKey(entry.roomId, entry.event.event_id);
// TODO: map error? or in idb/store? // TODO: map error? or in idb/store?
@ -244,22 +274,22 @@ export class TimelineEventStore {
/** Updates the entry into the store with the given [roomId, eventKey] combination. /** Updates the entry into the store with the given [roomId, eventKey] combination.
* If not yet present, will insert. Might be slower than add. * If not yet present, will insert. Might be slower than add.
* @param {Entry} entry the entry to update. * @param entry the entry to update.
* @return {Promise<>} a promise resolving to undefined if the operation was successful, or a StorageError if not. * @return nothing. To wait for the operation to finish, await the transaction it's part of.
*/ */
update(entry) { update(entry: StorageEntry): void {
this._timelineStore.put(entry); this._timelineStore.put(entry);
} }
get(roomId, eventKey) { get(roomId: string, eventKey: EventKey): Promise<StorageEntry | null> {
return this._timelineStore.get(encodeKey(roomId, eventKey.fragmentId, eventKey.eventIndex)); return this._timelineStore.get(encodeKey(roomId, eventKey.fragmentId, eventKey.eventIndex));
} }
getByEventId(roomId, eventId) { getByEventId(roomId: string, eventId: string): Promise<StorageEntry | null> {
return this._timelineStore.index("byEventId").get(encodeEventIdKey(roomId, eventId)); return this._timelineStore.index("byEventId").get(encodeEventIdKey(roomId, eventId));
} }
removeAllForRoom(roomId) { removeAllForRoom(roomId: string): void {
const minKey = encodeKey(roomId, KeyLimits.minStorageKey, KeyLimits.minStorageKey); const minKey = encodeKey(roomId, KeyLimits.minStorageKey, KeyLimits.minStorageKey);
const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey, KeyLimits.maxStorageKey); const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey, KeyLimits.maxStorageKey);
const range = this._timelineStore.IDBKeyRange.bound(minKey, maxKey); const range = this._timelineStore.IDBKeyRange.bound(minKey, maxKey);

View file

@ -13,31 +13,41 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {MIN_UNICODE, MAX_UNICODE} from "./common.js"; import {MIN_UNICODE, MAX_UNICODE} from "./common";
import {Store} from "../Store";
function encodeKey(roomId, targetEventId, relType, sourceEventId) { function encodeKey(roomId: string, targetEventId: string, relType: string, sourceEventId: string): string {
return `${roomId}|${targetEventId}|${relType}|${sourceEventId}`; return `${roomId}|${targetEventId}|${relType}|${sourceEventId}`;
} }
function decodeKey(key) { interface RelationEntry {
roomId: string;
targetEventId: string;
sourceEventId: string;
relType: string;
}
function decodeKey(key: string): RelationEntry {
const [roomId, targetEventId, relType, sourceEventId] = key.split("|"); const [roomId, targetEventId, relType, sourceEventId] = key.split("|");
return {roomId, targetEventId, relType, sourceEventId}; return {roomId, targetEventId, relType, sourceEventId};
} }
export class TimelineRelationStore { export class TimelineRelationStore {
constructor(store) { private _store: Store<{ key: string }>;
constructor(store: Store<{ key: string }>) {
this._store = store; this._store = store;
} }
add(roomId, targetEventId, relType, sourceEventId) { add(roomId: string, targetEventId: string, relType: string, sourceEventId: string): void {
return this._store.add({key: encodeKey(roomId, targetEventId, relType, sourceEventId)}); this._store.add({key: encodeKey(roomId, targetEventId, relType, sourceEventId)});
} }
remove(roomId, targetEventId, relType, sourceEventId) { remove(roomId: string, targetEventId: string, relType: string, sourceEventId: string): Promise<undefined> {
return this._store.delete(encodeKey(roomId, targetEventId, relType, sourceEventId)); return this._store.delete(encodeKey(roomId, targetEventId, relType, sourceEventId));
} }
removeAllForTarget(roomId, targetId) { removeAllForTarget(roomId: string, targetId: string): Promise<undefined> {
const range = this._store.IDBKeyRange.bound( const range = this._store.IDBKeyRange.bound(
encodeKey(roomId, targetId, MIN_UNICODE, MIN_UNICODE), encodeKey(roomId, targetId, MIN_UNICODE, MIN_UNICODE),
encodeKey(roomId, targetId, MAX_UNICODE, MAX_UNICODE), encodeKey(roomId, targetId, MAX_UNICODE, MAX_UNICODE),
@ -47,7 +57,7 @@ export class TimelineRelationStore {
return this._store.delete(range); return this._store.delete(range);
} }
async getForTargetAndType(roomId, targetId, relType) { async getForTargetAndType(roomId: string, targetId: string, relType: string): Promise<RelationEntry[]> {
// exclude both keys as they are theoretical min and max, // exclude both keys as they are theoretical min and max,
// but we should't have a match for just the room id, or room id with max // but we should't have a match for just the room id, or room id with max
const range = this._store.IDBKeyRange.bound( const range = this._store.IDBKeyRange.bound(
@ -60,7 +70,7 @@ export class TimelineRelationStore {
return items.map(i => decodeKey(i.key)); return items.map(i => decodeKey(i.key));
} }
async getAllForTarget(roomId, targetId) { async getAllForTarget(roomId: string, targetId: string): Promise<RelationEntry[]> {
// exclude both keys as they are theoretical min and max, // exclude both keys as they are theoretical min and max,
// but we should't have a match for just the room id, or room id with max // but we should't have a match for just the room id, or room id with max
const range = this._store.IDBKeyRange.bound( const range = this._store.IDBKeyRange.bound(

View file

@ -14,20 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
export class InviteStore { export type Content = { [key: string]: any }
constructor(inviteStore) {
this._inviteStore = inviteStore; export interface TimelineEvent {
content: Content;
type: string;
event_id: string;
sender: string;
origin_server_ts: number;
unsigned?: Content;
} }
getAll() { export type StateEvent = TimelineEvent & { prev_content?: Content, state_key: string }
return this._inviteStore.selectAll();
}
set(invite) {
return this._inviteStore.put(invite);
}
remove(roomId) {
this._inviteStore.delete(roomId);
}
}