You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-06 12:02:40 +03:00
Distinguish room state and timeline events in embedded clients (#4574)
* Distinguish room state and timeline events in embedded clients This change enables room widget clients to take advantage of the more reliable method of communicating room state over the widget API provided by a recent update to MSC2762. * Add missing awaits * Upgrade matrix-widget-api
This commit is contained in:
@@ -28,7 +28,6 @@ import {
|
|||||||
WidgetApiToWidgetAction,
|
WidgetApiToWidgetAction,
|
||||||
MatrixCapabilities,
|
MatrixCapabilities,
|
||||||
ITurnServer,
|
ITurnServer,
|
||||||
IRoomEvent,
|
|
||||||
IOpenIDCredentials,
|
IOpenIDCredentials,
|
||||||
ISendEventFromWidgetResponseData,
|
ISendEventFromWidgetResponseData,
|
||||||
WidgetApiResponseError,
|
WidgetApiResponseError,
|
||||||
@@ -635,12 +634,20 @@ describe("RoomWidgetClient", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("receives", async () => {
|
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.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||||
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
|
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
|
||||||
|
// Client needs to be told that the room state is loaded
|
||||||
|
widgetApi.emit(
|
||||||
|
`action:${WidgetApiToWidgetAction.UpdateState}`,
|
||||||
|
new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, { detail: { data: { state: [] } } }),
|
||||||
|
);
|
||||||
|
await init;
|
||||||
|
|
||||||
const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
|
const emittedEvent = new Promise<MatrixEvent>((resolve) => client.once(ClientEvent.Event, resolve));
|
||||||
const emittedSync = new Promise<SyncState>((resolve) => client.once(ClientEvent.Sync, 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(
|
widgetApi.emit(
|
||||||
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
||||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
|
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
|
||||||
@@ -649,26 +656,43 @@ describe("RoomWidgetClient", () => {
|
|||||||
// The client should've emitted about the received event
|
// The client should've emitted about the received event
|
||||||
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
|
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
|
||||||
expect(await emittedSync).toEqual(SyncState.Syncing);
|
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");
|
const room = client.getRoom("!1:example.org");
|
||||||
expect(room).not.toBeNull();
|
expect(room!.currentState.getStateEvents("org.example.foo", "bar")).toBe(null);
|
||||||
|
|
||||||
|
// 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}`,
|
||||||
|
new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, {
|
||||||
|
detail: { data: { state: [event] } },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// It should now have changed the room state
|
||||||
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
|
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("backfills", async () => {
|
it("ignores state updates for other rooms", async () => {
|
||||||
widgetApi.readStateEvents.mockImplementation(async (eventType, limit, stateKey) =>
|
const init = makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
||||||
eventType === "org.example.foo" && (limit ?? Infinity) > 0 && stateKey === "bar"
|
// Client needs to be told that the room state is loaded
|
||||||
? [event as IRoomEvent]
|
widgetApi.emit(
|
||||||
: [],
|
`action:${WidgetApiToWidgetAction.UpdateState}`,
|
||||||
|
new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, { detail: { data: { state: [] } } }),
|
||||||
);
|
);
|
||||||
|
await init;
|
||||||
|
|
||||||
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
|
// Now a room we're not interested in receives a state update
|
||||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
widgetApi.emit(
|
||||||
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
|
`action:${WidgetApiToWidgetAction.UpdateState}`,
|
||||||
|
new CustomEvent(`action:${WidgetApiToWidgetAction.UpdateState}`, {
|
||||||
const room = client.getRoom("!1:example.org");
|
detail: { data: { state: [{ ...event, room_id: "!other-room:example.org" }] } },
|
||||||
expect(room).not.toBeNull();
|
}),
|
||||||
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
|
);
|
||||||
|
// No change to the room state
|
||||||
|
for (const room of client.getRooms()) {
|
||||||
|
expect(room.currentState.getStateEvents("org.example.foo", "bar")).toBe(null);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -28,6 +28,7 @@ import {
|
|||||||
WidgetApiAction,
|
WidgetApiAction,
|
||||||
IWidgetApiResponse,
|
IWidgetApiResponse,
|
||||||
IWidgetApiResponseData,
|
IWidgetApiResponseData,
|
||||||
|
IUpdateStateToWidgetActionRequest,
|
||||||
} from "matrix-widget-api";
|
} from "matrix-widget-api";
|
||||||
|
|
||||||
import { MatrixEvent, IEvent, IContent, EventStatus } from "./models/event.ts";
|
import { MatrixEvent, IEvent, IContent, EventStatus } from "./models/event.ts";
|
||||||
@@ -136,6 +137,7 @@ export type EventHandlerMap = { [RoomWidgetClientEvent.PendingEventsChanged]: ()
|
|||||||
export class RoomWidgetClient extends MatrixClient {
|
export class RoomWidgetClient extends MatrixClient {
|
||||||
private room?: Room;
|
private room?: Room;
|
||||||
private readonly widgetApiReady: Promise<void>;
|
private readonly widgetApiReady: Promise<void>;
|
||||||
|
private readonly roomStateSynced: Promise<void>;
|
||||||
private lifecycle?: AbortController;
|
private lifecycle?: AbortController;
|
||||||
private syncState: SyncState | null = null;
|
private syncState: SyncState | null = null;
|
||||||
|
|
||||||
@@ -189,6 +191,11 @@ export class RoomWidgetClient extends MatrixClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.widgetApiReady = new Promise<void>((resolve) => this.widgetApi.once("ready", resolve));
|
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),
|
||||||
|
)
|
||||||
|
: Promise.resolve();
|
||||||
|
|
||||||
// Request capabilities for the functionality this client needs to support
|
// Request capabilities for the functionality this client needs to support
|
||||||
if (
|
if (
|
||||||
@@ -241,6 +248,7 @@ export class RoomWidgetClient extends MatrixClient {
|
|||||||
|
|
||||||
widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
|
widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
|
||||||
widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
|
widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
|
||||||
|
widgetApi.on(`action:${WidgetApiToWidgetAction.UpdateState}`, this.onStateUpdate);
|
||||||
|
|
||||||
// Open communication with the host
|
// Open communication with the host
|
||||||
widgetApi.start();
|
widgetApi.start();
|
||||||
@@ -276,28 +284,6 @@ export class RoomWidgetClient extends MatrixClient {
|
|||||||
|
|
||||||
await this.widgetApiReady;
|
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) {
|
if (opts.clientWellKnownPollPeriod !== undefined) {
|
||||||
this.clientWellKnownIntervalID = setInterval(() => {
|
this.clientWellKnownIntervalID = setInterval(() => {
|
||||||
this.fetchClientWellKnown();
|
this.fetchClientWellKnown();
|
||||||
@@ -305,8 +291,9 @@ export class RoomWidgetClient extends MatrixClient {
|
|||||||
this.fetchClientWellKnown();
|
this.fetchClientWellKnown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.roomStateSynced;
|
||||||
this.setSyncState(SyncState.Syncing);
|
this.setSyncState(SyncState.Syncing);
|
||||||
logger.info("Finished backfilling events");
|
logger.info("Finished initial sync");
|
||||||
|
|
||||||
this.matrixRTC.start();
|
this.matrixRTC.start();
|
||||||
|
|
||||||
@@ -317,6 +304,7 @@ export class RoomWidgetClient extends MatrixClient {
|
|||||||
public stopClient(): void {
|
public stopClient(): void {
|
||||||
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
|
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
|
||||||
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
|
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
|
||||||
|
this.widgetApi.off(`action:${WidgetApiToWidgetAction.UpdateState}`, this.onStateUpdate);
|
||||||
|
|
||||||
super.stopClient();
|
super.stopClient();
|
||||||
this.lifecycle!.abort(); // Signal to other async tasks that the client has stopped
|
this.lifecycle!.abort(); // Signal to other async tasks that the client has stopped
|
||||||
@@ -574,36 +562,15 @@ export class RoomWidgetClient extends MatrixClient {
|
|||||||
// Only inject once we have update the txId
|
// Only inject once we have update the txId
|
||||||
await this.updateTxId(event);
|
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) {
|
if (this.syncApi instanceof SyncApi) {
|
||||||
// The code will want to be something like:
|
await this.syncApi.injectRoomEvents(this.room!, undefined, [], [event]);
|
||||||
// ```
|
|
||||||
// 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]);
|
|
||||||
} else {
|
} else {
|
||||||
// The code will want to be something like:
|
// Sliding Sync
|
||||||
// ```
|
await this.syncApi!.injectRoomEvents(this.room!, [], [event]);
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
this.emit(ClientEvent.Event, event);
|
this.emit(ClientEvent.Event, event);
|
||||||
this.setSyncState(SyncState.Syncing);
|
this.setSyncState(SyncState.Syncing);
|
||||||
logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
|
logger.info(`Received event ${event.getId()} ${event.getType()}`);
|
||||||
} else {
|
} else {
|
||||||
const { event_id: eventId, room_id: roomId } = ev.detail.data;
|
const { event_id: eventId, room_id: roomId } = ev.detail.data;
|
||||||
logger.info(`Received event ${eventId} for a different room ${roomId}; discarding`);
|
logger.info(`Received event ${eventId} for a different room ${roomId}; discarding`);
|
||||||
@@ -628,6 +595,32 @@ export class RoomWidgetClient extends MatrixClient {
|
|||||||
await this.ack(ev);
|
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) {
|
||||||
|
await this.syncApi.injectRoomEvents(this.room!, undefined, [event]);
|
||||||
|
} else {
|
||||||
|
// Sliding Sync
|
||||||
|
await 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> {
|
private async watchTurnServers(): Promise<void> {
|
||||||
const servers = this.widgetApi.getTurnServers();
|
const servers = this.widgetApi.getTurnServers();
|
||||||
const onClientStopped = (): void => {
|
const onClientStopped = (): void => {
|
||||||
|
@@ -4854,9 +4854,9 @@ matrix-mock-request@^2.5.0:
|
|||||||
expect "^28.1.0"
|
expect "^28.1.0"
|
||||||
|
|
||||||
matrix-widget-api@^1.10.0:
|
matrix-widget-api@^1.10.0:
|
||||||
version "1.10.0"
|
version "1.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.10.0.tgz#d31ea073a5871a1fb1a511ef900b0c125a37bf55"
|
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.12.0.tgz#b3d22bab1670051c8eeee66bb96d08b33148bc99"
|
||||||
integrity sha512-rkAJ29briYV7TJnfBVLVSKtpeBrBju15JZFSDP6wj8YdbCu1bdmlplJayQ+vYaw1x4fzI49Q+Nz3E85s46sRDw==
|
integrity sha512-6JRd9fJGGvuBRhcTg9wX+Skn/Q1wox3jdp5yYQKJ6pPw4urW9bkTR90APBKVDB1vorJKT44jml+lCzkDMRBjww==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/events" "^3.0.0"
|
"@types/events" "^3.0.0"
|
||||||
events "^3.2.0"
|
events "^3.2.0"
|
||||||
|
Reference in New Issue
Block a user