From 7d8eae7a1e91c9f21f93a69f57715b86cb7b01ba Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 6 Dec 2024 16:12:09 -0500 Subject: [PATCH] Distinguish room state and timeline events in embedded clients --- src/embedded.ts | 87 ++++++++++++++++++++++--------------------------- 1 file changed, 39 insertions(+), 48 deletions(-) diff --git a/src/embedded.ts b/src/embedded.ts index b0cc4c158e8..74bc92173e0 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -28,6 +28,7 @@ import { WidgetApiAction, IWidgetApiResponse, IWidgetApiResponseData, + IUpdateStateToWidgetActionRequest, } from "matrix-widget-api"; import { MatrixEvent, IEvent, IContent, EventStatus } from "./models/event.ts"; @@ -136,6 +137,7 @@ export type EventHandlerMap = { [RoomWidgetClientEvent.PendingEventsChanged]: () export class RoomWidgetClient extends MatrixClient { private room?: Room; private readonly widgetApiReady: Promise; + private readonly roomStateSynced: Promise; private lifecycle?: AbortController; private syncState: SyncState | null = null; @@ -189,6 +191,11 @@ export class RoomWidgetClient extends MatrixClient { }; this.widgetApiReady = new Promise((resolve) => this.widgetApi.once("ready", resolve)); + this.roomStateSynced = capabilities.receiveState?.length + ? new Promise((resolve) => + this.widgetApi.once(`action:${WidgetApiToWidgetAction.RoomStateSynced}`, resolve), + ) + : Promise.resolve(); // Request capabilities for the functionality this client needs to support if ( @@ -241,6 +248,7 @@ export class RoomWidgetClient extends MatrixClient { widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent); widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice); + widgetApi.on(`action:${WidgetApiToWidgetAction.UpdateState}`, this.onStateUpdate); // Open communication with the host widgetApi.start(); @@ -276,28 +284,6 @@ export class RoomWidgetClient extends MatrixClient { await this.widgetApiReady; - // Backfill the requested events - // We only get the most recent event for every type + state key combo, - // so it doesn't really matter what order we inject them in - await Promise.all( - this.capabilities.receiveState?.map(async ({ eventType, stateKey }) => { - const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey, [this.roomId]); - const events = rawEvents.map((rawEvent) => new MatrixEvent(rawEvent as Partial)); - - if (this.syncApi instanceof SyncApi) { - // Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode - // -> state events in `timelineEventList` will update the state. - await this.syncApi.injectRoomEvents(this.room!, undefined, events); - } else { - await this.syncApi!.injectRoomEvents(this.room!, events); // Sliding Sync - } - events.forEach((event) => { - this.emit(ClientEvent.Event, event); - logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`); - }); - }) ?? [], - ); - if (opts.clientWellKnownPollPeriod !== undefined) { this.clientWellKnownIntervalID = setInterval(() => { this.fetchClientWellKnown(); @@ -305,8 +291,9 @@ export class RoomWidgetClient extends MatrixClient { this.fetchClientWellKnown(); } + await this.roomStateSynced; this.setSyncState(SyncState.Syncing); - logger.info("Finished backfilling events"); + logger.info("Finished initial sync"); this.matrixRTC.start(); @@ -317,6 +304,7 @@ export class RoomWidgetClient extends MatrixClient { public stopClient(): void { this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent); this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice); + this.widgetApi.off(`action:${WidgetApiToWidgetAction.UpdateState}`, this.onStateUpdate); super.stopClient(); this.lifecycle!.abort(); // Signal to other async tasks that the client has stopped @@ -574,36 +562,15 @@ export class RoomWidgetClient extends MatrixClient { // Only inject once we have update the txId await this.updateTxId(event); - // The widget API does not tell us whether a state event came from `state_after` or not so we assume legacy behaviour for now. if (this.syncApi instanceof SyncApi) { - // The code will want to be something like: - // ``` - // if (!params.addToTimeline && !params.addToState) { - // // Passing undefined for `stateAfterEventList` makes `injectRoomEvents` run in "legacy mode" - // // -> state events part of the `timelineEventList` parameter will update the state. - // this.injectRoomEvents(this.room!, [], undefined, [event]); - // } else { - // this.injectRoomEvents(this.room!, undefined, params.addToState ? [event] : [], params.addToTimeline ? [event] : []); - // } - // ``` - - // Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode - // -> state events in `timelineEventList` will update the state. - await this.syncApi.injectRoomEvents(this.room!, [], undefined, [event]); + this.syncApi.injectRoomEvents(this.room!, undefined, [], [event]); } else { - // The code will want to be something like: - // ``` - // if (!params.addToTimeline && !params.addToState) { - // this.injectRoomEvents(this.room!, [], [event]); - // } else { - // this.injectRoomEvents(this.room!, params.addToState ? [event] : [], params.addToTimeline ? [event] : []); - // } - // ``` - await this.syncApi!.injectRoomEvents(this.room!, [], [event]); // Sliding Sync + // Sliding Sync + this.syncApi!.injectRoomEvents(this.room!, [], [event]); } this.emit(ClientEvent.Event, event); this.setSyncState(SyncState.Syncing); - logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`); + logger.info(`Received event ${event.getId()} ${event.getType()}`); } else { const { event_id: eventId, room_id: roomId } = ev.detail.data; logger.info(`Received event ${eventId} for a different room ${roomId}; discarding`); @@ -628,6 +595,30 @@ export class RoomWidgetClient extends MatrixClient { await this.ack(ev); }; + private onStateUpdate = async (ev: CustomEvent): Promise => { + ev.preventDefault(); + + // Verify the room ID matches, since it's possible for the client to + // send us state updates from other rooms if this widget is always on + // screen + if (ev.detail.data.room_id === this.roomId) { + const event = new MatrixEvent(ev.detail.data as Partial); + + if (this.syncApi instanceof SyncApi) { + this.syncApi.injectRoomEvents(this.room!, undefined, [event]); + } else { + // Sliding Sync + this.syncApi!.injectRoomEvents(this.room!, [event]); + } + logger.info(`Updated state entry ${event.getType()} ${event.getStateKey()} to ${event.getId()}`); + } else { + const { event_id: eventId, room_id: roomId } = ev.detail.data; + logger.info(`Received state entry ${eventId} for a different room ${roomId}; discarding`); + } + + await this.ack(ev); + }; + private async watchTurnServers(): Promise { const servers = this.widgetApi.getTurnServers(); const onClientStopped = (): void => {