Skip to content

Commit 258c8c6

Browse files
committed
Feed events to widgets as they are decrypted (even if out of order)
1 parent 2631b90 commit 258c8c6

File tree

1 file changed

+94
-63
lines changed

1 file changed

+94
-63
lines changed

src/stores/widgets/StopGapWidget.ts

Lines changed: 94 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,10 @@ export class StopGapWidget extends EventEmitter {
154154
private kind: WidgetKind;
155155
private readonly virtual: boolean;
156156
private readUpToMap: { [roomId: string]: string } = {}; // room ID to event ID
157-
private stickyPromise?: () => Promise<void>; // This promise will be called and needs to resolve before the widget will actually become sticky.
157+
// This promise will be called and needs to resolve before the widget will actually become sticky.
158+
private stickyPromise?: () => Promise<void>;
159+
// Holds events that should be fed to the widget once they finish decrypting
160+
private readonly eventsToFeed = new WeakSet<MatrixEvent>();
158161

159162
public constructor(private appTileProps: IAppTileProps) {
160163
super();
@@ -465,12 +468,10 @@ export class StopGapWidget extends EventEmitter {
465468

466469
private onEvent = (ev: MatrixEvent): void => {
467470
this.client.decryptEventIfNeeded(ev);
468-
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
469471
this.feedEvent(ev);
470472
};
471473

472474
private onEventDecrypted = (ev: MatrixEvent): void => {
473-
if (ev.isDecryptionFailure()) return;
474475
this.feedEvent(ev);
475476
};
476477

@@ -480,72 +481,102 @@ export class StopGapWidget extends EventEmitter {
480481
await this.messaging?.feedToDevice(ev.getEffectiveEvent() as IRoomEvent, ev.isEncrypted());
481482
};
482483

483-
private feedEvent(ev: MatrixEvent): void {
484-
if (!this.messaging) return;
485-
486-
// Check to see if this event would be before or after our "read up to" marker. If it's
487-
// before, or we can't decide, then we assume the widget will have already seen the event.
488-
// If the event is after, or we don't have a marker for the room, then we'll send it through.
489-
//
490-
// This approach of "read up to" prevents widgets receiving decryption spam from startup or
491-
// receiving out-of-order events from backfill and such.
492-
//
493-
// Skip marker timeline check for events with relations to unknown parent because these
494-
// events are not added to the timeline here and will be ignored otherwise:
495-
// https://github.com/matrix-org/matrix-js-sdk/blob/d3dfcd924201d71b434af3d77343b5229b6ed75e/src/models/room.ts#L2207-L2213
496-
let isRelationToUnknown: boolean | undefined = undefined;
497-
const upToEventId = this.readUpToMap[ev.getRoomId()!];
498-
if (upToEventId) {
499-
// Small optimization for exact match (prevent search)
500-
if (upToEventId === ev.getId()) {
501-
return;
502-
}
484+
/**
485+
* Determines whether the event has a relation to an unknown parent.
486+
*/
487+
private relatesToUnknown(ev: MatrixEvent): boolean {
488+
// Replies to unknown events don't count
489+
if (!ev.relationEventId || ev.replyEventId) return false;
490+
const room = this.client.getRoom(ev.getRoomId());
491+
return room === null || !room.findEventById(ev.relationEventId);
492+
}
503493

504-
// should be true to forward the event to the widget
505-
let shouldForward = false;
506-
507-
const room = this.client.getRoom(ev.getRoomId()!);
508-
if (!room) return;
509-
// Timelines are most recent last, so reverse the order and limit ourselves to 100 events
510-
// to avoid overusing the CPU.
511-
const timeline = room.getLiveTimeline();
512-
const events = arrayFastClone(timeline.getEvents()).reverse().slice(0, 100);
513-
514-
for (const timelineEvent of events) {
515-
if (timelineEvent.getId() === upToEventId) {
516-
break;
517-
} else if (timelineEvent.getId() === ev.getId()) {
518-
shouldForward = true;
519-
break;
520-
}
521-
}
494+
/**
495+
* Determines whether the event comes from a room that we've been invited to
496+
* (in which case we likely don't have the full timeline).
497+
*/
498+
private isFromInvite(ev: MatrixEvent): boolean {
499+
const room = this.client.getRoom(ev.getRoomId());
500+
return room?.getMyMembership() === KnownMembership.Invite;
501+
}
522502

523-
if (!shouldForward) {
524-
// checks that the event has a relation to unknown event
525-
isRelationToUnknown =
526-
!ev.replyEventId && !!ev.relationEventId && !room.findEventById(ev.relationEventId);
527-
if (!isRelationToUnknown) {
528-
// Ignore the event: it is before our interest.
529-
return;
530-
}
531-
}
503+
/**
504+
* Advances the "read up to" marker for a room to a certain event. No-ops if
505+
* the event is before the marker.
506+
* @returns Whether the "read up to" marker was advanced.
507+
*/
508+
private advanceReadUpToMarker(ev: MatrixEvent): boolean {
509+
const evId = ev.getId();
510+
if (evId === undefined) return false;
511+
const roomId = ev.getRoomId();
512+
if (roomId === undefined) return false;
513+
const room = this.client.getRoom(roomId);
514+
if (room === null) return false;
515+
516+
const upToEventId = this.readUpToMap[ev.getRoomId()!];
517+
if (!upToEventId) {
518+
// There's no marker yet; start it at this event
519+
this.readUpToMap[roomId] = evId;
520+
return true;
532521
}
533522

534-
// Skip marker assignment if membership is 'invite', otherwise 'm.room.member' from
535-
// invitation room will assign it and new state events will be not forwarded to the widget
536-
// because of empty timeline for invitation room and assigned marker.
537-
const evRoomId = ev.getRoomId();
538-
const evId = ev.getId();
539-
if (evRoomId && evId) {
540-
const room = this.client.getRoom(evRoomId);
541-
if (room && room.getMyMembership() === KnownMembership.Join && !isRelationToUnknown) {
542-
this.readUpToMap[evRoomId] = evId;
523+
// Small optimization for exact match (skip the search)
524+
if (upToEventId === evId) return false;
525+
526+
// Timelines are most recent last, so reverse the order and limit ourselves to 100 events
527+
// to avoid overusing the CPU.
528+
const timeline = room.getLiveTimeline();
529+
const events = arrayFastClone(timeline.getEvents()).reverse().slice(0, 100);
530+
531+
for (const timelineEvent of events) {
532+
if (timelineEvent.getId() === upToEventId) {
533+
// The event must be somewhere before the "read up to" marker
534+
return false;
535+
} else if (timelineEvent.getId() === ev.getId()) {
536+
// The event is after the marker; advance it
537+
this.readUpToMap[roomId] = evId;
538+
return true;
543539
}
544540
}
545541

546-
const raw = ev.getEffectiveEvent();
547-
this.messaging.feedEvent(raw as IRoomEvent, this.eventListenerRoomId!).catch((e) => {
548-
logger.error("Error sending event to widget: ", e);
549-
});
542+
// We can't say for sure whether the widget has seen the event; let's
543+
// just assume that it has
544+
return false;
545+
}
546+
547+
private feedEvent(ev: MatrixEvent): void {
548+
if (this.messaging === null) return;
549+
if (
550+
// If we had decided earlier to feed this event to the widget, but
551+
// it just wasn't ready, give it another try
552+
this.eventsToFeed.has(ev) ||
553+
// Skip marker timeline check for events with relations to unknown parent because these
554+
// events are not added to the timeline here and will be ignored otherwise:
555+
// https://github.com/matrix-org/matrix-js-sdk/blob/d3dfcd924201d71b434af3d77343b5229b6ed75e/src/models/room.ts#L2207-L2213
556+
this.relatesToUnknown(ev) ||
557+
// Skip marker timeline check for rooms where membership is
558+
// 'invite', otherwise the membership event from the invitation room
559+
// will advance the marker and new state events will not be
560+
// forwarded to the widget.
561+
this.isFromInvite(ev) ||
562+
// Check whether this event would be before or after our "read up to" marker. If it's
563+
// before, or we can't decide, then we assume the widget will have already seen the event.
564+
// If the event is after, or we don't have a marker for the room, then the marker will advance and we'll
565+
// send it through.
566+
// This approach of "read up to" prevents widgets receiving decryption spam from startup or
567+
// receiving ancient events from backfill and such.
568+
this.advanceReadUpToMarker(ev)
569+
) {
570+
// If the event is still being decrypted, remember that we want to
571+
// feed it to the widget (even if not strictly in the order given by
572+
// the timeline) and get back to it later
573+
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) this.eventsToFeed.add(ev);
574+
else {
575+
const raw = ev.getEffectiveEvent();
576+
this.messaging.feedEvent(raw as IRoomEvent, this.eventListenerRoomId!).catch((e) => {
577+
logger.error("Error sending event to widget: ", e);
578+
});
579+
}
580+
}
550581
}
551582
}

0 commit comments

Comments
 (0)