From d0c1ddb51b41c23d234bdb0af7a4a4cf527ef48f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 26 Aug 2021 15:16:51 +0200 Subject: [PATCH 01/23] add failing test --- src/matrix/room/timeline/persistence/MemberWriter.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/matrix/room/timeline/persistence/MemberWriter.js b/src/matrix/room/timeline/persistence/MemberWriter.js index db649f68..3d35ff47 100644 --- a/src/matrix/room/timeline/persistence/MemberWriter.js +++ b/src/matrix/room/timeline/persistence/MemberWriter.js @@ -243,6 +243,17 @@ export function tests() { assert.equal(change.member.membership, "join"); assert.equal(txn.members.get(alice).displayName, "Alice"); }, + "new join through both timeline and state": async assert => { + const writer = new MemberWriter(roomId); + const txn = createStorage(); + const aliceJoin = createMemberEvent("join", alice, "Alice"); + const change = await writer.writeStateMemberEvent(aliceJoin, false, txn); + assert(!change.hasJoined); + assert(!change.hasLeft); + const timelineChange = await writer.writeTimelineMemberEvent(aliceJoin, txn); + assert(change.hasJoined); + assert(!change.hasLeft); + }, "newly joined member causes a change with lookup done first": async assert => { const event = createMemberEvent("join", alice, "Alice"); const writer = new MemberWriter(roomId); From 826de7e9cb13ff5b9d50e9266892784875d7dea3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Aug 2021 11:49:40 +0200 Subject: [PATCH 02/23] Write all members of a sync in one go so state member events written but not causing a memberChange.hasJoined don't prevent timeline member events for the same user from doing so --- src/matrix/room/Room.js | 4 +- src/matrix/room/members/RoomMember.js | 10 + .../room/timeline/persistence/MemberWriter.js | 363 +++++++++++++----- .../room/timeline/persistence/SyncWriter.js | 117 +++--- 4 files changed, 332 insertions(+), 162 deletions(-) diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index f5c42097..cab0e13b 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -30,6 +30,7 @@ const EVENT_ENCRYPTED_TYPE = "m.room.encrypted"; export class Room extends BaseRoom { constructor(options) { super(options); + // TODO: pass pendingEvents to start like pendingOperations? const {pendingEvents} = options; const relationWriter = new RelationWriter({ roomId: this.id, @@ -120,7 +121,8 @@ export class Room extends BaseRoom { txn.roomMembers.removeAllForRoom(this.id); } const {entries: newEntries, updatedEntries, newLiveKey, memberChanges} = - await log.wrap("syncWriter", log => this._syncWriter.writeSync(roomResponse, isRejoin, txn, log), log.level.Detail); + await log.wrap("syncWriter", log => this._syncWriter.writeSync( + roomResponse, isRejoin, summaryChanges.hasFetchedMembers, txn, log), log.level.Detail); if (decryptChanges) { const decryption = await log.wrap("decryptChanges", log => decryptChanges.write(txn, log)); log.set("decryptionResults", decryption.results.size); diff --git a/src/matrix/room/members/RoomMember.js b/src/matrix/room/members/RoomMember.js index f810a9fe..78096060 100644 --- a/src/matrix/room/members/RoomMember.js +++ b/src/matrix/room/members/RoomMember.js @@ -141,6 +141,16 @@ export class MemberChange { return this.previousMembership === "join" && this.membership !== "join"; } + /** The result can be a false negative when all of these apply: + * - the complete set of room members hasn't been fetched yet. + * - the member event for this change was received in the + * state section and wasn't present in the timeline section. + * - the room response was limited, e.g. there was a gap. + * + * This is because during sync, in this case it is not possible + * to distinguish between a new member that joined the room + * during a gap and a lazy-loading member. + * */ get hasJoined() { return this.previousMembership !== "join" && this.membership === "join"; } diff --git a/src/matrix/room/timeline/persistence/MemberWriter.js b/src/matrix/room/timeline/persistence/MemberWriter.js index 3d35ff47..0c24b06f 100644 --- a/src/matrix/room/timeline/persistence/MemberWriter.js +++ b/src/matrix/room/timeline/persistence/MemberWriter.js @@ -23,57 +23,27 @@ export class MemberWriter { this._cache = new LRUCache(5, member => member.userId); } - writeTimelineMemberEvent(event, txn) { - return this._writeMemberEvent(event, false, txn); + prepareMemberSync(stateEvents, timelineEvents, hasFetchedMembers) { + return new MemberSync(this, stateEvents, timelineEvents, hasFetchedMembers); } - writeStateMemberEvent(event, isLimited, txn) { - // member events in the state section when the room response - // is not limited must always be lazy loaded members. - // If they are not, they will be repeated in the timeline anyway. - return this._writeMemberEvent(event, !isLimited, txn); - } - - async _writeMemberEvent(event, isLazyLoadingMember, txn) { - const userId = event.state_key; - if (!userId) { - return; - } - const member = RoomMember.fromMemberEvent(this._roomId, event); - if (!member) { - return; - } - - let existingMember = this._cache.get(userId); + async _writeMember(member, txn) { + let existingMember = this._cache.get(member.userId); if (!existingMember) { - const memberData = await txn.roomMembers.get(this._roomId, userId); + const memberData = await txn.roomMembers.get(this._roomId, member.userId); if (memberData) { existingMember = new RoomMember(memberData); } } - // either never heard of the member, or something changed if (!existingMember || !existingMember.equals(member)) { txn.roomMembers.set(member.serialize()); this._cache.set(member); - // we also return a member change for lazy loading members if something changed, - // so when the dupe timeline event comes and it doesn't see a diff - // with the cache, we already returned the event here. - // - // it's just important that we don't consider the first LL event - // for a user we see as a membership change, or we'll share keys with - // them, etc... - if (isLazyLoadingMember && !existingMember) { - // we don't have a previous member, but we know this is not a - // membership change as it's a lazy loaded - // member so take the membership from the member - return new MemberChange(member, member.membership); - } return new MemberChange(member, existingMember?.membership); } } - async lookupMember(userId, event, timelineEvents, txn) { + async lookupMember(userId, txn) { let member = this._cache.get(userId); if (!member) { const memberData = await txn.roomMembers.get(this._roomId, userId); @@ -82,60 +52,170 @@ export class MemberWriter { this._cache.set(member); } } - if (!member) { - // sometimes the member event isn't included in state, but rather in the timeline, - // even if it is not the first event in the timeline. In this case, go look for - // the last one before the event, or if none is found, - // the least recent matching member event in the timeline. - // The latter is needed because of new joins picking up their own display name - let foundEvent = false; - let memberEventBefore; - let firstMemberEvent; - for (let i = timelineEvents.length - 1; i >= 0; i -= 1) { - const e = timelineEvents[i]; - let matchingEvent; - if (e.type === MEMBER_EVENT_TYPE && e.state_key === userId) { - matchingEvent = e; - firstMemberEvent = matchingEvent; + return member; + } +} + +class MemberSync { + constructor(memberWriter, stateEvents, timelineEvents, hasFetchedMembers) { + this._memberWriter = memberWriter; + this._timelineEvents = timelineEvents; + this._hasFetchedMembers = hasFetchedMembers; + this._newStateMembers = stateEvents && this._stateEventsToMembers(stateEvents); + } + + get _roomId() { + return this._memberWriter._roomId; + } + + _stateEventsToMembers(stateEvents) { + let members; + for (const event of stateEvents) { + if (event.type === MEMBER_EVENT_TYPE) { + const member = RoomMember.fromMemberEvent(this._roomId, event); + if (member) { + if (!members) { + members = new Map(); + } + members.set(member.userId, member); + } + } + } + return members; + } + + _timelineEventsToMembers() { + let members; + // iterate backwards to only add the last member in the timeline + for (let i = this._timelineEvents.length - 1; i >= 0; i--) { + const e = this._timelineEvents[i]; + const userId = e.state_key; + if (e.type === MEMBER_EVENT_TYPE && !members?.has(userId)) { + const member = RoomMember.fromMemberEvent(this._roomId, e); + if (member) { + if (!members) { + members = new Map(); + } + members.set(member.userId, member); + } + } + } + return members; + } + + async lookupMemberAtEvent(userId, event, txn) { + let member = this._findPrecedingMemberEventInTimeline(userId, event); + if (member) { + return member; + } + member = this._newStateMembers?.get(userId); + if (member) { + return member; + } + return await this._memberWriter.lookupMember(userId, txn); + } + + async write(txn) { + const memberChanges = new Map(); + const newTimelineMembers = this._timelineEventsToMembers(); + if (this._newStateMembers) { + for (const member of this._newStateMembers.values()) { + if (!newTimelineMembers?.has(member.userId)) { + const memberChange = await this._memberWriter._writeMember(member, txn); + if (memberChange) { + // if the member event appeared only in the state section, + // AND we haven't heard about it AND we haven't fetched all members yet (to avoid #470), + // this may be a lazy loading member (if it's not in a gap, we are certain + // it is a ll member, in a gap, we can't tell), so we pass in our own membership as + // as the previous one so we won't consider it a join to not have false positives (to avoid #192). + // see also MemberChange.hasJoined + const maybeLazyLoadingMember = !this._hasFetchedMembers && !memberChange.previousMembership; + if (maybeLazyLoadingMember) { + memberChange.previousMembership = member.membership; + } + memberChanges.set(memberChange.userId, memberChange); + } + } + } + } + if (newTimelineMembers) { + for (const member of newTimelineMembers.values()) { + const memberChange = await this._memberWriter._writeMember(member, txn); + if (memberChange) { + memberChanges.set(memberChange.userId, memberChange); + } + } + } + return memberChanges; + } + + // try to find the first member event before the given event, + // so we respect historical display names within the chunk of timeline + _findPrecedingMemberEventInTimeline(userId, event) { + let eventIndex = -1; + for (let i = this._timelineEvents.length - 1; i >= 0; i--) { + const e = this._timelineEvents[i]; + if (e.event_id === event.event_id) { + eventIndex = i; + break; + } + } + for (let i = eventIndex - 1; i >= 0; i--) { + const e = this._timelineEvents[i]; + if (e.type === MEMBER_EVENT_TYPE && e.state_key === userId) { + const member = RoomMember.fromMemberEvent(this._roomId, e); + if (member) { + return member; } - if (!foundEvent) { - if (e.event_id === event.event_id) { - foundEvent = true; - } - } else if (matchingEvent) { - memberEventBefore = matchingEvent; - break; - } - } - // first see if we found a member event before the event we're looking up the sender for - if (memberEventBefore) { - member = RoomMember.fromMemberEvent(this._roomId, memberEventBefore); - } - // and only if we didn't, fall back to the first member event, - // regardless of where it is positioned relative to the lookup event - else if (firstMemberEvent) { - member = RoomMember.fromMemberEvent(this._roomId, firstMemberEvent); } } - return member; } } export function tests() { + let idCounter = 0; + function createMemberEvent(membership, userId, displayName, avatarUrl) { + idCounter += 1; return { content: { membership, "displayname": displayName, "avatar_url": avatarUrl }, + event_id: `$${idCounter}`, sender: userId, "state_key": userId, type: "m.room.member" }; } + function createRoomResponse(stateEvents, timelineEvents) { + if (!Array.isArray(timelineEvents)) { + timelineEvents = [timelineEvents]; + } + if (!Array.isArray(stateEvents)) { + stateEvents = [stateEvents]; + } + return { + timeline: { + limited: false, + events: timelineEvents, + }, + state: { + events: stateEvents + } + }; + } + + function createTimelineResponse(timelineEvents) { + return createRoomResponse([], timelineEvents) + } + + function createStateReponse(stateEvents) { + return createRoomResponse(stateEvents, []); + } function createStorage(initialMembers = []) { const members = new Map(); @@ -163,81 +243,126 @@ export function tests() { const alice = "@alice:hs.tld"; const avatar = "mxc://hs.tld/def"; +/* + join without previous membership + join during gap with hasFetchedMembers=false + join during gap with hasFetchedMembers=true + join after invite + +*/ + return { - "new join through state": async assert => { + "new join": async assert => { const writer = new MemberWriter(roomId); const txn = createStorage(); - const change = await writer.writeStateMemberEvent(createMemberEvent("join", alice), true, txn); + const memberSync = writer.prepareMemberSync([], [createMemberEvent("join", alice)], false); + const changes = await memberSync.write(txn); + assert.equal(changes.size, 1); + const change = changes.get(alice); assert(change.hasJoined); assert.equal(txn.members.get(alice).membership, "join"); }, - "accept invite through state": async assert => { + "accept invite": async assert => { const writer = new MemberWriter(roomId); const txn = createStorage([member("invite", alice)]); - const change = await writer.writeStateMemberEvent(createMemberEvent("join", alice), true, txn); + const memberSync = writer.prepareMemberSync([], [createMemberEvent("join", alice)], false); + const changes = await memberSync.write(txn); + assert.equal(changes.size, 1); + const change = changes.get(alice); assert.equal(change.previousMembership, "invite"); assert(change.hasJoined); assert.equal(txn.members.get(alice).membership, "join"); }, - "change display name through timeline": async assert => { + "change display name": async assert => { const writer = new MemberWriter(roomId); const txn = createStorage([member("join", alice, "Alice")]); - const change = await writer.writeTimelineMemberEvent(createMemberEvent("join", alice, "Alies"), txn); + const memberSync = writer.prepareMemberSync([], [createMemberEvent("join", alice, "Alies")], false); + const changes = await memberSync.write(txn); + assert.equal(changes.size, 1); + const change = changes.get(alice); assert(!change.hasJoined); assert.equal(change.member.displayName, "Alies"); assert.equal(txn.members.get(alice).displayName, "Alies"); }, - "set avatar through timeline": async assert => { + "set avatar": async assert => { const writer = new MemberWriter(roomId); const txn = createStorage([member("join", alice, "Alice")]); - const change = await writer.writeTimelineMemberEvent(createMemberEvent("join", alice, "Alice", avatar), txn); + const memberSync = writer.prepareMemberSync([], [createMemberEvent("join", alice, "Alice", avatar)], false); + const changes = await memberSync.write(txn); + assert.equal(changes.size, 1); + const change = changes.get(alice); assert(!change.hasJoined); assert.equal(change.member.avatarUrl, avatar); assert.equal(txn.members.get(alice).avatarUrl, avatar); }, - "ignore redundant member event": async assert => { + "ignore redundant member event in timeline": async assert => { const writer = new MemberWriter(roomId); const txn = createStorage([member("join", alice, "Alice", avatar)]); - const change = await writer.writeTimelineMemberEvent(createMemberEvent("join", alice, "Alice", avatar), txn); - assert(!change); + const memberSync = writer.prepareMemberSync([], [createMemberEvent("join", alice, "Alice", avatar)], false); + const changes = await memberSync.write(txn); + assert.equal(changes.size, 0); + }, + "ignore redundant member event in state": async assert => { + const writer = new MemberWriter(roomId); + const txn = createStorage([member("join", alice, "Alice", avatar)]); + const memberSync = writer.prepareMemberSync([createMemberEvent("join", alice, "Alice", avatar)], [], false); + const changes = await memberSync.write(txn); + assert.equal(changes.size, 0); }, "leave": async assert => { const writer = new MemberWriter(roomId); const txn = createStorage([member("join", alice, "Alice")]); - const change = await writer.writeTimelineMemberEvent(createMemberEvent("leave", alice, "Alice"), txn); + const memberSync = writer.prepareMemberSync([], [createMemberEvent("leave", alice, "Alice")], false); + const changes = await memberSync.write(txn); + assert.equal(changes.size, 1); + const change = changes.get(alice); assert(change.hasLeft); assert(!change.hasJoined); }, "ban": async assert => { const writer = new MemberWriter(roomId); const txn = createStorage([member("join", alice, "Alice")]); - const change = await writer.writeTimelineMemberEvent(createMemberEvent("ban", alice, "Alice"), txn); + const memberSync = writer.prepareMemberSync([], [createMemberEvent("ban", alice, "Alice")], false); + const changes = await memberSync.write(txn); + assert.equal(changes.size, 1); + const change = changes.get(alice); assert(change.hasLeft); assert(!change.hasJoined); }, "reject invite": async assert => { const writer = new MemberWriter(roomId); const txn = createStorage([member("invite", alice, "Alice")]); - const change = await writer.writeTimelineMemberEvent(createMemberEvent("leave", alice, "Alice"), txn); + const memberSync = writer.prepareMemberSync([], [createMemberEvent("leave", alice, "Alice")], false); + const changes = await memberSync.write(txn); + assert.equal(changes.size, 1); + const change = changes.get(alice); assert(!change.hasLeft); assert(!change.hasJoined); }, "lazy loaded member we already know about doens't return change": async assert => { const writer = new MemberWriter(roomId); const txn = createStorage([member("join", alice, "Alice")]); - const change = await writer.writeStateMemberEvent(createMemberEvent("join", alice, "Alice"), false, txn); - assert(!change); + const memberSync = writer.prepareMemberSync([createMemberEvent("join", alice, "Alice")], [], false); + const changes = await memberSync.write(txn); + assert.equal(changes.size, 0); }, "lazy loaded member we already know about changes display name": async assert => { const writer = new MemberWriter(roomId); const txn = createStorage([member("join", alice, "Alice")]); - const change = await writer.writeStateMemberEvent(createMemberEvent("join", alice, "Alies"), false, txn); + const memberSync = writer.prepareMemberSync([createMemberEvent("join", alice, "Alies")], [], false); + const changes = await memberSync.write(txn); + assert.equal(changes.size, 1); + const change = changes.get(alice); + assert(!change.hasJoined); assert.equal(change.member.displayName, "Alies"); }, - "unknown lazy loaded member returns change, but not considered a membership change": async assert => { + "unknown lazy loaded member returns change, but not considered a join": async assert => { const writer = new MemberWriter(roomId); const txn = createStorage(); - const change = await writer.writeStateMemberEvent(createMemberEvent("join", alice, "Alice"), false, txn); + const memberSync = writer.prepareMemberSync([createMemberEvent("join", alice, "Alice")], [], false); + const changes = await memberSync.write(txn); + assert.equal(changes.size, 1); + const change = changes.get(alice); assert(!change.hasJoined); assert(!change.hasLeft); assert.equal(change.member.membership, "join"); @@ -247,30 +372,68 @@ export function tests() { const writer = new MemberWriter(roomId); const txn = createStorage(); const aliceJoin = createMemberEvent("join", alice, "Alice"); - const change = await writer.writeStateMemberEvent(aliceJoin, false, txn); - assert(!change.hasJoined); - assert(!change.hasLeft); - const timelineChange = await writer.writeTimelineMemberEvent(aliceJoin, txn); + const memberSync = writer.prepareMemberSync([aliceJoin], [aliceJoin], false); + const changes = await memberSync.write(txn); + assert.equal(changes.size, 1); + const change = changes.get(alice); assert(change.hasJoined); assert(!change.hasLeft); }, - "newly joined member causes a change with lookup done first": async assert => { - const event = createMemberEvent("join", alice, "Alice"); + "change display name in timeline with lazy loaded member in state": async assert => { const writer = new MemberWriter(roomId); const txn = createStorage(); - const member = await writer.lookupMember(event.sender, event, [event], txn); - assert(member); - const change = await writer.writeTimelineMemberEvent(event, txn); - assert(change); + const memberSync = writer.prepareMemberSync( + [createMemberEvent("join", alice, "Alice")], + [createMemberEvent("join", alice, "Alies")], + false); + const changes = await memberSync.write(txn); + assert.equal(changes.size, 1); + const change = changes.get(alice); + assert(change.hasJoined); + assert(!change.hasLeft); + assert.equal(change.member.displayName, "Alies"); }, - "lookupMember returns closest member in the past": async assert => { + "lookupMemberAtEvent returns closest member in the past": async assert => { const event1 = createMemberEvent("join", alice, "Alice"); const event2 = createMemberEvent("join", alice, "Alies"); const event3 = createMemberEvent("join", alice, "Alys"); + const events = [event1, event2, event3]; + // we write first because the MemberWriter assumes it is called before + // the SyncWriter does any lookups const writer = new MemberWriter(roomId); const txn = createStorage(); - const member = await writer.lookupMember(event3.sender, event3, [event1, event2, event3], txn); + const memberSync = await writer.prepareMemberSync([], events, false); + let member = await memberSync.lookupMemberAtEvent(event1.sender, event1, txn); + assert.equal(member, undefined); + member = await memberSync.lookupMemberAtEvent(event2.sender, event2, txn); + assert.equal(member.displayName, "Alice"); + member = await memberSync.lookupMemberAtEvent(event3.sender, event3, txn); assert.equal(member.displayName, "Alies"); + + assert.equal(txn.members.size, 0); + const changes = await memberSync.write(txn); + assert.equal(txn.members.size, 1); + assert.equal(changes.size, 1); + const change = changes.get(alice); + assert(change.hasJoined); + }, + "lookupMemberAtEvent falls back on state event": async assert => { + const event1 = createMemberEvent("join", alice, "Alice"); + const event2 = createMemberEvent("join", alice, "Alies"); + // we write first because the MemberWriter assumes it is called before + // the SyncWriter does any lookups + const writer = new MemberWriter(roomId); + const txn = createStorage(); + const memberSync = await writer.prepareMemberSync([event1], [event2], false); + const member = await memberSync.lookupMemberAtEvent(event2.sender, event2, txn); + assert.equal(member.displayName, "Alice"); + + assert.equal(txn.members.size, 0); + const changes = await memberSync.write(txn); + assert.equal(txn.members.size, 1); + assert.equal(changes.size, 1); + const change = changes.get(alice); + assert(change.hasJoined); }, }; } diff --git a/src/matrix/room/timeline/persistence/SyncWriter.js b/src/matrix/room/timeline/persistence/SyncWriter.js index 96551056..b0970ca0 100644 --- a/src/matrix/room/timeline/persistence/SyncWriter.js +++ b/src/matrix/room/timeline/persistence/SyncWriter.js @@ -133,66 +133,51 @@ export class SyncWriter { return currentKey; } - async _writeStateEvents(roomResponse, memberChanges, isLimited, txn, log) { - // persist state - const {state} = roomResponse; - if (Array.isArray(state?.events)) { - log.set("stateEvents", state.events.length); - for (const event of state.events) { - if (event.type === MEMBER_EVENT_TYPE) { - const memberChange = await this._memberWriter.writeStateMemberEvent(event, isLimited, txn); - if (memberChange) { - memberChanges.set(memberChange.userId, memberChange); - } - } else { - txn.roomState.set(this._roomId, event); - } + async _writeStateEvents(stateEvents, txn, log) { + let nonMemberStateEvents = 0; + for (const event of stateEvents) { + // member events are written prior by MemberWriter + if (event.type !== MEMBER_EVENT_TYPE) { + txn.roomState.set(this._roomId, event); + nonMemberStateEvents += 1; } } + log.set("stateEvents", nonMemberStateEvents); } - async _writeTimeline(timeline, currentKey, memberChanges, txn, log) { + async _writeTimeline(timelineEvents, timeline, memberSync, currentKey, txn, log) { const entries = []; const updatedEntries = []; - if (Array.isArray(timeline?.events) && timeline.events.length) { - // only create a fragment when we will really write an event - currentKey = await this._ensureLiveFragment(currentKey, entries, timeline, txn, log); - const events = deduplicateEvents(timeline.events); - log.set("timelineEvents", events.length); - let timelineStateEventCount = 0; - for(const event of events) { - // store event in timeline - currentKey = currentKey.nextKey(); - const storageEntry = createEventEntry(currentKey, this._roomId, event); - let member = await this._memberWriter.lookupMember(event.sender, event, events, txn); - if (member) { - storageEntry.displayName = member.displayName; - storageEntry.avatarUrl = member.avatarUrl; - } - txn.timelineEvents.insert(storageEntry); - const entry = new EventEntry(storageEntry, this._fragmentIdComparer); - entries.push(entry); - const updatedRelationTargetEntries = await this._relationWriter.writeRelation(entry, txn, log); - if (updatedRelationTargetEntries) { - updatedEntries.push(...updatedRelationTargetEntries); - } - // update state events after writing event, so for a member event, - // we only update the member info after having written the member event - // to the timeline, as we want that event to have the old profile info - if (typeof event.state_key === "string") { - timelineStateEventCount += 1; - if (event.type === MEMBER_EVENT_TYPE) { - const memberChange = await this._memberWriter.writeTimelineMemberEvent(event, txn); - if (memberChange) { - memberChanges.set(memberChange.userId, memberChange); - } - } else { - txn.roomState.set(this._roomId, event); - } - } + // only create a fragment when we will really write an event + currentKey = await this._ensureLiveFragment(currentKey, entries, timeline, txn, log); + log.set("timelineEvents", timelineEvents.length); + let timelineStateEventCount = 0; + for(const event of timelineEvents) { + // store event in timeline + currentKey = currentKey.nextKey(); + const storageEntry = createEventEntry(currentKey, this._roomId, event); + let member = await memberSync.lookupMemberAtEvent(event.sender, event, txn); + if (member) { + storageEntry.displayName = member.displayName; + storageEntry.avatarUrl = member.avatarUrl; + } + txn.timelineEvents.insert(storageEntry); + const entry = new EventEntry(storageEntry, this._fragmentIdComparer); + entries.push(entry); + const updatedRelationTargetEntries = await this._relationWriter.writeRelation(entry, txn, log); + if (updatedRelationTargetEntries) { + updatedEntries.push(...updatedRelationTargetEntries); + } + // update state events after writing event, so for a member event, + // we only update the member info after having written the member event + // to the timeline, as we want that event to have the old profile info. + // member events are written prior by MemberWriter. + if (typeof event.state_key === "string" && event.type !== MEMBER_EVENT_TYPE) { + timelineStateEventCount += 1; + txn.roomState.set(this._roomId, event); } - log.set("timelineStateEventCount", timelineStateEventCount); } + log.set("timelineStateEventCount", timelineStateEventCount); return {currentKey, entries, updatedEntries}; } @@ -224,14 +209,13 @@ export class SyncWriter { * @type {SyncWriterResult} * @property {Array} entries new timeline entries written * @property {EventKey} newLiveKey the advanced key to write events at - * @property {Map} memberChanges member changes in the processed sync ny user id * * @param {Object} roomResponse [description] * @param {boolean} isRejoin whether the room was rejoined in the sync being processed * @param {Transaction} txn * @return {SyncWriterResult} */ - async writeSync(roomResponse, isRejoin, txn, log) { + async writeSync(roomResponse, isRejoin, hasFetchedMembers, txn, log) { let {timeline} = roomResponse; // we have rejoined the room after having synced it before, // check for overlap with the last synced event @@ -239,13 +223,24 @@ export class SyncWriter { if (isRejoin) { timeline = await this._handleRejoinOverlap(timeline, txn, log); } - const memberChanges = new Map(); - // important this happens before _writeTimeline so - // members are available in the transaction - await this._writeStateEvents(roomResponse, memberChanges, timeline?.limited, txn, log); - const {currentKey, entries, updatedEntries} = - await this._writeTimeline(timeline, this._lastLiveKey, memberChanges, txn, log); - log.set("memberChanges", memberChanges.size); + let timelineEvents; + if (Array.isArray(timeline?.events)) { + timelineEvents = deduplicateEvents(timeline.events); + } + const {state} = roomResponse; + let stateEvents; + if (Array.isArray(state?.events)) { + stateEvents = state.events; + } + const memberSync = this._memberWriter.prepareMemberSync(stateEvents, timelineEvents, hasFetchedMembers); + if (stateEvents) { + await this._writeStateEvents(roomResponse, txn, log); + } + if (timelineEvents?.length) { + const {currentKey, entries, updatedEntries} = + await this._writeTimeline(timelineEvents, timeline, memberSync, this._lastLiveKey, txn, log); + } + const memberChanges = await memberSync.write(txn); return {entries, updatedEntries, newLiveKey: currentKey, memberChanges}; } From a61f052fe86c7e4f2ca7175d81968213f1a281dd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Aug 2021 17:23:32 +0200 Subject: [PATCH 03/23] fix lint --- .../room/timeline/persistence/MemberWriter.js | 26 -------- .../room/timeline/persistence/SyncWriter.js | 62 +++++++++---------- 2 files changed, 31 insertions(+), 57 deletions(-) diff --git a/src/matrix/room/timeline/persistence/MemberWriter.js b/src/matrix/room/timeline/persistence/MemberWriter.js index 0c24b06f..fba43ddf 100644 --- a/src/matrix/room/timeline/persistence/MemberWriter.js +++ b/src/matrix/room/timeline/persistence/MemberWriter.js @@ -191,32 +191,6 @@ export function tests() { }; } - function createRoomResponse(stateEvents, timelineEvents) { - if (!Array.isArray(timelineEvents)) { - timelineEvents = [timelineEvents]; - } - if (!Array.isArray(stateEvents)) { - stateEvents = [stateEvents]; - } - return { - timeline: { - limited: false, - events: timelineEvents, - }, - state: { - events: stateEvents - } - }; - } - - function createTimelineResponse(timelineEvents) { - return createRoomResponse([], timelineEvents) - } - - function createStateReponse(stateEvents) { - return createRoomResponse(stateEvents, []); - } - function createStorage(initialMembers = []) { const members = new Map(); for (const m of initialMembers) { diff --git a/src/matrix/room/timeline/persistence/SyncWriter.js b/src/matrix/room/timeline/persistence/SyncWriter.js index b0970ca0..f6c167e8 100644 --- a/src/matrix/room/timeline/persistence/SyncWriter.js +++ b/src/matrix/room/timeline/persistence/SyncWriter.js @@ -149,35 +149,37 @@ export class SyncWriter { const entries = []; const updatedEntries = []; // only create a fragment when we will really write an event - currentKey = await this._ensureLiveFragment(currentKey, entries, timeline, txn, log); - log.set("timelineEvents", timelineEvents.length); - let timelineStateEventCount = 0; - for(const event of timelineEvents) { - // store event in timeline - currentKey = currentKey.nextKey(); - const storageEntry = createEventEntry(currentKey, this._roomId, event); - let member = await memberSync.lookupMemberAtEvent(event.sender, event, txn); - if (member) { - storageEntry.displayName = member.displayName; - storageEntry.avatarUrl = member.avatarUrl; - } - txn.timelineEvents.insert(storageEntry); - const entry = new EventEntry(storageEntry, this._fragmentIdComparer); - entries.push(entry); - const updatedRelationTargetEntries = await this._relationWriter.writeRelation(entry, txn, log); - if (updatedRelationTargetEntries) { - updatedEntries.push(...updatedRelationTargetEntries); - } - // update state events after writing event, so for a member event, - // we only update the member info after having written the member event - // to the timeline, as we want that event to have the old profile info. - // member events are written prior by MemberWriter. - if (typeof event.state_key === "string" && event.type !== MEMBER_EVENT_TYPE) { - timelineStateEventCount += 1; - txn.roomState.set(this._roomId, event); + if (timelineEvents?.length) { + currentKey = await this._ensureLiveFragment(currentKey, entries, timeline, txn, log); + log.set("timelineEvents", timelineEvents.length); + let timelineStateEventCount = 0; + for(const event of timelineEvents) { + // store event in timeline + currentKey = currentKey.nextKey(); + const storageEntry = createEventEntry(currentKey, this._roomId, event); + let member = await memberSync.lookupMemberAtEvent(event.sender, event, txn); + if (member) { + storageEntry.displayName = member.displayName; + storageEntry.avatarUrl = member.avatarUrl; + } + txn.timelineEvents.insert(storageEntry); + const entry = new EventEntry(storageEntry, this._fragmentIdComparer); + entries.push(entry); + const updatedRelationTargetEntries = await this._relationWriter.writeRelation(entry, txn, log); + if (updatedRelationTargetEntries) { + updatedEntries.push(...updatedRelationTargetEntries); + } + // update state events after writing event, so for a member event, + // we only update the member info after having written the member event + // to the timeline, as we want that event to have the old profile info. + // member events are written prior by MemberWriter. + if (typeof event.state_key === "string" && event.type !== MEMBER_EVENT_TYPE) { + timelineStateEventCount += 1; + txn.roomState.set(this._roomId, event); + } } + log.set("timelineStateEventCount", timelineStateEventCount); } - log.set("timelineStateEventCount", timelineStateEventCount); return {currentKey, entries, updatedEntries}; } @@ -236,10 +238,8 @@ export class SyncWriter { if (stateEvents) { await this._writeStateEvents(roomResponse, txn, log); } - if (timelineEvents?.length) { - const {currentKey, entries, updatedEntries} = - await this._writeTimeline(timelineEvents, timeline, memberSync, this._lastLiveKey, txn, log); - } + const {currentKey, entries, updatedEntries} = + await this._writeTimeline(timelineEvents, timeline, memberSync, this._lastLiveKey, txn, log); const memberChanges = await memberSync.write(txn); return {entries, updatedEntries, newLiveKey: currentKey, memberChanges}; } From 0c05e974653aa7db53652d352d4ade94cb9b678d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Aug 2021 19:07:27 +0200 Subject: [PATCH 04/23] abort upgrade txn on error --- src/matrix/storage/idb/utils.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/matrix/storage/idb/utils.js b/src/matrix/storage/idb/utils.js index fbb82fc5..b4e6209d 100644 --- a/src/matrix/storage/idb/utils.js +++ b/src/matrix/storage/idb/utils.js @@ -66,11 +66,18 @@ export function decodeUint32(str) { export function openDatabase(name, createObjectStore, version, idbFactory = window.indexedDB) { const req = idbFactory.open(name, version); - req.onupgradeneeded = (ev) => { + req.onupgradeneeded = async (ev) => { const db = ev.target.result; const txn = ev.target.transaction; const oldVersion = ev.oldVersion; - createObjectStore(db, txn, oldVersion, version); + try { + await createObjectStore(db, txn, oldVersion, version); + } catch (err) { + console.error("Aborting upgrade transaction because migration threw error"); + console.log(err.message); + console.log(err.stack); + txn.abort(); + } }; return reqAsPromise(req); } From fa555bedf0cb94fb0bbbec9b7338f66252ffb230 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Aug 2021 19:34:53 +0200 Subject: [PATCH 05/23] log storage migration --- src/matrix/SessionContainer.js | 2 +- src/matrix/storage/idb/StorageFactory.js | 19 +++++++++++-------- src/mocks/Storage.js | 5 +++-- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index f375fdd7..b58589d0 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -204,7 +204,7 @@ export class SessionContainer { reconnector: this._reconnector, }); this._sessionId = sessionInfo.id; - this._storage = await this._platform.storageFactory.create(sessionInfo.id); + this._storage = await this._platform.storageFactory.create(sessionInfo.id, log); // no need to pass access token to session const filteredSessionInfo = { id: sessionInfo.id, diff --git a/src/matrix/storage/idb/StorageFactory.js b/src/matrix/storage/idb/StorageFactory.js index 719d2672..de166a60 100644 --- a/src/matrix/storage/idb/StorageFactory.js +++ b/src/matrix/storage/idb/StorageFactory.js @@ -21,8 +21,9 @@ import { schema } from "./schema.js"; import { detectWebkitEarlyCloseTxnBug } from "./quirks.js"; const sessionName = sessionId => `hydrogen_session_${sessionId}`; -const openDatabaseWithSessionId = function(sessionId, idbFactory) { - return openDatabase(sessionName(sessionId), createStores, schema.length, idbFactory); +const openDatabaseWithSessionId = function(sessionId, idbFactory, log) { + const create = (db, txn, oldVersion, version) => createStores(db, txn, oldVersion, version, log); + return openDatabase(sessionName(sessionId), create, schema.length, idbFactory); } async function requestPersistedStorage() { @@ -49,7 +50,7 @@ export class StorageFactory { this._IDBKeyRange = IDBKeyRange; } - async create(sessionId) { + async create(sessionId, log) { await this._serviceWorkerHandler?.preventConcurrentSessionAccess(sessionId); requestPersistedStorage().then(persisted => { // Firefox lies here though, and returns true even if the user denied the request @@ -59,7 +60,7 @@ export class StorageFactory { }); const hasWebkitEarlyCloseTxnBug = await detectWebkitEarlyCloseTxnBug(this._idbFactory); - const db = await openDatabaseWithSessionId(sessionId, this._idbFactory); + const db = await openDatabaseWithSessionId(sessionId, this._idbFactory, log); return new Storage(db, this._IDBKeyRange, hasWebkitEarlyCloseTxnBug); } @@ -80,10 +81,12 @@ export class StorageFactory { } } -async function createStores(db, txn, oldVersion, version) { +async function createStores(db, txn, oldVersion, version, log) { const startIdx = oldVersion || 0; + return log.wrap({l: "storage migration", oldVersion, version}, async log => { + for(let i = startIdx; i < version; ++i) { + await log.wrap(`v${i + 1}`, log => schema[i](db, txn, log)); + } + }); - for(let i = startIdx; i < version; ++i) { - await schema[i](db, txn); - } } diff --git a/src/mocks/Storage.js b/src/mocks/Storage.js index 0cddc7bc..48e7056c 100644 --- a/src/mocks/Storage.js +++ b/src/mocks/Storage.js @@ -16,7 +16,8 @@ limitations under the License. import {FDBFactory, FDBKeyRange} from "../../lib/fake-indexeddb/index.js"; import {StorageFactory} from "../matrix/storage/idb/StorageFactory.js"; +import {NullLogItem} from "../logging/NullLogger.js"; export function createMockStorage() { - return new StorageFactory(null, new FDBFactory(), FDBKeyRange).create(1); -} \ No newline at end of file + return new StorageFactory(null, new FDBFactory(), FDBKeyRange).create(1, new NullLogItem()); +} From 8e6bd6a7a182486b5a638c69e792e45b7abace1f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Aug 2021 19:39:24 +0200 Subject: [PATCH 06/23] add missing room ids to identities for tracked rooms & clear outbound session --- src/matrix/e2ee/DeviceTracker.js | 16 +++++++++++ src/matrix/storage/idb/schema.js | 47 ++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index a14b42f3..32e2d00c 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -19,6 +19,22 @@ import {verifyEd25519Signature, SIGNATURE_ALGORITHM} from "./common.js"; const TRACKING_STATUS_OUTDATED = 0; const TRACKING_STATUS_UPTODATE = 1; +export function addRoomToIdentity(identity, userId, roomId) { + if (!identity) { + identity = { + userId: userId, + roomIds: [roomId], + deviceTrackingStatus: TRACKING_STATUS_OUTDATED, + }; + return identity; + } else { + if (!identity.roomIds.includes(roomId)) { + identity.roomIds.push(roomId); + return identity; + } + } +} + // map 1 device from /keys/query response to DeviceIdentity function deviceKeysAsDeviceIdentity(deviceSection) { const deviceId = deviceSection["device_id"]; diff --git a/src/matrix/storage/idb/schema.js b/src/matrix/storage/idb/schema.js index 352c810c..6f02b828 100644 --- a/src/matrix/storage/idb/schema.js +++ b/src/matrix/storage/idb/schema.js @@ -1,8 +1,10 @@ import {iterateCursor, reqAsPromise} from "./utils.js"; import {RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "../../room/members/RoomMember.js"; +import {addRoomToIdentity} from "../../e2ee/DeviceTracker.js"; import {RoomMemberStore} from "./stores/RoomMemberStore.js"; import {SessionStore} from "./stores/SessionStore.js"; import {encodeScopeTypeKey} from "./stores/OperationStore.js"; +import {MAX_UNICODE} from "./stores/common.js"; // FUNCTIONS SHOULD ONLY BE APPENDED!! // the index in the array is the database version @@ -17,6 +19,7 @@ export const schema = [ createArchivedRoomSummaryStore, migrateOperationScopeIndex, createTimelineRelationsStore, + fixMissingRoomsInUserIdentities ]; // TODO: how to deal with git merge conflicts of this array? @@ -142,3 +145,47 @@ async function migrateOperationScopeIndex(db, txn) { function createTimelineRelationsStore(db) { db.createObjectStore("timelineRelations", {keyPath: "key"}); } + +//v11 doesn't change the schema, but ensures all userIdentities have all the roomIds they should (see #470) +async function fixMissingRoomsInUserIdentities(db, txn, log) { + const roomSummaryStore = txn.objectStore("roomSummary"); + const trackedRoomIds = []; + await iterateCursor(roomSummaryStore.openCursor(), roomSummary => { + if (roomSummary.isTrackingMembers) { + trackedRoomIds.push(roomSummary.roomId); + } + }); + const outboundGroupSessionsStore = txn.objectStore("outboundGroupSessions"); + const userIdentitiesStore = txn.objectStore("userIdentities"); + const roomMemberStore = txn.objectStore("roomMembers"); + for (const roomId of trackedRoomIds) { + let foundMissing = false; + const joinedUserIds = []; + const memberRange = IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true); + await log.wrap({l: "room", id: roomId}, async log => { + await iterateCursor(roomMemberStore.openCursor(memberRange), member => { + if (member.membership === "join") { + joinedUserIds.push(member.userId); + } + }); + log.set("joinedUserIds", joinedUserIds.length); + for (const userId of joinedUserIds) { + const identity = await reqAsPromise(userIdentitiesStore.get(userId)); + const originalRoomCount = identity?.roomIds?.length; + const updatedIdentity = addRoomToIdentity(identity, userId, roomId); + if (updatedIdentity) { + log.log({l: `fixing up`, id: userId, + roomsBefore: originalRoomCount, roomsAfter: updatedIdentity.roomIds.length}); + userIdentitiesStore.put(updatedIdentity); + foundMissing = true; + } + } + log.set("foundMissing", foundMissing); + if (foundMissing) { + // clear outbound megolm session, + // so we'll create a new one on the next message that will be properly shared + outboundGroupSessionsStore.delete(roomId); + } + }); + } +} From 6a6762f03682f6dfbc5f656158e60f3a702fced6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Aug 2021 20:05:34 +0200 Subject: [PATCH 07/23] ensure memberwriter works with undefined for state/timeline events array --- .../room/timeline/persistence/MemberWriter.js | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/matrix/room/timeline/persistence/MemberWriter.js b/src/matrix/room/timeline/persistence/MemberWriter.js index fba43ddf..b21a4461 100644 --- a/src/matrix/room/timeline/persistence/MemberWriter.js +++ b/src/matrix/room/timeline/persistence/MemberWriter.js @@ -61,7 +61,10 @@ class MemberSync { this._memberWriter = memberWriter; this._timelineEvents = timelineEvents; this._hasFetchedMembers = hasFetchedMembers; - this._newStateMembers = stateEvents && this._stateEventsToMembers(stateEvents); + this._newStateMembers = null; + if (stateEvents) { + this._newStateMembers = this._stateEventsToMembers(stateEvents); + } } get _roomId() { @@ -84,11 +87,11 @@ class MemberSync { return members; } - _timelineEventsToMembers() { + _timelineEventsToMembers(timelineEvents) { let members; // iterate backwards to only add the last member in the timeline - for (let i = this._timelineEvents.length - 1; i >= 0; i--) { - const e = this._timelineEvents[i]; + for (let i = timelineEvents.length - 1; i >= 0; i--) { + const e = timelineEvents[i]; const userId = e.state_key; if (e.type === MEMBER_EVENT_TYPE && !members?.has(userId)) { const member = RoomMember.fromMemberEvent(this._roomId, e); @@ -104,9 +107,12 @@ class MemberSync { } async lookupMemberAtEvent(userId, event, txn) { - let member = this._findPrecedingMemberEventInTimeline(userId, event); - if (member) { - return member; + let member; + if (this._timelineEvents) { + member = this._findPrecedingMemberEventInTimeline(userId, event); + if (member) { + return member; + } } member = this._newStateMembers?.get(userId); if (member) { @@ -117,7 +123,10 @@ class MemberSync { async write(txn) { const memberChanges = new Map(); - const newTimelineMembers = this._timelineEventsToMembers(); + let newTimelineMembers; + if (this._timelineEvents) { + newTimelineMembers = this._timelineEventsToMembers(this._timelineEvents); + } if (this._newStateMembers) { for (const member of this._newStateMembers.values()) { if (!newTimelineMembers?.has(member.userId)) { @@ -217,14 +226,6 @@ export function tests() { const alice = "@alice:hs.tld"; const avatar = "mxc://hs.tld/def"; -/* - join without previous membership - join during gap with hasFetchedMembers=false - join during gap with hasFetchedMembers=true - join after invite - -*/ - return { "new join": async assert => { const writer = new MemberWriter(roomId); @@ -409,5 +410,12 @@ export function tests() { const change = changes.get(alice); assert(change.hasJoined); }, + "write works without event arrays": async assert => { + const writer = new MemberWriter(roomId); + const txn = createStorage(); + const memberSync = await writer.prepareMemberSync(undefined, undefined, false); + const changes = await memberSync.write(txn); + assert.equal(changes.size, 0); + }, }; } From ddb6753f8d7eed6f10948aa2ddd5a60708692e67 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Aug 2021 20:05:53 +0200 Subject: [PATCH 08/23] fix refactor error --- src/matrix/room/timeline/persistence/SyncWriter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/persistence/SyncWriter.js b/src/matrix/room/timeline/persistence/SyncWriter.js index f6c167e8..240feb76 100644 --- a/src/matrix/room/timeline/persistence/SyncWriter.js +++ b/src/matrix/room/timeline/persistence/SyncWriter.js @@ -236,7 +236,7 @@ export class SyncWriter { } const memberSync = this._memberWriter.prepareMemberSync(stateEvents, timelineEvents, hasFetchedMembers); if (stateEvents) { - await this._writeStateEvents(roomResponse, txn, log); + await this._writeStateEvents(stateEvents, txn, log); } const {currentKey, entries, updatedEntries} = await this._writeTimeline(timelineEvents, timeline, memberSync, this._lastLiveKey, txn, log); From 0ca46bf2ac621d25ab357508ea7111f5a5da5567 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 30 Aug 2021 14:52:04 +0200 Subject: [PATCH 09/23] don't log here as we log at a lower level, and don't fail on abort --- src/matrix/storage/idb/utils.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/matrix/storage/idb/utils.js b/src/matrix/storage/idb/utils.js index b4e6209d..3b9ca8bc 100644 --- a/src/matrix/storage/idb/utils.js +++ b/src/matrix/storage/idb/utils.js @@ -73,10 +73,10 @@ export function openDatabase(name, createObjectStore, version, idbFactory = wind try { await createObjectStore(db, txn, oldVersion, version); } catch (err) { - console.error("Aborting upgrade transaction because migration threw error"); - console.log(err.message); - console.log(err.stack); - txn.abort(); + // try aborting on error, if that hasn't been done already + try { + txn.abort(); + } catch (err) {} } }; return reqAsPromise(req); From a4373324821e53d3404afbabfa01e2b0d149a3dd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 30 Aug 2021 14:52:20 +0200 Subject: [PATCH 10/23] whitespace --- src/matrix/storage/idb/StorageFactory.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/matrix/storage/idb/StorageFactory.js b/src/matrix/storage/idb/StorageFactory.js index de166a60..a9b86273 100644 --- a/src/matrix/storage/idb/StorageFactory.js +++ b/src/matrix/storage/idb/StorageFactory.js @@ -88,5 +88,4 @@ async function createStores(db, txn, oldVersion, version, log) { await log.wrap(`v${i + 1}`, log => schema[i](db, txn, log)); } }); - } From 0d6ae19d99600d4e9717985309c617c2ef47b9ba Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 30 Aug 2021 15:05:57 +0200 Subject: [PATCH 11/23] use same code to add room to identity in migration as in device tracker --- src/matrix/e2ee/DeviceTracker.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index 32e2d00c..e230ea7d 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -123,17 +123,9 @@ export class DeviceTracker { async _writeMember(member, txn) { const {userIdentities} = txn; const identity = await userIdentities.get(member.userId); - if (!identity) { - userIdentities.set({ - userId: member.userId, - roomIds: [member.roomId], - deviceTrackingStatus: TRACKING_STATUS_OUTDATED, - }); - } else { - if (!identity.roomIds.includes(member.roomId)) { - identity.roomIds.push(member.roomId); - userIdentities.set(identity); - } + const updatedIdentity = addRoomToIdentity(identity, member.userId, member.roomId); + if (updatedIdentity) { + userIdentities.set(updatedIdentity); } } From 7fb541217686b6af5b8eaccf35cbdbd9da9e3dfd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 30 Aug 2021 15:12:25 +0200 Subject: [PATCH 12/23] keep comment where it was --- src/matrix/room/timeline/persistence/SyncWriter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/persistence/SyncWriter.js b/src/matrix/room/timeline/persistence/SyncWriter.js index 240feb76..54fd5a0b 100644 --- a/src/matrix/room/timeline/persistence/SyncWriter.js +++ b/src/matrix/room/timeline/persistence/SyncWriter.js @@ -148,8 +148,8 @@ export class SyncWriter { async _writeTimeline(timelineEvents, timeline, memberSync, currentKey, txn, log) { const entries = []; const updatedEntries = []; - // only create a fragment when we will really write an event if (timelineEvents?.length) { + // only create a fragment when we will really write an event currentKey = await this._ensureLiveFragment(currentKey, entries, timeline, txn, log); log.set("timelineEvents", timelineEvents.length); let timelineStateEventCount = 0; From 8d414970c695d9432c09cebc5eb9ee46b39d9675 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 30 Aug 2021 15:23:09 +0200 Subject: [PATCH 13/23] release v0.2.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c7e0cd9f..3116863c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrogen-web", - "version": "0.2.7", + "version": "0.2.8", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "main": "index.js", "directories": { From 3ded5b20d3a812a62393e46ee6f73d8784ce8a1f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 31 Aug 2021 08:16:27 +0200 Subject: [PATCH 14/23] dedupe some code here --- src/matrix/storage/idb/error.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/matrix/storage/idb/error.ts b/src/matrix/storage/idb/error.ts index bd3fb0a8..0d723837 100644 --- a/src/matrix/storage/idb/error.ts +++ b/src/matrix/storage/idb/error.ts @@ -33,15 +33,10 @@ export class IDBError extends StorageError { storeName: string; databaseName: string; - constructor(message: string, source: IDBIndex | IDBCursor | IDBObjectStore, cause: DOMException | null = null) { - let storeName: string, databaseName: string; - if (source instanceof IDBCursor) { - storeName = _sourceName(source.source); - databaseName = _sourceDatabase(source.source); - } else { - storeName = _sourceName(source); - databaseName = _sourceDatabase(source); - } + constructor(message: string, sourceOrCursor: IDBIndex | IDBCursor | IDBObjectStore, cause: DOMException | null = null) { + const source = "source" in sourceOrCursor ? sourceOrCursor.source : sourceOrCursor; + const storeName = _sourceName(source); + const databaseName = _sourceDatabase(source); let fullMessage = `${message} on ${databaseName}.${storeName}`; if (cause) { fullMessage += ": "; From f466266a5f93e6ee637de9a9cd8cfb3efb82e71a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 31 Aug 2021 08:16:37 +0200 Subject: [PATCH 15/23] bring back extra caution --- src/matrix/storage/idb/error.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/storage/idb/error.ts b/src/matrix/storage/idb/error.ts index 0d723837..388ad4c0 100644 --- a/src/matrix/storage/idb/error.ts +++ b/src/matrix/storage/idb/error.ts @@ -25,8 +25,8 @@ function _sourceName(source: IDBIndex | IDBObjectStore): string { function _sourceDatabase(source: IDBIndex | IDBObjectStore): string { return "objectStore" in source ? - source.objectStore.transaction.db.name : - source.transaction.db.name; + source.objectStore?.transaction?.db?.name : + source.transaction?.db?.name; } export class IDBError extends StorageError { From 995ed23b3ef4d726e0cdcb12fb5ff4f69d548595 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 31 Aug 2021 08:43:39 +0200 Subject: [PATCH 16/23] tell TS we're certain to have a txn --- src/matrix/storage/idb/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/storage/idb/utils.ts b/src/matrix/storage/idb/utils.ts index 76c4d1ed..bd1683ea 100644 --- a/src/matrix/storage/idb/utils.ts +++ b/src/matrix/storage/idb/utils.ts @@ -74,7 +74,7 @@ export function openDatabase(name: string, createObjectStore: CreateObjectStore, req.onupgradeneeded = async (ev : IDBVersionChangeEvent) => { const req = ev.target as IDBRequest; const db = req.result; - const txn = req.transaction; + const txn = req.transaction!; const oldVersion = ev.oldVersion; try { await createObjectStore(db, txn, oldVersion, version); From 0a13c12b1f342a14b2f71fe369cc7fb8d2a2e899 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 31 Aug 2021 12:16:59 +0000 Subject: [PATCH 17/23] Update FAQ.md mention olm issues when integrating for now --- doc/FAQ.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/FAQ.md b/doc/FAQ.md index 1a4dbe9e..c6445c72 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -37,3 +37,5 @@ There are no npm modules yet published for Hydrogen. The easiest is probably to For example, for a single room chat, you could create an instance of `Platform`, you create a new `SessionContainer` with it, call `startWithLogin` on it, observe `sessionContainer.loadStatus` to know when initial sync is done, then do `sessionContainer.session.rooms.get('roomid')` and you create a `RoomViewModel` with it and pass that to a `RoomView`. Then you call `document.appendChild(roomView.mount())` and you should see a syncing room. Feel free to ask for pointers in #hydrogen:matrix.org as the documentation is still lacking considerably. Note that at this early, pre 1.0 stage of the project, there is no promise of API stability yet. + +Also, to make end-to-end encryption work, you'll likely need some tweaks to your build system, see [this issue](https://github.com/vector-im/hydrogen-web/issues/467). From ce20d40ff78ecbc4a3255e289900fcc9123b371c Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Tue, 31 Aug 2021 11:31:17 -0700 Subject: [PATCH 18/23] Revert the return-promise change --- src/matrix/storage/idb/stores/RoomMemberStore.ts | 4 ++-- src/matrix/storage/idb/stores/SessionStore.ts | 12 ++++++------ .../storage/idb/stores/TimelineEventStore.ts | 16 ++++++++-------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/matrix/storage/idb/stores/RoomMemberStore.ts b/src/matrix/storage/idb/stores/RoomMemberStore.ts index 847e8dae..6abd897f 100644 --- a/src/matrix/storage/idb/stores/RoomMemberStore.ts +++ b/src/matrix/storage/idb/stores/RoomMemberStore.ts @@ -78,10 +78,10 @@ export class RoomMemberStore { return userIds; } - removeAllForRoom(roomId: string): Promise { + removeAllForRoom(roomId: string): void { // 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 const range = this._roomMembersStore.IDBKeyRange.bound(roomId, `${roomId}|${MAX_UNICODE}`, true, true); - return this._roomMembersStore.delete(range); + this._roomMembersStore.delete(range); } } diff --git a/src/matrix/storage/idb/stores/SessionStore.ts b/src/matrix/storage/idb/stores/SessionStore.ts index bf3d6d11..4dc467fe 100644 --- a/src/matrix/storage/idb/stores/SessionStore.ts +++ b/src/matrix/storage/idb/stores/SessionStore.ts @@ -34,15 +34,15 @@ export class SessionStore { } } - set(key: string, value: any): Promise { - return this._sessionStore.put({key, value}); + set(key: string, value: any): void { + this._sessionStore.put({key, value}); } - add(key: string, value: any): Promise { - return this._sessionStore.add({key, value}); + add(key: string, value: any): void { + this._sessionStore.add({key, value}); } - remove(key: IDBValidKey): Promise { - return this._sessionStore.delete(key); + remove(key: IDBValidKey): void { + this._sessionStore.delete(key); } } diff --git a/src/matrix/storage/idb/stores/TimelineEventStore.ts b/src/matrix/storage/idb/stores/TimelineEventStore.ts index 764b886b..a479efd7 100644 --- a/src/matrix/storage/idb/stores/TimelineEventStore.ts +++ b/src/matrix/storage/idb/stores/TimelineEventStore.ts @@ -262,23 +262,23 @@ 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 the entry to insert - * @return 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} ... */ - insert(entry: StorageEntry): Promise { + insert(entry: StorageEntry): void { 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? - return this._timelineStore.add(entry); + 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 the entry to update. - * @return 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: StorageEntry): Promise { - return this._timelineStore.put(entry); + update(entry: StorageEntry): void { + this._timelineStore.put(entry); } get(roomId: string, eventKey: EventKey): Promise { @@ -289,10 +289,10 @@ export class TimelineEventStore { return this._timelineStore.index("byEventId").get(encodeEventIdKey(roomId, eventId)); } - removeAllForRoom(roomId: string): Promise { + removeAllForRoom(roomId: string): void { const minKey = encodeKey(roomId, KeyLimits.minStorageKey, KeyLimits.minStorageKey); const maxKey = encodeKey(roomId, KeyLimits.maxStorageKey, KeyLimits.maxStorageKey); const range = this._timelineStore.IDBKeyRange.bound(minKey, maxKey); - return this._timelineStore.delete(range); + this._timelineStore.delete(range); } } From 16d3ed579b0427566f0b704e6c57d851bc9ce8c2 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Tue, 31 Aug 2021 11:41:07 -0700 Subject: [PATCH 19/23] Reduce IDBValidKey returns to the public API --- src/matrix/storage/idb/Store.ts | 8 ++++---- src/matrix/storage/idb/schema.js | 2 +- src/matrix/storage/idb/stores/InviteStore.ts | 4 ++-- src/matrix/storage/idb/stores/RoomMemberStore.ts | 4 ++-- src/matrix/storage/idb/stores/RoomSummaryStore.ts | 4 ++-- src/matrix/storage/idb/stores/SessionStore.ts | 4 ++-- src/matrix/storage/idb/stores/TimelineRelationStore.ts | 4 ++-- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/matrix/storage/idb/Store.ts b/src/matrix/storage/idb/Store.ts index 8063a4c8..e2da0707 100644 --- a/src/matrix/storage/idb/Store.ts +++ b/src/matrix/storage/idb/Store.ts @@ -148,7 +148,7 @@ export class Store extends QueryTarget { return new QueryTarget(new QueryTargetWrapper(this._idbStore.index(indexName))); } - put(value: T): Promise { + put(value: T): void { // 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 // request and await it. @@ -159,12 +159,12 @@ export class Store extends QueryTarget { // // Note that this can still throw synchronously, like it does for TransactionInactiveError, // 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 { + add(value: T): void { // 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 { diff --git a/src/matrix/storage/idb/schema.js b/src/matrix/storage/idb/schema.js index f5472107..50e298d7 100644 --- a/src/matrix/storage/idb/schema.js +++ b/src/matrix/storage/idb/schema.js @@ -4,7 +4,7 @@ import {addRoomToIdentity} from "../../e2ee/DeviceTracker.js"; import {RoomMemberStore} from "./stores/RoomMemberStore"; import {SessionStore} from "./stores/SessionStore"; import {encodeScopeTypeKey} from "./stores/OperationStore.js"; -import {MAX_UNICODE} from "./stores/common.js"; +import {MAX_UNICODE} from "./stores/common"; // FUNCTIONS SHOULD ONLY BE APPENDED!! // the index in the array is the database version diff --git a/src/matrix/storage/idb/stores/InviteStore.ts b/src/matrix/storage/idb/stores/InviteStore.ts index f8841a71..7616ebf7 100644 --- a/src/matrix/storage/idb/stores/InviteStore.ts +++ b/src/matrix/storage/idb/stores/InviteStore.ts @@ -41,8 +41,8 @@ export class InviteStore { return this._inviteStore.selectAll(); } - set(invite: InviteData): Promise { - return this._inviteStore.put(invite); + set(invite: InviteData): void { + this._inviteStore.put(invite); } remove(roomId: string): void { diff --git a/src/matrix/storage/idb/stores/RoomMemberStore.ts b/src/matrix/storage/idb/stores/RoomMemberStore.ts index 6abd897f..e389bb77 100644 --- a/src/matrix/storage/idb/stores/RoomMemberStore.ts +++ b/src/matrix/storage/idb/stores/RoomMemberStore.ts @@ -50,10 +50,10 @@ export class RoomMemberStore { return this._roomMembersStore.get(encodeKey(roomId, userId)); } - async set(member: MemberData): Promise { + set(member: MemberData): void { // Object.assign would be more typesafe, but small objects (member as any).key = encodeKey(member.roomId, member.userId); - return this._roomMembersStore.put(member as MemberStorageEntry); + this._roomMembersStore.put(member as MemberStorageEntry); } getAll(roomId: string): Promise { diff --git a/src/matrix/storage/idb/stores/RoomSummaryStore.ts b/src/matrix/storage/idb/stores/RoomSummaryStore.ts index 19e847b1..bd911572 100644 --- a/src/matrix/storage/idb/stores/RoomSummaryStore.ts +++ b/src/matrix/storage/idb/stores/RoomSummaryStore.ts @@ -42,8 +42,8 @@ export class RoomSummaryStore { return this._summaryStore.selectAll(); } - set(summary: SummaryData): Promise { - return this._summaryStore.put(summary); + set(summary: SummaryData): void { + this._summaryStore.put(summary); } get(roomId: string): Promise { diff --git a/src/matrix/storage/idb/stores/SessionStore.ts b/src/matrix/storage/idb/stores/SessionStore.ts index 4dc467fe..859d3319 100644 --- a/src/matrix/storage/idb/stores/SessionStore.ts +++ b/src/matrix/storage/idb/stores/SessionStore.ts @@ -27,7 +27,7 @@ export class SessionStore { this._sessionStore = sessionStore; } - async get(key: IDBValidKey): Promise { + async get(key: string): Promise { const entry = await this._sessionStore.get(key); if (entry) { return entry.value; @@ -42,7 +42,7 @@ export class SessionStore { this._sessionStore.add({key, value}); } - remove(key: IDBValidKey): void { + remove(key: string): void { this._sessionStore.delete(key); } } diff --git a/src/matrix/storage/idb/stores/TimelineRelationStore.ts b/src/matrix/storage/idb/stores/TimelineRelationStore.ts index 0905eb0c..6772864a 100644 --- a/src/matrix/storage/idb/stores/TimelineRelationStore.ts +++ b/src/matrix/storage/idb/stores/TimelineRelationStore.ts @@ -39,8 +39,8 @@ export class TimelineRelationStore { this._store = store; } - add(roomId: string, targetEventId: string, relType: string, sourceEventId: string): Promise { - return this._store.add({key: encodeKey(roomId, targetEventId, relType, sourceEventId)}); + add(roomId: string, targetEventId: string, relType: string, sourceEventId: string): void { + this._store.add({key: encodeKey(roomId, targetEventId, relType, sourceEventId)}); } remove(roomId: string, targetEventId: string, relType: string, sourceEventId: string): Promise { From 056c7d40eb941696de5e42c0d9d9c137c86931e7 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Tue, 31 Aug 2021 12:10:36 -0700 Subject: [PATCH 20/23] Rename RoomEvent to TimelineEvent --- src/matrix/storage/idb/stores/TimelineEventStore.ts | 4 ++-- src/matrix/storage/types.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/matrix/storage/idb/stores/TimelineEventStore.ts b/src/matrix/storage/idb/stores/TimelineEventStore.ts index a479efd7..466f9737 100644 --- a/src/matrix/storage/idb/stores/TimelineEventStore.ts +++ b/src/matrix/storage/idb/stores/TimelineEventStore.ts @@ -19,7 +19,7 @@ import { StorageError } from "../../common"; import { encodeUint32 } from "../utils"; import {KeyLimits} from "../../common"; import {Store} from "../Store"; -import {RoomEvent, StateEvent} from "../../types"; +import {TimelineEvent, StateEvent} from "../../types"; interface Annotation { count: number; @@ -31,7 +31,7 @@ interface StorageEntry { roomId: string; fragmentId: number; eventIndex: number; - event: RoomEvent | StateEvent; + event: TimelineEvent | StateEvent; displayName?: string; avatarUrl?: string; annotations?: { [key : string]: Annotation }; diff --git a/src/matrix/storage/types.ts b/src/matrix/storage/types.ts index bc61e1f8..a03fa1d7 100644 --- a/src/matrix/storage/types.ts +++ b/src/matrix/storage/types.ts @@ -16,7 +16,7 @@ limitations under the License. export type Content = { [key: string]: any } -export interface RoomEvent { +export interface TimelineEvent { content: Content; type: string; event_id: string; @@ -25,4 +25,4 @@ export interface RoomEvent { unsigned?: Content; } -export type StateEvent = RoomEvent & { prev_content?: Content, state_key: string } +export type StateEvent = TimelineEvent & { prev_content?: Content, state_key: string } From 1fcc147da7a02b3847d2d21d217e9ac9e74ac62c Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Tue, 31 Aug 2021 12:16:16 -0700 Subject: [PATCH 21/23] Add type to the IDBKeyRange field --- src/matrix/storage/idb/stores/TimelineEventStore.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/matrix/storage/idb/stores/TimelineEventStore.ts b/src/matrix/storage/idb/stores/TimelineEventStore.ts index 466f9737..eb4c70cc 100644 --- a/src/matrix/storage/idb/stores/TimelineEventStore.ts +++ b/src/matrix/storage/idb/stores/TimelineEventStore.ts @@ -53,15 +53,15 @@ function decodeEventIdKey(eventIdKey: string): { roomId: string, eventId: string } class Range { - private _IDBKeyRange: any; // TODO what's the appropriate representation here? + private _IDBKeyRange: typeof 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; + constructor(_IDBKeyRange: any, only?: EventKey, lower?: EventKey, upper?: EventKey, lowerOpen: boolean = false, upperOpen: boolean = false) { + this._IDBKeyRange = _IDBKeyRange; this._only = only; this._lower = lower; this._upper = upper; From bef02d238f9f4526b8ee792d037de270c618fda9 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Tue, 31 Aug 2021 15:12:09 -0700 Subject: [PATCH 22/23] Split keys out of stored data types --- .../idb/stores/GroupSessionDecryptionStore.ts | 13 ++++++------ .../storage/idb/stores/OperationStore.ts | 21 +++++++++++-------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.ts b/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.ts index 2b38c1fb..b6636f13 100644 --- a/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.ts +++ b/src/matrix/storage/idb/stores/GroupSessionDecryptionStore.ts @@ -24,13 +24,14 @@ function encodeKey(roomId: string, sessionId: string, messageIndex: number | str interface GroupSessionDecryption { eventId: string; timestamp: number; - key: string; } -export class GroupSessionDecryptionStore { - private _store: Store; +type GroupSessionEntry = GroupSessionDecryption & { key: string } - constructor(store: Store) { +export class GroupSessionDecryptionStore { + private _store: Store; + + constructor(store: Store) { this._store = store; } @@ -39,8 +40,8 @@ export class GroupSessionDecryptionStore { } set(roomId: string, sessionId: string, messageIndex: number, decryption: GroupSessionDecryption): void { - decryption.key = encodeKey(roomId, sessionId, messageIndex); - this._store.put(decryption); + (decryption as GroupSessionEntry).key = encodeKey(roomId, sessionId, messageIndex); + this._store.put(decryption as GroupSessionEntry); } removeAllForRoom(roomId: string): Promise { diff --git a/src/matrix/storage/idb/stores/OperationStore.ts b/src/matrix/storage/idb/stores/OperationStore.ts index 080d3e66..cccd8e2d 100644 --- a/src/matrix/storage/idb/stores/OperationStore.ts +++ b/src/matrix/storage/idb/stores/OperationStore.ts @@ -20,15 +20,18 @@ export function encodeScopeTypeKey(scope: string, type: string): string { return `${scope}|${type}`; } -interface Operation { +interface BaseOperation { id: string; - type: string; scope: string; userIds: string[]; - scopeTypeKey: string; - roomKeyMessage: RoomKeyMessage; } +type OperationType = { type: "share_room_key"; roomKeyMessage: RoomKeyMessage; } + +type Operation = BaseOperation & OperationType + +type OperationEntry = Operation & { scopeTypeKey: string; } + interface RoomKeyMessage { room_id: string; session_id: string; @@ -38,9 +41,9 @@ interface RoomKeyMessage { } export class OperationStore { - private _store: Store; + private _store: Store; - constructor(store: Store) { + constructor(store: Store) { this._store = store; } @@ -62,12 +65,12 @@ export class OperationStore { } add(operation: Operation): void { - operation.scopeTypeKey = encodeScopeTypeKey(operation.scope, operation.type); - this._store.add(operation); + (operation as OperationEntry).scopeTypeKey = encodeScopeTypeKey(operation.scope, operation.type); + this._store.add(operation as OperationEntry); } update(operation: Operation): void { - this._store.put(operation); + this._store.put(operation as OperationEntry); } remove(id: string): Promise { From f34ee53a1220551ef93b39b916e0b455ae342bf0 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Tue, 31 Aug 2021 15:14:21 -0700 Subject: [PATCH 23/23] Avoid casting to any when a more specific type is possible --- src/matrix/storage/idb/stores/RoomMemberStore.ts | 2 +- src/matrix/storage/idb/stores/TimelineFragmentStore.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/storage/idb/stores/RoomMemberStore.ts b/src/matrix/storage/idb/stores/RoomMemberStore.ts index e389bb77..00cf607f 100644 --- a/src/matrix/storage/idb/stores/RoomMemberStore.ts +++ b/src/matrix/storage/idb/stores/RoomMemberStore.ts @@ -52,7 +52,7 @@ export class RoomMemberStore { set(member: MemberData): void { // Object.assign would be more typesafe, but small objects - (member as any).key = encodeKey(member.roomId, member.userId); + (member as MemberStorageEntry).key = encodeKey(member.roomId, member.userId); this._roomMembersStore.put(member as MemberStorageEntry); } diff --git a/src/matrix/storage/idb/stores/TimelineFragmentStore.ts b/src/matrix/storage/idb/stores/TimelineFragmentStore.ts index 54b66489..813fc3f3 100644 --- a/src/matrix/storage/idb/stores/TimelineFragmentStore.ts +++ b/src/matrix/storage/idb/stores/TimelineFragmentStore.ts @@ -75,7 +75,7 @@ export class TimelineFragmentStore { // depends if we want to do anything smart with fragment ids, // like give them meaning depending on range. not for now probably ... add(fragment: Fragment): void { - (fragment as any).key = encodeKey(fragment.roomId, fragment.id); + (fragment as FragmentEntry).key = encodeKey(fragment.roomId, fragment.id); this._store.add(fragment as FragmentEntry); }