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 <toger5@hotmail.de>
This commit is contained in:
+115
-2
@@ -31,6 +31,9 @@ import {
|
||||
type IOpenIDCredentials,
|
||||
type ISendEventFromWidgetResponseData,
|
||||
WidgetApiResponseError,
|
||||
UnstableApiVersion,
|
||||
type ApiVersion,
|
||||
type IRoomEvent,
|
||||
} from "matrix-widget-api";
|
||||
|
||||
import { createRoomWidgetClient, MatrixError, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
|
||||
@@ -40,6 +43,9 @@ import { type ICapabilities, type RoomWidgetClient } from "../../src/embedded";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { type ToDeviceBatch } from "../../src/models/ToDeviceMessage";
|
||||
import { sleep } from "../../src/utils";
|
||||
import { SlidingSync } from "../../src/sliding-sync";
|
||||
import { logger } from "../../src/logger";
|
||||
import { flushPromises } from "../test-utils/flushPromises";
|
||||
|
||||
const testOIDCToken = {
|
||||
access_token: "12345678",
|
||||
@@ -49,6 +55,7 @@ const testOIDCToken = {
|
||||
};
|
||||
class MockWidgetApi extends EventEmitter {
|
||||
public start = jest.fn().mockResolvedValue(undefined);
|
||||
public getClientVersions = jest.fn();
|
||||
public requestCapability = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilities = jest.fn().mockResolvedValue(undefined);
|
||||
public requestCapabilityForRoomTimeline = jest.fn().mockResolvedValue(undefined);
|
||||
@@ -96,6 +103,15 @@ class MockWidgetApi extends EventEmitter {
|
||||
send: jest.fn(),
|
||||
sendComplete: jest.fn(),
|
||||
};
|
||||
|
||||
/**
|
||||
* This mocks the widget's view of what is supported by its environment.
|
||||
* @param clientVersions The versions that the widget believes are supported by the host client's widget driver.
|
||||
*/
|
||||
public constructor(clientVersions: ApiVersion[]) {
|
||||
super();
|
||||
this.getClientVersions.mockResolvedValue(clientVersions);
|
||||
}
|
||||
}
|
||||
|
||||
declare module "../../src/types" {
|
||||
@@ -117,7 +133,7 @@ describe("RoomWidgetClient", () => {
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
widgetApi = new MockWidgetApi() as unknown as MockedObject<WidgetApi>;
|
||||
widgetApi = new MockWidgetApi([UnstableApiVersion.MSC2762_UPDATE_STATE]) as unknown as MockedObject<WidgetApi>;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -128,6 +144,7 @@ describe("RoomWidgetClient", () => {
|
||||
capabilities: ICapabilities,
|
||||
sendContentLoaded: boolean | undefined = undefined,
|
||||
userId?: string,
|
||||
useSlidingSync?: boolean,
|
||||
): Promise<void> => {
|
||||
const baseUrl = "https://example.org";
|
||||
client = createRoomWidgetClient(
|
||||
@@ -139,7 +156,7 @@ describe("RoomWidgetClient", () => {
|
||||
);
|
||||
expect(widgetApi.start).toHaveBeenCalled(); // needs to have been called early in order to not miss messages
|
||||
widgetApi.emit("ready");
|
||||
await client.startClient();
|
||||
await client.startClient(useSlidingSync ? { slidingSync: new SlidingSync("", new Map(), {}, client, 0) } : {});
|
||||
};
|
||||
|
||||
describe("events", () => {
|
||||
@@ -668,10 +685,106 @@ describe("RoomWidgetClient", () => {
|
||||
detail: { data: { state: [event] } },
|
||||
}),
|
||||
);
|
||||
// Allow the getClientVersions promise to resolve
|
||||
await flushPromises();
|
||||
// It should now have changed the room state
|
||||
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
|
||||
});
|
||||
|
||||
describe("without support for update_state", () => {
|
||||
beforeEach(() => {
|
||||
widgetApi = new MockWidgetApi([]) as unknown as MockedObject<WidgetApi>;
|
||||
});
|
||||
|
||||
it("receives", async () => {
|
||||
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
|
||||
|
||||
const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
|
||||
const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, resolve));
|
||||
widgetApi.emit(
|
||||
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
|
||||
);
|
||||
|
||||
// The client should've emitted about the received event
|
||||
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
|
||||
expect(await emittedSync).toEqual(SyncState.Syncing);
|
||||
// It should've also inserted the event into the room object
|
||||
const room = client.getRoom("!1:example.org");
|
||||
expect(room).not.toBeNull();
|
||||
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
|
||||
});
|
||||
|
||||
it("does not receive with sliding sync (update_state is needed for sliding sync)", async () => {
|
||||
await makeClient(
|
||||
{ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] },
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
|
||||
|
||||
const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
|
||||
const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, resolve));
|
||||
const logSpy = jest.spyOn(logger, "error");
|
||||
widgetApi.emit(
|
||||
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
|
||||
);
|
||||
|
||||
// The client should've emitted about the received event
|
||||
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
|
||||
expect(await emittedSync).toEqual(SyncState.Syncing);
|
||||
|
||||
// The incompatibility of sliding sync without update_state to get logged.
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
"slididng sync cannot be used in widget mode if the client widget driver does not support the version: 'org.matrix.msc2762_update_state'",
|
||||
);
|
||||
// It should not have inserted the event into the room object
|
||||
const room = client.getRoom("!1:example.org");
|
||||
expect(room).not.toBeNull();
|
||||
expect(room!.currentState.getStateEvents("org.example.foo", "bar")).toEqual(null);
|
||||
});
|
||||
|
||||
it("backfills", async () => {
|
||||
widgetApi.readStateEvents.mockImplementation(async (eventType, limit, stateKey) =>
|
||||
eventType === "org.example.foo" && (limit ?? Infinity) > 0 && stateKey === "bar"
|
||||
? [event as IRoomEvent]
|
||||
: [],
|
||||
);
|
||||
|
||||
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
|
||||
|
||||
const room = client.getRoom("!1:example.org");
|
||||
expect(room).not.toBeNull();
|
||||
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
|
||||
});
|
||||
it("backfills with sliding sync", async () => {
|
||||
widgetApi.readStateEvents.mockImplementation(async (eventType, limit, stateKey) =>
|
||||
eventType === "org.example.foo" && (limit ?? Infinity) > 0 && stateKey === "bar"
|
||||
? [event as IRoomEvent]
|
||||
: [],
|
||||
);
|
||||
await makeClient(
|
||||
{ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] },
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
|
||||
|
||||
const room = client.getRoom("!1:example.org");
|
||||
expect(room).not.toBeNull();
|
||||
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores state updates for other rooms", async () => {
|
||||
const init = makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
||||
// Client needs to be told that the room state is loaded
|
||||
|
||||
+54
-5
@@ -29,6 +29,7 @@ import {
|
||||
type IWidgetApiResponse,
|
||||
type IWidgetApiResponseData,
|
||||
type IUpdateStateToWidgetActionRequest,
|
||||
UnstableApiVersion,
|
||||
} from "matrix-widget-api";
|
||||
|
||||
import { MatrixEvent, type IEvent, type IContent, EventStatus } from "./models/event.ts";
|
||||
@@ -259,6 +260,10 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
if (sendContentLoaded) widgetApi.sendContentLoaded();
|
||||
}
|
||||
|
||||
public async supportUpdateState(): Promise<boolean> {
|
||||
return (await this.widgetApi.getClientVersions()).includes(UnstableApiVersion.MSC2762_UPDATE_STATE);
|
||||
}
|
||||
|
||||
public async startClient(opts: IStartClientOpts = {}): Promise<void> {
|
||||
this.lifecycle = new AbortController();
|
||||
|
||||
@@ -283,14 +288,41 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
|
||||
await this.widgetApiReady;
|
||||
|
||||
// sync room state:
|
||||
if (await this.supportUpdateState()) {
|
||||
// This will resolve once the client driver has sent us all the allowed room state.
|
||||
await this.roomStateSynced;
|
||||
} else {
|
||||
// Backfill the requested events
|
||||
// We only get the most recent event for every type + state key combo,
|
||||
// so it doesn't really matter what order we inject them in
|
||||
await Promise.all(
|
||||
this.capabilities.receiveState?.map(async ({ eventType, stateKey }) => {
|
||||
const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey, [
|
||||
this.roomId,
|
||||
]);
|
||||
const events = rawEvents.map((rawEvent) => new MatrixEvent(rawEvent as Partial<IEvent>));
|
||||
|
||||
if (this.syncApi instanceof SyncApi) {
|
||||
// Passing events as `stateAfterEventList` will update the state.
|
||||
await this.syncApi.injectRoomEvents(this.room!, undefined, events);
|
||||
} else {
|
||||
await this.syncApi!.injectRoomEvents(this.room!, events); // Sliding Sync
|
||||
}
|
||||
events.forEach((event) => {
|
||||
this.emit(ClientEvent.Event, event);
|
||||
logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
|
||||
});
|
||||
}) ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
if (opts.clientWellKnownPollPeriod !== undefined) {
|
||||
this.clientWellKnownIntervalID = setInterval(() => {
|
||||
this.fetchClientWellKnown();
|
||||
}, 1000 * opts.clientWellKnownPollPeriod);
|
||||
this.fetchClientWellKnown();
|
||||
}
|
||||
|
||||
await this.roomStateSynced;
|
||||
this.setSyncState(SyncState.Syncing);
|
||||
logger.info("Finished initial sync");
|
||||
|
||||
@@ -589,11 +621,24 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
await this.updateTxId(event);
|
||||
|
||||
if (this.syncApi instanceof SyncApi) {
|
||||
await this.syncApi.injectRoomEvents(this.room!, undefined, [], [event]);
|
||||
if (await this.supportUpdateState()) {
|
||||
await this.syncApi.injectRoomEvents(this.room!, undefined, [], [event]);
|
||||
} else {
|
||||
// Passing undefined for `stateAfterEventList` will make `injectRoomEvents` run in legacy mode
|
||||
// -> state events in `timelineEventList` will update the state.
|
||||
await this.syncApi.injectRoomEvents(this.room!, [], undefined, [event]);
|
||||
}
|
||||
} else {
|
||||
// Sliding Sync
|
||||
await this.syncApi!.injectRoomEvents(this.room!, [], [event]);
|
||||
if (await this.supportUpdateState()) {
|
||||
await this.syncApi!.injectRoomEvents(this.room!, [], [event]);
|
||||
} else {
|
||||
logger.error(
|
||||
"slididng sync cannot be used in widget mode if the client widget driver does not support the version: 'org.matrix.msc2762_update_state'",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(ClientEvent.Event, event);
|
||||
this.setSyncState(SyncState.Syncing);
|
||||
logger.info(`Received event ${event.getId()} ${event.getType()}`);
|
||||
@@ -623,7 +668,11 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
|
||||
private onStateUpdate = async (ev: CustomEvent<IUpdateStateToWidgetActionRequest>): Promise<void> => {
|
||||
ev.preventDefault();
|
||||
|
||||
if (!(await this.supportUpdateState())) {
|
||||
logger.warn(
|
||||
"received update_state widget action but the widget driver did not claim to support 'org.matrix.msc2762_update_state'",
|
||||
);
|
||||
}
|
||||
for (const rawEvent of ev.detail.data.state) {
|
||||
// Verify the room ID matches, since it's possible for the client to
|
||||
// send us state updates from other rooms if this widget is always
|
||||
|
||||
Reference in New Issue
Block a user