Skip to content

Commit b8903dd

Browse files
authored
Reapply "Distinguish room state and timeline events in embedded clients" (#4790)
This reverts commit fd9a44e. We are ready to reintroduce support for the `update_state` widget action (matrix-org/matrix-spec-proposals#4237) now that matrix-rust-sdk is about to gain support for it as well.
1 parent c35c7d1 commit b8903dd

File tree

2 files changed

+81
-64
lines changed

2 files changed

+81
-64
lines changed

spec/unit/embedded.spec.ts

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import {
2828
WidgetApiToWidgetAction,
2929
MatrixCapabilities,
3030
type ITurnServer,
31-
type IRoomEvent,
3231
type IOpenIDCredentials,
3332
type ISendEventFromWidgetResponseData,
3433
WidgetApiResponseError,
@@ -634,12 +633,20 @@ describe("RoomWidgetClient", () => {
634633
});
635634

636635
it("receives", async () => {
637-
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
636+
const init = makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
638637
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
639638
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
639+
// Client needs to be told that the room state is loaded
640+
widgetApi.emit(
641+
`action:${WidgetApiToWidgetAction.UpdateState}`,
642+
new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, { detail: { data: { state: [] } } }),
643+
);
644+
await init;
640645

641646
const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
642647
const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, resolve));
648+
// Let's assume that a state event comes in but it doesn't actually
649+
// update the state of the room just yet (maybe it's unauthorized)
643650
widgetApi.emit(
644651
`action:${WidgetApiToWidgetAction.SendEvent}`,
645652
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
@@ -648,26 +655,43 @@ describe("RoomWidgetClient", () => {
648655
// The client should've emitted about the received event
649656
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
650657
expect(await emittedSync).toEqual(SyncState.Syncing);
651-
// It should've also inserted the event into the room object
658+
// However it should not have changed the room state
652659
const room = client.getRoom("!1:example.org");
653-
expect(room).not.toBeNull();
660+
expect(room!.currentState.getStateEvents("org.example.foo", "bar")).toBe(null);
661+
662+
// Now assume that the state event becomes favored by state
663+
// resolution for whatever reason and enters into the current state
664+
// of the room
665+
widgetApi.emit(
666+
`action:${WidgetApiToWidgetAction.UpdateState}`,
667+
new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, {
668+
detail: { data: { state: [event] } },
669+
}),
670+
);
671+
// It should now have changed the room state
654672
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
655673
});
656674

657-
it("backfills", async () => {
658-
widgetApi.readStateEvents.mockImplementation(async (eventType, limit, stateKey) =>
659-
eventType === "org.example.foo" && (limit ?? Infinity) > 0 && stateKey === "bar"
660-
? [event as IRoomEvent]
661-
: [],
675+
it("ignores state updates for other rooms", async () => {
676+
const init = makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
677+
// Client needs to be told that the room state is loaded
678+
widgetApi.emit(
679+
`action:${WidgetApiToWidgetAction.UpdateState}`,
680+
new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, { detail: { data: { state: [] } } }),
662681
);
682+
await init;
663683

664-
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
665-
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
666-
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
667-
668-
const room = client.getRoom("!1:example.org");
669-
expect(room).not.toBeNull();
670-
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
684+
// Now a room we're not interested in receives a state update
685+
widgetApi.emit(
686+
`action:${WidgetApiToWidgetAction.UpdateState}`,
687+
new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, {
688+
detail: { data: { state: [{ ...event, room_id: "!other-room:example.org" }] } },
689+
}),
690+
);
691+
// No change to the room state
692+
for (const room of client.getRooms()) {
693+
expect(room.currentState.getStateEvents("org.example.foo", "bar")).toBe(null);
694+
}
671695
});
672696
});
673697

src/embedded.ts

Lines changed: 41 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
type WidgetApiAction,
2929
type IWidgetApiResponse,
3030
type IWidgetApiResponseData,
31+
type IUpdateStateToWidgetActionRequest,
3132
} from "matrix-widget-api";
3233

3334
import { MatrixEvent, type IEvent, type IContent, EventStatus } from "./models/event.ts";
@@ -135,6 +136,7 @@ export type EventHandlerMap = { [RoomWidgetClientEvent.PendingEventsChanged]: ()
135136
export class RoomWidgetClient extends MatrixClient {
136137
private room?: Room;
137138
private readonly widgetApiReady: Promise<void>;
139+
private readonly roomStateSynced: Promise<void>;
138140
private lifecycle?: AbortController;
139141
private syncState: SyncState | null = null;
140142

@@ -188,6 +190,11 @@ export class RoomWidgetClient extends MatrixClient {
188190
};
189191

190192
this.widgetApiReady = new Promise<void>((resolve) => this.widgetApi.once("ready", resolve));
193+
this.roomStateSynced = capabilities.receiveState?.length
194+
? new Promise<void>((resolve) =>
195+
this.widgetApi.once(`action:${WidgetApiToWidgetAction.UpdateState}`, resolve),
196+
)
197+
: Promise.resolve();
191198

192199
// Request capabilities for the functionality this client needs to support
193200
if (
@@ -240,6 +247,7 @@ export class RoomWidgetClient extends MatrixClient {
240247

241248
widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
242249
widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
250+
widgetApi.on(`action:${WidgetApiToWidgetAction.UpdateState}`, this.onStateUpdate);
243251

244252
// Open communication with the host
245253
widgetApi.start();
@@ -275,37 +283,16 @@ export class RoomWidgetClient extends MatrixClient {
275283

276284
await this.widgetApiReady;
277285

278-
// Backfill the requested events
279-
// We only get the most recent event for every type + state key combo,
280-
// so it doesn't really matter what order we inject them in
281-
await Promise.all(
282-
this.capabilities.receiveState?.map(async ({ eventType, stateKey }) => {
283-
const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey, [this.roomId]);
284-
const events = rawEvents.map((rawEvent) => new MatrixEvent(rawEvent as Partial<IEvent>));
285-
286-
if (this.syncApi instanceof SyncApi) {
287-
// Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode
288-
// -> state events in `timelineEventList` will update the state.
289-
await this.syncApi.injectRoomEvents(this.room!, undefined, events);
290-
} else {
291-
await this.syncApi!.injectRoomEvents(this.room!, events); // Sliding Sync
292-
}
293-
events.forEach((event) => {
294-
this.emit(ClientEvent.Event, event);
295-
logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
296-
});
297-
}) ?? [],
298-
);
299-
300286
if (opts.clientWellKnownPollPeriod !== undefined) {
301287
this.clientWellKnownIntervalID = setInterval(() => {
302288
this.fetchClientWellKnown();
303289
}, 1000 * opts.clientWellKnownPollPeriod);
304290
this.fetchClientWellKnown();
305291
}
306292

293+
await this.roomStateSynced;
307294
this.setSyncState(SyncState.Syncing);
308-
logger.info("Finished backfilling events");
295+
logger.info("Finished initial sync");
309296

310297
this.matrixRTC.start();
311298

@@ -316,6 +303,7 @@ export class RoomWidgetClient extends MatrixClient {
316303
public stopClient(): void {
317304
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
318305
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
306+
this.widgetApi.off(`action:${WidgetApiToWidgetAction.UpdateState}`, this.onStateUpdate);
319307

320308
super.stopClient();
321309
this.lifecycle!.abort(); // Signal to other async tasks that the client has stopped
@@ -600,36 +588,15 @@ export class RoomWidgetClient extends MatrixClient {
600588
// Only inject once we have update the txId
601589
await this.updateTxId(event);
602590

603-
// The widget API does not tell us whether a state event came from `state_after` or not so we assume legacy behaviour for now.
604591
if (this.syncApi instanceof SyncApi) {
605-
// The code will want to be something like:
606-
// ```
607-
// if (!params.addToTimeline && !params.addToState) {
608-
// // Passing undefined for `stateAfterEventList` makes `injectRoomEvents` run in "legacy mode"
609-
// // -> state events part of the `timelineEventList` parameter will update the state.
610-
// this.injectRoomEvents(this.room!, [], undefined, [event]);
611-
// } else {
612-
// this.injectRoomEvents(this.room!, undefined, params.addToState ? [event] : [], params.addToTimeline ? [event] : []);
613-
// }
614-
// ```
615-
616-
// Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode
617-
// -> state events in `timelineEventList` will update the state.
618-
await this.syncApi.injectRoomEvents(this.room!, [], undefined, [event]);
592+
await this.syncApi.injectRoomEvents(this.room!, undefined, [], [event]);
619593
} else {
620-
// The code will want to be something like:
621-
// ```
622-
// if (!params.addToTimeline && !params.addToState) {
623-
// this.injectRoomEvents(this.room!, [], [event]);
624-
// } else {
625-
// this.injectRoomEvents(this.room!, params.addToState ? [event] : [], params.addToTimeline ? [event] : []);
626-
// }
627-
// ```
628-
await this.syncApi!.injectRoomEvents(this.room!, [], [event]); // Sliding Sync
594+
// Sliding Sync
595+
await this.syncApi!.injectRoomEvents(this.room!, [], [event]);
629596
}
630597
this.emit(ClientEvent.Event, event);
631598
this.setSyncState(SyncState.Syncing);
632-
logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
599+
logger.info(`Received event ${event.getId()} ${event.getType()}`);
633600
} else {
634601
const { event_id: eventId, room_id: roomId } = ev.detail.data;
635602
logger.info(`Received event ${eventId} for a different room ${roomId}; discarding`);
@@ -654,6 +621,32 @@ export class RoomWidgetClient extends MatrixClient {
654621
await this.ack(ev);
655622
};
656623

624+
private onStateUpdate = async (ev: CustomEvent<IUpdateStateToWidgetActionRequest>): Promise<void> => {
625+
ev.preventDefault();
626+
627+
for (const rawEvent of ev.detail.data.state) {
628+
// Verify the room ID matches, since it's possible for the client to
629+
// send us state updates from other rooms if this widget is always
630+
// on screen
631+
if (rawEvent.room_id === this.roomId) {
632+
const event = new MatrixEvent(rawEvent as Partial<IEvent>);
633+
634+
if (this.syncApi instanceof SyncApi) {
635+
await this.syncApi.injectRoomEvents(this.room!, undefined, [event]);
636+
} else {
637+
// Sliding Sync
638+
await this.syncApi!.injectRoomEvents(this.room!, [event]);
639+
}
640+
logger.info(`Updated state entry ${event.getType()} ${event.getStateKey()} to ${event.getId()}`);
641+
} else {
642+
const { event_id: eventId, room_id: roomId } = ev.detail.data;
643+
logger.info(`Received state entry ${eventId} for a different room ${roomId}; discarding`);
644+
}
645+
}
646+
647+
await this.ack(ev);
648+
};
649+
657650
private async watchTurnServers(): Promise<void> {
658651
const servers = this.widgetApi.getTurnServers();
659652
const onClientStopped = (): void => {

0 commit comments

Comments
 (0)