Skip to content

Commit

Permalink
Distinguish room state and timeline events in embedded clients
Browse files Browse the repository at this point in the history
  • Loading branch information
robintown committed Dec 17, 2024
1 parent cf39595 commit a1eac0c
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 66 deletions.
38 changes: 20 additions & 18 deletions spec/unit/embedded.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
WidgetApiToWidgetAction,
MatrixCapabilities,
ITurnServer,
IRoomEvent,
IOpenIDCredentials,
ISendEventFromWidgetResponseData,
WidgetApiResponseError,
Expand Down Expand Up @@ -635,12 +634,20 @@ describe("RoomWidgetClient", () => {
});

it("receives", async () => {
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
const init = makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
// Client needs to be told that the room state is loaded
widgetApi.emit(
`action:${WidgetApiToWidgetAction.UpdateState}`,

Check failure on line 642 in spec/unit/embedded.spec.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Property 'UpdateState' does not exist on type 'typeof WidgetApiToWidgetAction'.
new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, { detail: { data: { state: [] } } }),

Check failure on line 643 in spec/unit/embedded.spec.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Property 'UpdateState' does not exist on type 'typeof WidgetApiToWidgetAction'.
);
await init;

const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, resolve));
// Let's assume that a state event comes in but it doesn't actually
// update the state of the room just yet (maybe it's unauthorized)
widgetApi.emit(
`action:${WidgetApiToWidgetAction.SendEvent}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
Expand All @@ -649,25 +656,20 @@ describe("RoomWidgetClient", () => {
// 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
// However it should not have changed the room state
const room = client.getRoom("!1:example.org");
expect(room).not.toBeNull();
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
});
expect(room!.currentState.getStateEvents("org.example.foo", "bar")).toBe(null);

it("backfills", async () => {
widgetApi.readStateEvents.mockImplementation(async (eventType, limit, stateKey) =>
eventType === "org.example.foo" && (limit ?? Infinity) > 0 && stateKey === "bar"
? [event as IRoomEvent]
: [],
// Now assume that the state event becomes favored by state
// resolution for whatever reason and enters into the current state
// of the room
widgetApi.emit(
`action:${WidgetApiToWidgetAction.UpdateState}`,

Check failure on line 667 in spec/unit/embedded.spec.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Property 'UpdateState' does not exist on type 'typeof WidgetApiToWidgetAction'.
new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, {

Check failure on line 668 in spec/unit/embedded.spec.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Property 'UpdateState' does not exist on type 'typeof WidgetApiToWidgetAction'.
detail: { data: { state: [event] } },
}),
);

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();
// It should now have changed the room state
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
});
});
Expand Down
89 changes: 41 additions & 48 deletions src/embedded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
WidgetApiAction,
IWidgetApiResponse,
IWidgetApiResponseData,
IUpdateStateToWidgetActionRequest,

Check failure on line 31 in src/embedded.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Module '"matrix-widget-api"' has no exported member 'IUpdateStateToWidgetActionRequest'.
} from "matrix-widget-api";

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

Expand Down Expand Up @@ -189,6 +191,11 @@ export class RoomWidgetClient extends MatrixClient {
};

this.widgetApiReady = new Promise<void>((resolve) => this.widgetApi.once("ready", resolve));
this.roomStateSynced = capabilities.receiveState?.length
? new Promise<void>((resolve) =>
this.widgetApi.once(`action:${WidgetApiToWidgetAction.UpdateState}`, resolve),

Check failure on line 196 in src/embedded.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Property 'UpdateState' does not exist on type 'typeof WidgetApiToWidgetAction'.
)
: Promise.resolve();

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

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

Check failure on line 251 in src/embedded.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Property 'UpdateState' does not exist on type 'typeof WidgetApiToWidgetAction'.

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

await this.widgetApiReady;

// 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 undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode
// -> state events in `timelineEventList` 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 backfilling events");
logger.info("Finished initial sync");

this.matrixRTC.start();

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

Check failure on line 307 in src/embedded.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Property 'UpdateState' does not exist on type 'typeof WidgetApiToWidgetAction'.

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

// The widget API does not tell us whether a state event came from `state_after` or not so we assume legacy behaviour for now.
if (this.syncApi instanceof SyncApi) {
// The code will want to be something like:
// ```
// if (!params.addToTimeline && !params.addToState) {
// // Passing undefined for `stateAfterEventList` makes `injectRoomEvents` run in "legacy mode"
// // -> state events part of the `timelineEventList` parameter will update the state.
// this.injectRoomEvents(this.room!, [], undefined, [event]);
// } else {
// this.injectRoomEvents(this.room!, undefined, params.addToState ? [event] : [], params.addToTimeline ? [event] : []);
// }
// ```

// Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode
// -> state events in `timelineEventList` will update the state.
await this.syncApi.injectRoomEvents(this.room!, [], undefined, [event]);
this.syncApi.injectRoomEvents(this.room!, undefined, [], [event]);
} else {
// The code will want to be something like:
// ```
// if (!params.addToTimeline && !params.addToState) {
// this.injectRoomEvents(this.room!, [], [event]);
// } else {
// this.injectRoomEvents(this.room!, params.addToState ? [event] : [], params.addToTimeline ? [event] : []);
// }
// ```
await this.syncApi!.injectRoomEvents(this.room!, [], [event]); // Sliding Sync
// Sliding Sync
this.syncApi!.injectRoomEvents(this.room!, [], [event]);
}
this.emit(ClientEvent.Event, event);
this.setSyncState(SyncState.Syncing);
logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
logger.info(`Received event ${event.getId()} ${event.getType()}`);
} else {
const { event_id: eventId, room_id: roomId } = ev.detail.data;
logger.info(`Received event ${eventId} for a different room ${roomId}; discarding`);
Expand All @@ -628,6 +595,32 @@ export class RoomWidgetClient extends MatrixClient {
await this.ack(ev);
};

private onStateUpdate = async (ev: CustomEvent<IUpdateStateToWidgetActionRequest>): Promise<void> => {
ev.preventDefault();

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
// on screen
if (rawEvent.room_id === this.roomId) {
const event = new MatrixEvent(rawEvent as Partial<IEvent>);

if (this.syncApi instanceof SyncApi) {
this.syncApi.injectRoomEvents(this.room!, undefined, [event]);
} else {
// Sliding Sync
this.syncApi!.injectRoomEvents(this.room!, [event]);
}
logger.info(`Updated state entry ${event.getType()} ${event.getStateKey()} to ${event.getId()}`);
} else {
const { event_id: eventId, room_id: roomId } = ev.detail.data;
logger.info(`Received state entry ${eventId} for a different room ${roomId}; discarding`);
}
}

await this.ack(ev);
};

private async watchTurnServers(): Promise<void> {
const servers = this.widgetApi.getTurnServers();
const onClientStopped = (): void => {
Expand Down

0 comments on commit a1eac0c

Please sign in to comment.