Skip to content

Commit 6f5481e

Browse files
committed
Reapply "Distinguish room state and timeline events in embedded clients"
This reverts commit fd9a44e.
1 parent eb793aa commit 6f5481e

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";
@@ -146,6 +147,7 @@ export type EventHandlerMap = { [RoomWidgetClientEvent.PendingEventsChanged]: ()
146147
export class RoomWidgetClient extends MatrixClient {
147148
private room?: Room;
148149
private readonly widgetApiReady: Promise<void>;
150+
private readonly roomStateSynced: Promise<void>;
149151
private lifecycle?: AbortController;
150152
private syncState: SyncState | null = null;
151153

@@ -199,6 +201,11 @@ export class RoomWidgetClient extends MatrixClient {
199201
};
200202

201203
this.widgetApiReady = new Promise<void>((resolve) => this.widgetApi.once("ready", resolve));
204+
this.roomStateSynced = capabilities.receiveState?.length
205+
? new Promise<void>((resolve) =>
206+
this.widgetApi.once(`action:${WidgetApiToWidgetAction.UpdateState}`, resolve),
207+
)
208+
: Promise.resolve();
202209

203210
// Request capabilities for the functionality this client needs to support
204211
if (
@@ -251,6 +258,7 @@ export class RoomWidgetClient extends MatrixClient {
251258

252259
widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
253260
widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
261+
widgetApi.on(`action:${WidgetApiToWidgetAction.UpdateState}`, this.onStateUpdate);
254262

255263
// Open communication with the host
256264
widgetApi.start();
@@ -286,37 +294,16 @@ export class RoomWidgetClient extends MatrixClient {
286294

287295
await this.widgetApiReady;
288296

289-
// Backfill the requested events
290-
// We only get the most recent event for every type + state key combo,
291-
// so it doesn't really matter what order we inject them in
292-
await Promise.all(
293-
this.capabilities.receiveState?.map(async ({ eventType, stateKey }) => {
294-
const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey, [this.roomId]);
295-
const events = rawEvents.map((rawEvent) => new MatrixEvent(rawEvent as Partial<IEvent>));
296-
297-
if (this.syncApi instanceof SyncApi) {
298-
// Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode
299-
// -> state events in `timelineEventList` will update the state.
300-
await this.syncApi.injectRoomEvents(this.room!, undefined, events);
301-
} else {
302-
await this.syncApi!.injectRoomEvents(this.room!, events); // Sliding Sync
303-
}
304-
events.forEach((event) => {
305-
this.emit(ClientEvent.Event, event);
306-
logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
307-
});
308-
}) ?? [],
309-
);
310-
311297
if (opts.clientWellKnownPollPeriod !== undefined) {
312298
this.clientWellKnownIntervalID = setInterval(() => {
313299
this.fetchClientWellKnown();
314300
}, 1000 * opts.clientWellKnownPollPeriod);
315301
this.fetchClientWellKnown();
316302
}
317303

304+
await this.roomStateSynced;
318305
this.setSyncState(SyncState.Syncing);
319-
logger.info("Finished backfilling events");
306+
logger.info("Finished initial sync");
320307

321308
this.matrixRTC.start();
322309

@@ -327,6 +314,7 @@ export class RoomWidgetClient extends MatrixClient {
327314
public stopClient(): void {
328315
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
329316
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
317+
this.widgetApi.off(`action:${WidgetApiToWidgetAction.UpdateState}`, this.onStateUpdate);
330318

331319
super.stopClient();
332320
this.lifecycle!.abort(); // Signal to other async tasks that the client has stopped
@@ -604,36 +592,15 @@ export class RoomWidgetClient extends MatrixClient {
604592
// Only inject once we have update the txId
605593
await this.updateTxId(event);
606594

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

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

0 commit comments

Comments
 (0)