Skip to content

Commit bf6dc16

Browse files
robintowntoger5
andauthored
Allow the embedded client to work without update_state support (#4849)
* Allow the embedded client to work without UpdateState version * Test that RoomWidgetClient can receive state without update_state * add sliding sync test * sliding sync receive test * review * add doc comment --------- Co-authored-by: Timo <[email protected]>
1 parent 9398271 commit bf6dc16

File tree

2 files changed

+169
-7
lines changed

2 files changed

+169
-7
lines changed

spec/unit/embedded.spec.ts

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ import {
3131
type IOpenIDCredentials,
3232
type ISendEventFromWidgetResponseData,
3333
WidgetApiResponseError,
34+
UnstableApiVersion,
35+
type ApiVersion,
36+
type IRoomEvent,
3437
} from "matrix-widget-api";
3538

3639
import { createRoomWidgetClient, MatrixError, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
@@ -40,6 +43,9 @@ import { type ICapabilities, type RoomWidgetClient } from "../../src/embedded";
4043
import { MatrixEvent } from "../../src/models/event";
4144
import { type ToDeviceBatch } from "../../src/models/ToDeviceMessage";
4245
import { sleep } from "../../src/utils";
46+
import { SlidingSync } from "../../src/sliding-sync";
47+
import { logger } from "../../src/logger";
48+
import { flushPromises } from "../test-utils/flushPromises";
4349

4450
const testOIDCToken = {
4551
access_token: "12345678",
@@ -49,6 +55,7 @@ const testOIDCToken = {
4955
};
5056
class MockWidgetApi extends EventEmitter {
5157
public start = jest.fn().mockResolvedValue(undefined);
58+
public getClientVersions = jest.fn();
5259
public requestCapability = jest.fn().mockResolvedValue(undefined);
5360
public requestCapabilities = jest.fn().mockResolvedValue(undefined);
5461
public requestCapabilityForRoomTimeline = jest.fn().mockResolvedValue(undefined);
@@ -96,6 +103,15 @@ class MockWidgetApi extends EventEmitter {
96103
send: jest.fn(),
97104
sendComplete: jest.fn(),
98105
};
106+
107+
/**
108+
* This mocks the widget's view of what is supported by its environment.
109+
* @param clientVersions The versions that the widget believes are supported by the host client's widget driver.
110+
*/
111+
public constructor(clientVersions: ApiVersion[]) {
112+
super();
113+
this.getClientVersions.mockResolvedValue(clientVersions);
114+
}
99115
}
100116

101117
declare module "../../src/types" {
@@ -117,7 +133,7 @@ describe("RoomWidgetClient", () => {
117133
let client: MatrixClient;
118134

119135
beforeEach(() => {
120-
widgetApi = new MockWidgetApi() as unknown as MockedObject<WidgetApi>;
136+
widgetApi = new MockWidgetApi([UnstableApiVersion.MSC2762_UPDATE_STATE]) as unknown as MockedObject<WidgetApi>;
121137
});
122138

123139
afterEach(() => {
@@ -128,6 +144,7 @@ describe("RoomWidgetClient", () => {
128144
capabilities: ICapabilities,
129145
sendContentLoaded: boolean | undefined = undefined,
130146
userId?: string,
147+
useSlidingSync?: boolean,
131148
): Promise<void> => {
132149
const baseUrl = "https://example.org";
133150
client = createRoomWidgetClient(
@@ -139,7 +156,7 @@ describe("RoomWidgetClient", () => {
139156
);
140157
expect(widgetApi.start).toHaveBeenCalled(); // needs to have been called early in order to not miss messages
141158
widgetApi.emit("ready");
142-
await client.startClient();
159+
await client.startClient(useSlidingSync ? { slidingSync: new SlidingSync("", new Map(), {}, client, 0) } : {});
143160
};
144161

145162
describe("events", () => {
@@ -668,10 +685,106 @@ describe("RoomWidgetClient", () => {
668685
detail: { data: { state: [event] } },
669686
}),
670687
);
688+
// Allow the getClientVersions promise to resolve
689+
await flushPromises();
671690
// It should now have changed the room state
672691
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
673692
});
674693

694+
describe("without support for update_state", () => {
695+
beforeEach(() => {
696+
widgetApi = new MockWidgetApi([]) as unknown as MockedObject<WidgetApi>;
697+
});
698+
699+
it("receives", async () => {
700+
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
701+
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
702+
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
703+
704+
const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
705+
const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, resolve));
706+
widgetApi.emit(
707+
`action:${WidgetApiToWidgetAction.SendEvent}`,
708+
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
709+
);
710+
711+
// The client should've emitted about the received event
712+
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
713+
expect(await emittedSync).toEqual(SyncState.Syncing);
714+
// It should've also inserted the event into the room object
715+
const room = client.getRoom("!1:example.org");
716+
expect(room).not.toBeNull();
717+
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
718+
});
719+
720+
it("does not receive with sliding sync (update_state is needed for sliding sync)", async () => {
721+
await makeClient(
722+
{ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] },
723+
undefined,
724+
undefined,
725+
true,
726+
);
727+
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
728+
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
729+
730+
const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
731+
const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, resolve));
732+
const logSpy = jest.spyOn(logger, "error");
733+
widgetApi.emit(
734+
`action:${WidgetApiToWidgetAction.SendEvent}`,
735+
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
736+
);
737+
738+
// The client should've emitted about the received event
739+
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
740+
expect(await emittedSync).toEqual(SyncState.Syncing);
741+
742+
// The incompatibility of sliding sync without update_state to get logged.
743+
expect(logSpy).toHaveBeenCalledWith(
744+
"slididng sync cannot be used in widget mode if the client widget driver does not support the version: 'org.matrix.msc2762_update_state'",
745+
);
746+
// It should not have inserted the event into the room object
747+
const room = client.getRoom("!1:example.org");
748+
expect(room).not.toBeNull();
749+
expect(room!.currentState.getStateEvents("org.example.foo", "bar")).toEqual(null);
750+
});
751+
752+
it("backfills", async () => {
753+
widgetApi.readStateEvents.mockImplementation(async (eventType, limit, stateKey) =>
754+
eventType === "org.example.foo" && (limit ?? Infinity) > 0 && stateKey === "bar"
755+
? [event as IRoomEvent]
756+
: [],
757+
);
758+
759+
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
760+
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
761+
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
762+
763+
const room = client.getRoom("!1:example.org");
764+
expect(room).not.toBeNull();
765+
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
766+
});
767+
it("backfills with sliding sync", async () => {
768+
widgetApi.readStateEvents.mockImplementation(async (eventType, limit, stateKey) =>
769+
eventType === "org.example.foo" && (limit ?? Infinity) > 0 && stateKey === "bar"
770+
? [event as IRoomEvent]
771+
: [],
772+
);
773+
await makeClient(
774+
{ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] },
775+
undefined,
776+
undefined,
777+
true,
778+
);
779+
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
780+
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
781+
782+
const room = client.getRoom("!1:example.org");
783+
expect(room).not.toBeNull();
784+
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
785+
});
786+
});
787+
675788
it("ignores state updates for other rooms", async () => {
676789
const init = makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
677790
// Client needs to be told that the room state is loaded

src/embedded.ts

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
type IWidgetApiResponse,
3030
type IWidgetApiResponseData,
3131
type IUpdateStateToWidgetActionRequest,
32+
UnstableApiVersion,
3233
} from "matrix-widget-api";
3334

3435
import { MatrixEvent, type IEvent, type IContent, EventStatus } from "./models/event.ts";
@@ -259,6 +260,10 @@ export class RoomWidgetClient extends MatrixClient {
259260
if (sendContentLoaded) widgetApi.sendContentLoaded();
260261
}
261262

263+
public async supportUpdateState(): Promise<boolean> {
264+
return (await this.widgetApi.getClientVersions()).includes(UnstableApiVersion.MSC2762_UPDATE_STATE);
265+
}
266+
262267
public async startClient(opts: IStartClientOpts = {}): Promise<void> {
263268
this.lifecycle = new AbortController();
264269

@@ -283,14 +288,41 @@ export class RoomWidgetClient extends MatrixClient {
283288

284289
await this.widgetApiReady;
285290

291+
// sync room state:
292+
if (await this.supportUpdateState()) {
293+
// This will resolve once the client driver has sent us all the allowed room state.
294+
await this.roomStateSynced;
295+
} else {
296+
// Backfill the requested events
297+
// We only get the most recent event for every type + state key combo,
298+
// so it doesn't really matter what order we inject them in
299+
await Promise.all(
300+
this.capabilities.receiveState?.map(async ({ eventType, stateKey }) => {
301+
const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey, [
302+
this.roomId,
303+
]);
304+
const events = rawEvents.map((rawEvent) => new MatrixEvent(rawEvent as Partial<IEvent>));
305+
306+
if (this.syncApi instanceof SyncApi) {
307+
// Passing events as `stateAfterEventList` will update the state.
308+
await this.syncApi.injectRoomEvents(this.room!, undefined, events);
309+
} else {
310+
await this.syncApi!.injectRoomEvents(this.room!, events); // Sliding Sync
311+
}
312+
events.forEach((event) => {
313+
this.emit(ClientEvent.Event, event);
314+
logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
315+
});
316+
}) ?? [],
317+
);
318+
}
319+
286320
if (opts.clientWellKnownPollPeriod !== undefined) {
287321
this.clientWellKnownIntervalID = setInterval(() => {
288322
this.fetchClientWellKnown();
289323
}, 1000 * opts.clientWellKnownPollPeriod);
290324
this.fetchClientWellKnown();
291325
}
292-
293-
await this.roomStateSynced;
294326
this.setSyncState(SyncState.Syncing);
295327
logger.info("Finished initial sync");
296328

@@ -589,11 +621,24 @@ export class RoomWidgetClient extends MatrixClient {
589621
await this.updateTxId(event);
590622

591623
if (this.syncApi instanceof SyncApi) {
592-
await this.syncApi.injectRoomEvents(this.room!, undefined, [], [event]);
624+
if (await this.supportUpdateState()) {
625+
await this.syncApi.injectRoomEvents(this.room!, undefined, [], [event]);
626+
} else {
627+
// Passing undefined for `stateAfterEventList` will make `injectRoomEvents` run in legacy mode
628+
// -> state events in `timelineEventList` will update the state.
629+
await this.syncApi.injectRoomEvents(this.room!, [], undefined, [event]);
630+
}
593631
} else {
594632
// Sliding Sync
595-
await this.syncApi!.injectRoomEvents(this.room!, [], [event]);
633+
if (await this.supportUpdateState()) {
634+
await this.syncApi!.injectRoomEvents(this.room!, [], [event]);
635+
} else {
636+
logger.error(
637+
"slididng sync cannot be used in widget mode if the client widget driver does not support the version: 'org.matrix.msc2762_update_state'",
638+
);
639+
}
596640
}
641+
597642
this.emit(ClientEvent.Event, event);
598643
this.setSyncState(SyncState.Syncing);
599644
logger.info(`Received event ${event.getId()} ${event.getType()}`);
@@ -623,7 +668,11 @@ export class RoomWidgetClient extends MatrixClient {
623668

624669
private onStateUpdate = async (ev: CustomEvent<IUpdateStateToWidgetActionRequest>): Promise<void> => {
625670
ev.preventDefault();
626-
671+
if (!(await this.supportUpdateState())) {
672+
logger.warn(
673+
"received update_state widget action but the widget driver did not claim to support 'org.matrix.msc2762_update_state'",
674+
);
675+
}
627676
for (const rawEvent of ev.detail.data.state) {
628677
// Verify the room ID matches, since it's possible for the client to
629678
// send us state updates from other rooms if this widget is always

0 commit comments

Comments
 (0)