Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix infinite loop when restoring cached read receipts #2963

Merged
merged 2 commits into from
Dec 9, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 5 additions & 11 deletions src/models/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2079,8 +2079,13 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
room: this,
client: this.client,
pendingEventOrdering: this.opts.pendingEventOrdering,
receipts: this.cachedThreadReadReceipts.get(threadId) ?? []
});

// All read receipts should now come down from sync, we do not need to keep
// a reference to the cached receipts anymore.
this.cachedThreadReadReceipts.delete(threadId);

// This is necessary to be able to jump to events in threads:
// If we jump to an event in a thread where neither the event, nor the root,
// nor any thread event are loaded yet, we'll load the event as well as the thread root, create the thread,
Expand Down Expand Up @@ -2110,17 +2115,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
if (this.threadsReady) {
this.updateThreadRootEvents(thread, toStartOfTimeline, false);
}

// Pulling all the cached thread read receipts we've discovered when we
// did an initial sync, and applying them to the thread now that it exists
// on the client side
if (this.cachedThreadReadReceipts.has(threadId)) {
for (const { event, synthetic } of this.cachedThreadReadReceipts.get(threadId)!) {
this.addReceipt(event, synthetic);
}
this.cachedThreadReadReceipts.delete(threadId);
}

this.emit(ThreadEvent.New, thread, toStartOfTimeline);

return thread;
Expand Down
31 changes: 30 additions & 1 deletion src/models/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { RoomState } from "./room-state";
import { ServerControlledNamespacedValue } from "../NamespacedValue";
import { logger } from "../logger";
import { ReadReceipt } from "./read-receipt";
import { ReceiptType } from "../@types/read_receipts";
import { Receipt, ReceiptContent, ReceiptType } from "../@types/read_receipts";

export enum ThreadEvent {
New = "Thread.new",
Expand All @@ -50,6 +50,7 @@ interface IThreadOpts {
room: Room;
client: MatrixClient;
pendingEventOrdering?: PendingEventOrdering;
receipts?: { event: MatrixEvent, synthetic: boolean }[];
}

export enum FeatureSupport {
Expand Down Expand Up @@ -127,6 +128,8 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho);
this.timelineSet.on(RoomEvent.Timeline, this.onTimelineEvent);

this.processReceipts(opts.receipts);

// even if this thread is thought to be originating from this client, we initialise it as we may be in a
// gappy sync and a thread around this event may already exist.
this.updateThreadMetadata();
Expand Down Expand Up @@ -284,6 +287,32 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
this.timeline = this.events;
}

/**
* Processes the receipts that were caught during initial sync
* When clients become aware of a thread, they try to retrieve those read receipts
* and apply them to the current thread
* @param receipts A collection of the receipts cached from initial sync
*/
private processReceipts(receipts: { event: MatrixEvent, synthetic: boolean }[] = []): void {
for (const { event, synthetic } of receipts) {
const content = event.getContent<ReceiptContent>();
Object.keys(content).forEach((eventId: string) => {
Object.keys(content[eventId]).forEach((receiptType: ReceiptType | string) => {
Object.keys(content[eventId][receiptType]).forEach((userId: string) => {
const receipt = content[eventId][receiptType][userId] as Receipt;
this.addReceiptToStructure(
eventId,
receiptType as ReceiptType,
userId,
receipt,
synthetic,
);
});
});
});
}
}

private getRootEventBundledRelationship(rootEvent = this.rootEvent): IThreadBundledRelationship | undefined {
return rootEvent?.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
}
Expand Down