Skip to content

Commit 8a98718

Browse files
authored
Add UPDATE_STATE widget api version (#125)
* Add new version for UPDATE_STATE * Let the client only send `UpdateState` if the widget supports it. * fix tests * add vscode ignore * test for not sending update event if not available in versions * review
1 parent 61c45f7 commit 8a98718

File tree

4 files changed

+89
-24
lines changed

4 files changed

+89
-24
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ lib/
33
dist/
44
.npmrc
55
.idea
6+
.vscode
67

78
# Logs
89
logs

src/ClientWidgetApi.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import {
4040
ISupportedVersionsActionRequest,
4141
ISupportedVersionsActionResponseData,
4242
} from "./interfaces/SupportedVersionsAction";
43-
import { CurrentApiVersions } from "./interfaces/ApiVersion";
43+
import { ApiVersion, CurrentApiVersions, UnstableApiVersion } from "./interfaces/ApiVersion";
4444
import { IScreenshotActionResponseData } from "./interfaces/ScreenshotAction";
4545
import { IVisibilityActionRequestData } from "./interfaces/VisibilityAction";
4646
import { IWidgetApiAcknowledgeResponseData, IWidgetApiResponseData } from "./interfaces/IWidgetApiResponse";
@@ -138,6 +138,7 @@ import { IUpdateStateToWidgetRequestData } from "./interfaces/UpdateStateAction"
138138
export class ClientWidgetApi extends EventEmitter {
139139
public readonly transport: ITransport;
140140

141+
private cachedWidgetVersions: ApiVersion[] | null = null;
141142
// contentLoadedActionSent is used to check that only one ContentLoaded request is send.
142143
private contentLoadedActionSent = false;
143144
private allowedCapabilities = new Set<Capability>();
@@ -230,6 +231,24 @@ export class ClientWidgetApi extends EventEmitter {
230231
this.transport.stop();
231232
}
232233

234+
public async getWidgetVersions(): Promise<ApiVersion[]> {
235+
if (Array.isArray(this.cachedWidgetVersions)) {
236+
return Promise.resolve(this.cachedWidgetVersions);
237+
}
238+
239+
try {
240+
const r = await this.transport.send<IWidgetApiRequestEmptyData, ISupportedVersionsActionResponseData>(
241+
WidgetApiToWidgetAction.SupportedApiVersions,
242+
{},
243+
);
244+
this.cachedWidgetVersions = r.supported_versions;
245+
return r.supported_versions;
246+
} catch (e) {
247+
console.warn("non-fatal error getting supported widget versions: ", e);
248+
return [];
249+
}
250+
}
251+
233252
private beginCapabilities(): void {
234253
// widget has loaded - tell all the listeners that
235254
this.emit("preparing");
@@ -285,7 +304,7 @@ export class ClientWidgetApi extends EventEmitter {
285304

286305
private onIframeLoad(ev: Event): void {
287306
if (this.widget.waitForIframeLoad) {
288-
// If the widget is set to waitForIframeLoad the capabilities immediatly get setup after load.
307+
// If the widget is set to waitForIframeLoad the capabilities immediately get setup after load.
289308
// The client does not wait for the ContentLoaded action.
290309
this.beginCapabilities();
291310
} else {
@@ -1007,7 +1026,7 @@ export class ClientWidgetApi extends EventEmitter {
10071026
public async feedEvent(rawEvent: IRoomEvent, currentViewedRoomId: string): Promise<void>;
10081027
/**
10091028
* Feeds an event to the widget. As a client you are expected to call this
1010-
* for every new event in every room to which you are joined or invited.
1029+
* for every new event (including state events) in every room to which you are joined or invited.
10111030
* @param {IRoomEvent} rawEvent The event to (try to) send to the widget.
10121031
* @returns {Promise<void>} Resolves when delivered or if the widget is not
10131032
* able to read the event due to permissions, rejects if the widget failed
@@ -1087,10 +1106,12 @@ export class ClientWidgetApi extends EventEmitter {
10871106
events.push(...stateKeyMap.values());
10881107
}
10891108
}
1090-
await this.transport.send<IUpdateStateToWidgetRequestData>(
1091-
WidgetApiToWidgetAction.UpdateState,
1092-
{ state: events },
1093-
);
1109+
if ((await this.getWidgetVersions()).includes(UnstableApiVersion.MSC2762_UPDATE_STATE)) {
1110+
// Only send state updates when using UpdateState. Otherwise the SendEvent action will be responsible for state updates.
1111+
await this.transport.send<IUpdateStateToWidgetRequestData>(WidgetApiToWidgetAction.UpdateState, {
1112+
state: events,
1113+
});
1114+
}
10941115
} finally {
10951116
this.flushRoomStateTask = null;
10961117
}
@@ -1149,21 +1170,23 @@ export class ClientWidgetApi extends EventEmitter {
11491170
* room state entry.
11501171
* @returns {Promise<void>} Resolves when delivered or if the widget is not
11511172
* able to receive the room state due to permissions, rejects if the
1152-
widget failed to handle the update.
1173+
* widget failed to handle the update.
11531174
*/
11541175
public async feedStateUpdate(rawEvent: IRoomEvent): Promise<void> {
1155-
if (rawEvent.state_key === undefined) throw new Error('Not a state event');
1176+
if (rawEvent.state_key === undefined) throw new Error("Not a state event");
11561177
if (
11571178
(rawEvent.room_id === this.viewedRoomId || this.canUseRoomTimeline(rawEvent.room_id))
11581179
&& this.canReceiveStateEvent(rawEvent.type, rawEvent.state_key)
11591180
) {
11601181
// Updates could race with the initial push of the room's state
11611182
if (this.pushRoomStateTasks.size === 0) {
11621183
// No initial push tasks are pending; safe to send immediately
1163-
await this.transport.send<IUpdateStateToWidgetRequestData>(
1164-
WidgetApiToWidgetAction.UpdateState,
1165-
{ state: [rawEvent] },
1166-
);
1184+
if ((await this.getWidgetVersions()).includes(UnstableApiVersion.MSC2762_UPDATE_STATE)) {
1185+
// Only send state updates when using UpdateState. Otherwise the SendEvent action will be responsible for state updates.
1186+
await this.transport.send<IUpdateStateToWidgetRequestData>(WidgetApiToWidgetAction.UpdateState, {
1187+
state: [rawEvent],
1188+
});
1189+
}
11671190
} else {
11681191
// Lump the update in with whatever data will be sent in the
11691192
// initial push later. Even if we set it to an "outdated" entry

src/interfaces/ApiVersion.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export enum MatrixApiVersion {
2222

2323
export enum UnstableApiVersion {
2424
MSC2762 = "org.matrix.msc2762",
25+
MSC2762_UPDATE_STATE = "org.matrix.msc2762_update_state",
2526
MSC2871 = "org.matrix.msc2871",
2627
MSC2873 = "org.matrix.msc2873",
2728
MSC2931 = "org.matrix.msc2931",
@@ -41,6 +42,7 @@ export const CurrentApiVersions: ApiVersion[] = [
4142
MatrixApiVersion.Prerelease2,
4243
//MatrixApiVersion.V010,
4344
UnstableApiVersion.MSC2762,
45+
UnstableApiVersion.MSC2762_UPDATE_STATE,
4446
UnstableApiVersion.MSC2871,
4547
UnstableApiVersion.MSC2873,
4648
UnstableApiVersion.MSC2931,

test/ClientWidgetApi-test.ts

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,17 @@ import { waitFor } from '@testing-library/dom';
1919

2020
import { ClientWidgetApi } from "../src/ClientWidgetApi";
2121
import { WidgetDriver } from "../src/driver/WidgetDriver";
22-
import { UnstableApiVersion } from '../src/interfaces/ApiVersion';
23-
import { Capability } from '../src/interfaces/Capabilities';
24-
import { IRoomEvent } from '../src/interfaces/IRoomEvent';
25-
import { IWidgetApiRequest } from '../src/interfaces/IWidgetApiRequest';
26-
import { IReadRelationsFromWidgetActionRequest } from '../src/interfaces/ReadRelationsAction';
27-
import { ISupportedVersionsActionRequest } from '../src/interfaces/SupportedVersionsAction';
28-
import { IUserDirectorySearchFromWidgetActionRequest } from '../src/interfaces/UserDirectorySearchAction';
29-
import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from '../src/interfaces/WidgetApiAction';
30-
import { WidgetApiDirection } from '../src/interfaces/WidgetApiDirection';
31-
import { Widget } from '../src/models/Widget';
32-
import { PostmessageTransport } from '../src/transport/PostmessageTransport';
22+
import { CurrentApiVersions, UnstableApiVersion } from "../src/interfaces/ApiVersion";
23+
import { Capability } from "../src/interfaces/Capabilities";
24+
import { IRoomEvent } from "../src/interfaces/IRoomEvent";
25+
import { IWidgetApiRequest } from "../src/interfaces/IWidgetApiRequest";
26+
import { IReadRelationsFromWidgetActionRequest } from "../src/interfaces/ReadRelationsAction";
27+
import { ISupportedVersionsActionRequest } from "../src/interfaces/SupportedVersionsAction";
28+
import { IUserDirectorySearchFromWidgetActionRequest } from "../src/interfaces/UserDirectorySearchAction";
29+
import { WidgetApiFromWidgetAction, WidgetApiToWidgetAction } from "../src/interfaces/WidgetApiAction";
30+
import { WidgetApiDirection } from "../src/interfaces/WidgetApiDirection";
31+
import { Widget } from "../src/models/Widget";
32+
import { PostmessageTransport } from "../src/transport/PostmessageTransport";
3333
import {
3434
IDownloadFileActionFromWidgetActionRequest,
3535
IGetOpenIDActionRequest,
@@ -792,6 +792,14 @@ describe('ClientWidgetApi', () => {
792792
const roomId = '!room:example.org';
793793
const otherRoomId = '!other-room:example.org';
794794
clientWidgetApi.setViewedRoomId(roomId);
795+
796+
jest.spyOn(transport, "send").mockImplementation((action, data) => {
797+
if (action === WidgetApiToWidgetAction.SupportedApiVersions) {
798+
return Promise.resolve({ supported_versions: CurrentApiVersions });
799+
}
800+
return Promise.resolve({});
801+
});
802+
795803
const topicEvent = createRoomEvent({
796804
room_id: roomId,
797805
type: 'm.room.topic',
@@ -904,6 +912,37 @@ describe('ClientWidgetApi', () => {
904912
});
905913
});
906914

915+
describe('dont receive UpdateState if version not supported', () => {
916+
it('syncs initial state and feeds updates', async () => {
917+
const roomId = '!room:example.org';
918+
clientWidgetApi.setViewedRoomId(roomId);
919+
jest.spyOn(transport, "send").mockImplementation((action, data) => {
920+
if (action === WidgetApiToWidgetAction.SupportedApiVersions) {
921+
return Promise.resolve({ supported_versions: [] });
922+
}
923+
return Promise.resolve({});
924+
});
925+
926+
await loadIframe([
927+
'org.matrix.msc2762.receive.state_event:m.room.join_rules#',
928+
]);
929+
930+
const newJoinRulesEvent = createRoomEvent({
931+
room_id: roomId,
932+
type: 'm.room.join_rules',
933+
state_key: '',
934+
content: { join_rule: 'invite' },
935+
});
936+
clientWidgetApi.feedStateUpdate(newJoinRulesEvent);
937+
938+
await waitFor(() => {
939+
940+
// Only the updated join rules should have been delivered
941+
expect(transport.send).not.toHaveBeenCalledWith(WidgetApiToWidgetAction.UpdateState);
942+
});
943+
});
944+
});
945+
907946
describe('update_delayed_event action', () => {
908947
it('fails to update delayed events', async () => {
909948
const event: IUpdateDelayedEventFromWidgetActionRequest = {

0 commit comments

Comments
 (0)