Skip to content

Commit bdd4d82

Browse files
authored
Distinguish room state and timeline events in embedded clients (#4574)
* Distinguish room state and timeline events in embedded clients This change enables room widget clients to take advantage of the more reliable method of communicating room state over the widget API provided by a recent update to MSC2762. * Add missing awaits * Upgrade matrix-widget-api
1 parent 5babcaf commit bdd4d82

File tree

3 files changed

+84
-67
lines changed

3 files changed

+84
-67
lines changed

spec/unit/embedded.spec.ts

+40-16
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import {
2828
WidgetApiToWidgetAction,
2929
MatrixCapabilities,
3030
ITurnServer,
31-
IRoomEvent,
3231
IOpenIDCredentials,
3332
ISendEventFromWidgetResponseData,
3433
WidgetApiResponseError,
@@ -635,12 +634,20 @@ describe("RoomWidgetClient", () => {
635634
});
636635

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

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

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

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

src/embedded.ts

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

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

@@ -189,6 +191,11 @@ export class RoomWidgetClient extends MatrixClient {
189191
};
190192

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

193200
// Request capabilities for the functionality this client needs to support
194201
if (
@@ -241,6 +248,7 @@ export class RoomWidgetClient extends MatrixClient {
241248

242249
widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
243250
widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
251+
widgetApi.on(`action:${WidgetApiToWidgetAction.UpdateState}`, this.onStateUpdate);
244252

245253
// Open communication with the host
246254
widgetApi.start();
@@ -276,37 +284,16 @@ export class RoomWidgetClient extends MatrixClient {
276284

277285
await this.widgetApiReady;
278286

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

294+
await this.roomStateSynced;
308295
this.setSyncState(SyncState.Syncing);
309-
logger.info("Finished backfilling events");
296+
logger.info("Finished initial sync");
310297

311298
this.matrixRTC.start();
312299

@@ -317,6 +304,7 @@ export class RoomWidgetClient extends MatrixClient {
317304
public stopClient(): void {
318305
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
319306
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
307+
this.widgetApi.off(`action:${WidgetApiToWidgetAction.UpdateState}`, this.onStateUpdate);
320308

321309
super.stopClient();
322310
this.lifecycle!.abort(); // Signal to other async tasks that the client has stopped
@@ -574,36 +562,15 @@ export class RoomWidgetClient extends MatrixClient {
574562
// Only inject once we have update the txId
575563
await this.updateTxId(event);
576564

577-
// The widget API does not tell us whether a state event came from `state_after` or not so we assume legacy behaviour for now.
578565
if (this.syncApi instanceof SyncApi) {
579-
// The code will want to be something like:
580-
// ```
581-
// if (!params.addToTimeline && !params.addToState) {
582-
// // Passing undefined for `stateAfterEventList` makes `injectRoomEvents` run in "legacy mode"
583-
// // -> state events part of the `timelineEventList` parameter will update the state.
584-
// this.injectRoomEvents(this.room!, [], undefined, [event]);
585-
// } else {
586-
// this.injectRoomEvents(this.room!, undefined, params.addToState ? [event] : [], params.addToTimeline ? [event] : []);
587-
// }
588-
// ```
589-
590-
// Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode
591-
// -> state events in `timelineEventList` will update the state.
592-
await this.syncApi.injectRoomEvents(this.room!, [], undefined, [event]);
566+
await this.syncApi.injectRoomEvents(this.room!, undefined, [], [event]);
593567
} else {
594-
// The code will want to be something like:
595-
// ```
596-
// if (!params.addToTimeline && !params.addToState) {
597-
// this.injectRoomEvents(this.room!, [], [event]);
598-
// } else {
599-
// this.injectRoomEvents(this.room!, params.addToState ? [event] : [], params.addToTimeline ? [event] : []);
600-
// }
601-
// ```
602-
await this.syncApi!.injectRoomEvents(this.room!, [], [event]); // Sliding Sync
568+
// Sliding Sync
569+
await this.syncApi!.injectRoomEvents(this.room!, [], [event]);
603570
}
604571
this.emit(ClientEvent.Event, event);
605572
this.setSyncState(SyncState.Syncing);
606-
logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
573+
logger.info(`Received event ${event.getId()} ${event.getType()}`);
607574
} else {
608575
const { event_id: eventId, room_id: roomId } = ev.detail.data;
609576
logger.info(`Received event ${eventId} for a different room ${roomId}; discarding`);
@@ -628,6 +595,32 @@ export class RoomWidgetClient extends MatrixClient {
628595
await this.ack(ev);
629596
};
630597

598+
private onStateUpdate = async (ev: CustomEvent<IUpdateStateToWidgetActionRequest>): Promise<void> => {
599+
ev.preventDefault();
600+
601+
for (const rawEvent of ev.detail.data.state) {
602+
// Verify the room ID matches, since it's possible for the client to
603+
// send us state updates from other rooms if this widget is always
604+
// on screen
605+
if (rawEvent.room_id === this.roomId) {
606+
const event = new MatrixEvent(rawEvent as Partial<IEvent>);
607+
608+
if (this.syncApi instanceof SyncApi) {
609+
await this.syncApi.injectRoomEvents(this.room!, undefined, [event]);
610+
} else {
611+
// Sliding Sync
612+
await this.syncApi!.injectRoomEvents(this.room!, [event]);
613+
}
614+
logger.info(`Updated state entry ${event.getType()} ${event.getStateKey()} to ${event.getId()}`);
615+
} else {
616+
const { event_id: eventId, room_id: roomId } = ev.detail.data;
617+
logger.info(`Received state entry ${eventId} for a different room ${roomId}; discarding`);
618+
}
619+
}
620+
621+
await this.ack(ev);
622+
};
623+
631624
private async watchTurnServers(): Promise<void> {
632625
const servers = this.widgetApi.getTurnServers();
633626
const onClientStopped = (): void => {

yarn.lock

+3-3
Original file line numberDiff line numberDiff line change
@@ -4854,9 +4854,9 @@ matrix-mock-request@^2.5.0:
48544854
expect "^28.1.0"
48554855

48564856
matrix-widget-api@^1.10.0:
4857-
version "1.10.0"
4858-
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.10.0.tgz#d31ea073a5871a1fb1a511ef900b0c125a37bf55"
4859-
integrity sha512-rkAJ29briYV7TJnfBVLVSKtpeBrBju15JZFSDP6wj8YdbCu1bdmlplJayQ+vYaw1x4fzI49Q+Nz3E85s46sRDw==
4857+
version "1.12.0"
4858+
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.12.0.tgz#b3d22bab1670051c8eeee66bb96d08b33148bc99"
4859+
integrity sha512-6JRd9fJGGvuBRhcTg9wX+Skn/Q1wox3jdp5yYQKJ6pPw4urW9bkTR90APBKVDB1vorJKT44jml+lCzkDMRBjww==
48604860
dependencies:
48614861
"@types/events" "^3.0.0"
48624862
events "^3.2.0"

0 commit comments

Comments
 (0)