Skip to content

Commit b595396

Browse files
committed
Reapply "Distinguish room state and timeline events in embedded clients (#4574)" (#4656)
This reverts commit fd9a44e.
1 parent fd9a44e commit b595396

File tree

3 files changed

+84
-67
lines changed

3 files changed

+84
-67
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
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

Lines changed: 41 additions & 48 deletions
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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4875,9 +4875,9 @@ matrix-mock-request@^2.5.0:
48754875
expect "^28.1.0"
48764876

48774877
matrix-widget-api@^1.10.0:
4878-
version "1.10.0"
4879-
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.10.0.tgz#d31ea073a5871a1fb1a511ef900b0c125a37bf55"
4880-
integrity sha512-rkAJ29briYV7TJnfBVLVSKtpeBrBju15JZFSDP6wj8YdbCu1bdmlplJayQ+vYaw1x4fzI49Q+Nz3E85s46sRDw==
4878+
version "1.12.0"
4879+
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.12.0.tgz#b3d22bab1670051c8eeee66bb96d08b33148bc99"
4880+
integrity sha512-6JRd9fJGGvuBRhcTg9wX+Skn/Q1wox3jdp5yYQKJ6pPw4urW9bkTR90APBKVDB1vorJKT44jml+lCzkDMRBjww==
48814881
dependencies:
48824882
"@types/events" "^3.0.0"
48834883
events "^3.2.0"

0 commit comments

Comments
 (0)