Skip to content

MatrixRTC: New membership manager #4726

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 136 commits into from
Mar 11, 2025
Merged
Show file tree
Hide file tree
Changes from 125 commits
Commits
Show all changes
136 commits
Select commit Hold shift + click to select a range
a404539
WIP doodles on MembershipManager test cases
hughns Jan 23, 2025
834d461
.
hughns Jan 23, 2025
6594860
initial membership manager test setup.
toger5 Feb 17, 2025
738016f
Updates from discussion
hughns Feb 17, 2025
ec375f3
revert renaming comments
toger5 Feb 17, 2025
8c8e97e
remove unused import
toger5 Feb 17, 2025
d9192f3
fix leave delayed event resend test.
toger5 Feb 17, 2025
0444816
comment out and remove unused variables
toger5 Feb 17, 2025
7125d45
es lint
toger5 Feb 17, 2025
3d46d05
use jsdom instead of node test environment
toger5 Feb 17, 2025
b0492ca
remove unused variables
toger5 Feb 17, 2025
c59f04d
remove unused export
toger5 Feb 17, 2025
e1fbdcd
temp
toger5 Feb 18, 2025
cd1321b
review
toger5 Feb 19, 2025
28f17be
fixup tests
toger5 Feb 19, 2025
a21c6e5
more review
toger5 Feb 19, 2025
8df56bb
remove wait for expect dependency
toger5 Feb 21, 2025
73bed3b
temp
toger5 Feb 21, 2025
8db2d0a
fix wrong mocked meberhsip template
toger5 Feb 19, 2025
7e4636b
rename MembershipManager -> LegacyMembershipManager
toger5 Feb 19, 2025
412ee18
Add new memberhsip manager
toger5 Feb 19, 2025
cd87cb4
fix tests to be compatible with old and new membership manager
toger5 Feb 19, 2025
3ac28c2
Comment cleanup
toger5 Feb 19, 2025
a81106e
Allow join to throw
toger5 Feb 19, 2025
6c2a5d1
introduce membershipExpiryTimeoutSlack
toger5 Feb 20, 2025
061285f
more detailed comments and cleanup
toger5 Feb 20, 2025
d116b53
warn if slack is misconfigured and use default values instead
toger5 Feb 21, 2025
c3a02db
fix action resets.
toger5 Feb 21, 2025
0236657
flatten MembershipManager.spec.ts
toger5 Feb 21, 2025
1cf0caa
rename testEnvironment to memberManagerTestEnvironment
toger5 Feb 21, 2025
a16f1ca
allow configuring Legacy manager in the matrixRTC session
toger5 Feb 21, 2025
2f90d9c
deprecate LegacyMembershipManager
toger5 Feb 21, 2025
83f0af1
remove usage of waitForExpect
toger5 Feb 21, 2025
87bb597
flatten tests and add comments
toger5 Feb 21, 2025
0c4f6b2
Merge branch 'hughns/membershipmanager-test-cases' into toger5/new-Me…
toger5 Feb 21, 2025
c7017b9
clean up leave logic branch
toger5 Feb 24, 2025
4e60a35
add more leave test cases
toger5 Feb 24, 2025
d3f4ec6
Merge branch 'hughns/membershipmanager-test-cases' into toger5/new-Me…
toger5 Feb 24, 2025
4502083
use defer
toger5 Feb 24, 2025
d745605
review ("Some minor tidying things for now.")
toger5 Feb 24, 2025
5b35b73
add onError for join method and cleanup
toger5 Feb 24, 2025
7f75daa
use pop instead of filter
toger5 Feb 24, 2025
23c6eec
fixes
toger5 Feb 24, 2025
724f690
simplify error handling and MembershipAction
toger5 Feb 24, 2025
493f0db
Add diagram
toger5 Feb 24, 2025
e5e5434
Merge branch 'develop' into hughns/membershipmanager-test-cases
toger5 Feb 24, 2025
cf1a87b
Merge branch 'hughns/membershipmanager-test-cases' into toger5/new-Me…
toger5 Feb 24, 2025
33e203f
fix new error api in rtc session
toger5 Feb 24, 2025
9fa9bd6
fix up retry counter
toger5 Feb 24, 2025
d9b22cb
Merge branch 'develop' into hughns/membershipmanager-test-cases
toger5 Feb 24, 2025
77bf8b9
Merge branch 'hughns/membershipmanager-test-cases' into toger5/new-Me…
toger5 Feb 24, 2025
e835561
fix lints
toger5 Feb 24, 2025
4c3d1f4
make unrecoverable errors more explicit
toger5 Feb 24, 2025
3dee61a
fix tests
toger5 Feb 24, 2025
172c3ea
Allow multiple retries on the rtc state event http requests.
toger5 Feb 25, 2025
3488863
use then catch for startup
toger5 Feb 25, 2025
8f77442
no try catch 1
toger5 Feb 25, 2025
a1e4f25
update expire headroom logic
toger5 Feb 26, 2025
6334faf
replace flushPromise with advanceTimersByTimeAsync
toger5 Feb 26, 2025
495bf8b
fix leaving special cases
toger5 Feb 26, 2025
5a680b0
more unrecoverable errors special cases
toger5 Feb 26, 2025
bb5b0de
move to MatrixRTCSessionManager logger
toger5 Feb 26, 2025
8005dc8
add state reset and add another unhandleable error
toger5 Feb 26, 2025
b8aa7fc
missed review fixes
toger5 Feb 26, 2025
998e16b
remove @jest/environment dependency
toger5 Feb 26, 2025
72994f2
Cleanup awaits and Make mock types more correct.
toger5 Feb 26, 2025
ce24845
remove flush promise dependency
toger5 Feb 26, 2025
88f40e4
fix not recreating default state on reset
toger5 Feb 26, 2025
fe3cc26
Use per action rate limit and retry counter
toger5 Feb 26, 2025
c50fa32
add linting to matrixrtc tests
toger5 Feb 26, 2025
94bb78f
Add fix async lints and use matrix rtc logger for test environment.
toger5 Feb 26, 2025
5461fbf
Merge branch 'hughns/membershipmanager-test-cases' into toger5/new-Me…
toger5 Feb 26, 2025
1016b05
prettier
toger5 Feb 26, 2025
a2c8d93
review step 1
toger5 Feb 26, 2025
218c6d9
change to MatrixRTCSession logger
toger5 Feb 26, 2025
55c2e42
Merge branch 'hughns/membershipmanager-test-cases' into toger5/new-Me…
toger5 Feb 26, 2025
5c43bf2
review step 2
toger5 Feb 26, 2025
453b0cb
make LoopHandler Private
toger5 Feb 26, 2025
86b5a30
update config to use NewManager wording
toger5 Feb 26, 2025
66c1f8e
emit error on rtc session if the membership manager encounters one
toger5 Feb 26, 2025
fbb2a70
network error and throw refactor
toger5 Feb 26, 2025
177e2f4
make accessing the full room deprecated
toger5 Feb 26, 2025
9e60a36
remove deprecated usage of full room
toger5 Feb 26, 2025
c83429c
Merge branch 'develop' into hughns/membershipmanager-test-cases
toger5 Feb 26, 2025
74f76b4
Clean up the deprecation
hughns Feb 27, 2025
ce3ff17
Merge branch 'hughns/membershipmanager-test-cases' into toger5/new-Me…
toger5 Feb 27, 2025
4a2d35b
add network error handler and cleanup
toger5 Feb 27, 2025
631de6e
better logging, another test, make maximumNetworkErrorRetryCount conf…
toger5 Feb 27, 2025
713ea89
Merge branch 'develop' into toger5/new-MembershipManager
toger5 Feb 27, 2025
4b45931
more logging & refactor leave promise
toger5 Feb 27, 2025
7e20f11
add ConnectionError as possible retry cause
toger5 Feb 27, 2025
ec6a258
Make it work in embedded mode with a server that does not support del…
toger5 Feb 27, 2025
5d69555
review iteration 1
toger5 Feb 27, 2025
e2d131e
review iteration 2
toger5 Feb 28, 2025
79abd60
first step in improving widget error handling
toger5 Feb 28, 2025
b9c20cc
make the embedded client throw ConnectionErrors where desired.
toger5 Feb 28, 2025
8a711c3
fix tests
toger5 Feb 28, 2025
f4c4b7d
delayed event sending widget mode stop gap fix.
toger5 Feb 28, 2025
dabb8aa
improve comment
toger5 Feb 28, 2025
a2c4295
fix unrecoverable error joinState (and add JoinStateChanged) emission.
toger5 Mar 3, 2025
06545e8
check that we do not add multipe sendFirstDelayed Events
toger5 Mar 3, 2025
9c2eab3
also check insertions queue
toger5 Mar 3, 2025
f46f099
always log "Missing own membership: force re-join"
toger5 Mar 4, 2025
0603181
Do not update the membership if we are in any (a later) state of send…
toger5 Mar 4, 2025
987a0a8
make leave reset actually stop the manager.
toger5 Mar 4, 2025
27b689a
fix tests (and implementation)
toger5 Mar 5, 2025
6d67407
Allow MembershipManger to be set at runtime via JoinConfig.membership…
hughns Mar 5, 2025
c5435a5
Map actions into status as a sanity check
hughns Mar 5, 2025
c4eaf5e
Log status change after applying actions
hughns Mar 6, 2025
40dd4f6
Add todo
hughns Mar 6, 2025
d59f9d9
Cleanup
hughns Mar 6, 2025
5942f05
Log transition from earlier status
hughns Mar 6, 2025
31b6e45
remove redundant status implementation
toger5 Mar 7, 2025
2b464c9
More cleanup
hughns Mar 7, 2025
b501057
Consider insertions in status()
hughns Mar 7, 2025
075e186
Log duration for emitting MatrixRTCSessionEvent.MembershipsChanged
hughns Mar 7, 2025
3065180
add another valid condition for connected
toger5 Mar 7, 2025
72cc607
some TODO cleanup
toger5 Mar 7, 2025
e8d588d
review add warning when using addAction while the scheduler is not ru…
toger5 Mar 7, 2025
ddb02e4
Merge branch 'develop' into toger5/new-MembershipManager
toger5 Mar 7, 2025
47a7e4c
es lint
toger5 Mar 7, 2025
fce95bf
refactor to return based handler approach (remove insertions array)
toger5 Mar 8, 2025
246354a
refactor: Move action scheduler
toger5 Mar 8, 2025
b00a630
refactor: move different handler cases into separate functions
toger5 Mar 8, 2025
798c77c
linter
toger5 Mar 8, 2025
9cd2358
review: delayed events endpoint error
toger5 Mar 10, 2025
de492d9
review
toger5 Mar 10, 2025
31c5cfd
Suggestions from pair review
hughns Mar 10, 2025
6af4730
resetState is actually only used internally
hughns Mar 10, 2025
3e4caff
Revert "resetState is actually only used internally"
hughns Mar 10, 2025
2c6062f
refactor: running is part of the scheduler (not state)
toger5 Mar 11, 2025
2f77b7e
refactor: move everything state related from schduler to manager.
toger5 Mar 11, 2025
9af3bfe
review
toger5 Mar 11, 2025
2729f3a
Update src/matrixrtc/NewMembershipManager.ts
toger5 Mar 11, 2025
288edf5
review
toger5 Mar 11, 2025
08ffaba
public -> private + missed review fiexes (comment typos)
toger5 Mar 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 20 additions & 20 deletions spec/unit/embedded.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,26 +49,26 @@ const testOIDCToken = {
token_type: "Bearer",
};
class MockWidgetApi extends EventEmitter {
public start = jest.fn();
public requestCapability = jest.fn();
public requestCapabilities = jest.fn();
public requestCapabilityForRoomTimeline = jest.fn();
public requestCapabilityToSendEvent = jest.fn();
public requestCapabilityToReceiveEvent = jest.fn();
public requestCapabilityToSendMessage = jest.fn();
public requestCapabilityToReceiveMessage = jest.fn();
public requestCapabilityToSendState = jest.fn();
public requestCapabilityToReceiveState = jest.fn();
public requestCapabilityToSendToDevice = jest.fn();
public requestCapabilityToReceiveToDevice = jest.fn();
public start = jest.fn().mockResolvedValue(undefined);
public requestCapability = jest.fn().mockResolvedValue(undefined);
public requestCapabilities = jest.fn().mockResolvedValue(undefined);
public requestCapabilityForRoomTimeline = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToSendEvent = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveEvent = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToSendMessage = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveMessage = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToSendState = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveState = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToSendToDevice = jest.fn().mockResolvedValue(undefined);
public requestCapabilityToReceiveToDevice = jest.fn().mockResolvedValue(undefined);
public sendRoomEvent = jest.fn(
(eventType: string, content: unknown, roomId?: string, delay?: number, parentDelayId?: string) =>
async (eventType: string, content: unknown, roomId?: string, delay?: number, parentDelayId?: string) =>
delay === undefined && parentDelayId === undefined
? { event_id: `$${Math.random()}` }
: { delay_id: `id-${Math.random()}` },
);
public sendStateEvent = jest.fn(
(
async (
eventType: string,
stateKey: string,
content: unknown,
Expand All @@ -80,17 +80,17 @@ class MockWidgetApi extends EventEmitter {
? { event_id: `$${Math.random()}` }
: { delay_id: `id-${Math.random()}` },
);
public updateDelayedEvent = jest.fn();
public sendToDevice = jest.fn();
public requestOpenIDConnectToken = jest.fn(() => {
public updateDelayedEvent = jest.fn().mockResolvedValue(undefined);
public sendToDevice = jest.fn().mockResolvedValue(undefined);
public requestOpenIDConnectToken = jest.fn(async () => {
return testOIDCToken;
return new Promise<IOpenIDCredentials>(() => {
return testOIDCToken;
});
});
public readStateEvents = jest.fn(() => []);
public getTurnServers = jest.fn(() => []);
public sendContentLoaded = jest.fn();
public readStateEvents = jest.fn(async () => []);
public getTurnServers = jest.fn(async () => []);
public sendContentLoaded = jest.fn().mockResolvedValue(undefined);

public transport = {
reply: jest.fn(),
Expand Down
124 changes: 110 additions & 14 deletions spec/unit/matrixrtc/MembershipManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ limitations under the License.

import { type MockedFunction, type Mock } from "jest-mock";

import { EventType, HTTPError, MatrixError, type Room } from "../../../src";
import { EventType, HTTPError, MatrixError, UnsupportedEndpointError, type Room } from "../../../src";
import { type Focus, type LivekitFocusActive, type SessionMembershipData } from "../../../src/matrixrtc";
import { LegacyMembershipManager } from "../../../src/matrixrtc/MembershipManager";
import { LegacyMembershipManager } from "../../../src/matrixrtc/LegacyMembershipManager";
import { makeMockClient, makeMockRoom, membershipTemplate, mockCallMembership, type MockClient } from "./mocks";
import { MembershipManager } from "../../../src/matrixrtc/NewMembershipManager";
import { defer } from "../../../src/utils";

function waitForMockCall(method: MockedFunction<any>, returnVal?: Promise<any>) {
Expand All @@ -44,9 +45,10 @@ function createAsyncHandle(method: MockedFunction<any>) {
* Tests different MembershipManager implementations. Some tests don't apply to `LegacyMembershipManager`
* use !FailsForLegacy to skip those. See: testEnvironment for more details.
*/

describe.each([
{ TestMembershipManager: LegacyMembershipManager, description: "LegacyMembershipManager" },
// { TestMembershipManager: MembershipManager, description: "MembershipManager" },
{ TestMembershipManager: MembershipManager, description: "MembershipManager" },
])("$description", ({ TestMembershipManager }) => {
let client: MockClient;
let room: Room;
Expand Down Expand Up @@ -244,7 +246,12 @@ describe.each([
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent as Mock);
const manager = new TestMembershipManager({}, room, client, () => undefined);
manager.join([focus], focusActive);
delayedHandle.reject?.(Error("Server does not support the delayed events API"));
delayedHandle.reject?.(
new UnsupportedEndpointError(
"Server does not support the delayed events API",
"sendDelayedStateEvent",
),
);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
});
it("does try to schedule a delayed leave event again if rate limited", async () => {
Expand Down Expand Up @@ -328,6 +335,7 @@ describe.each([
await jest.advanceTimersByTimeAsync(1);
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValue("unknown");
await manager.leave();

// We send a normal leave event since we failed using updateDelayedEvent with the "send" action.
expect(client.sendStateEvent).toHaveBeenLastCalledWith(
room.roomId,
Expand All @@ -337,9 +345,9 @@ describe.each([
);
});
// FailsForLegacy because legacy implementation always sends the empty state event even though it isn't needed
it("does nothing if not joined !FailsForLegacy", async () => {
it("does nothing if not joined !FailsForLegacy", () => {
const manager = new TestMembershipManager({}, room, client, () => undefined);
await manager.leave();
expect(async () => await manager.leave()).not.toThrow();
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
expect(client.sendStateEvent).not.toHaveBeenCalled();
});
Expand Down Expand Up @@ -470,10 +478,10 @@ describe.each([
// !FailsForLegacy because the expires logic was removed for the legacy call manager.
// Delayed events should replace it entirely but before they have wide adoption
// the expiration logic still makes sense.
// TODO: add git commit when we removed it.
it("extends `expires` when call still active !FailsForLegacy", async () => {
// TODO: Add git commit when we removed it.
async function testExpires(expire: number, headroom?: number) {
const manager = new TestMembershipManager(
{ membershipExpiryTimeout: 10_000 },
{ membershipExpiryTimeout: expire, membershipExpiryTimeoutHeadroom: headroom },
room,
client,
() => undefined,
Expand All @@ -482,13 +490,19 @@ describe.each([
await waitForMockCall(client.sendStateEvent);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
const sentMembership = (client.sendStateEvent as Mock).mock.calls[0][2] as SessionMembershipData;
expect(sentMembership.expires).toBe(10_000);
expect(sentMembership.expires).toBe(expire);
for (let i = 2; i <= 12; i++) {
await jest.advanceTimersByTimeAsync(10_000);
await jest.advanceTimersByTimeAsync(expire);
expect(client.sendStateEvent).toHaveBeenCalledTimes(i);
const sentMembership = (client.sendStateEvent as Mock).mock.lastCall![2] as SessionMembershipData;
expect(sentMembership.expires).toBe(10_000 * i);
expect(sentMembership.expires).toBe(expire * i);
}
}
it("extends `expires` when call still active !FailsForLegacy", async () => {
await testExpires(10_000);
});
it("extends `expires` using headroom configuration !FailsForLegacy", async () => {
await testExpires(10_000, 1_000);
});
});

Expand Down Expand Up @@ -544,7 +558,7 @@ describe.each([
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
});
// FailsForLegacy as implementation does not re-check membership before retrying.
it("abandons retry loop if leave() was called !FailsForLegacy", async () => {
it("abandons retry loop if leave() was called before sending state event !FailsForLegacy", async () => {
const handle = createAsyncHandle(client._unstable_sendDelayedStateEvent);

const manager = new TestMembershipManager({}, room, client, () => undefined);
Expand All @@ -565,7 +579,6 @@ describe.each([
await manager.leave();

// Wait for all timers to be setup
// await flushPromises();
await jest.advanceTimersByTimeAsync(1000);

// No new events should have been sent:
Expand Down Expand Up @@ -603,4 +616,87 @@ describe.each([
});
});
});
describe("unrecoverable errors", () => {
// !FailsForLegacy because legacy does not have a retry limit and no mechanism to communicate unrecoverable errors.
it("throws, when reaching maximum number of retires for initial delayed event creation !FailsForLegacy", async () => {
const delayEventSendError = jest.fn();
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(
new MatrixError(
{ errcode: "M_LIMIT_EXCEEDED" },
429,
undefined,
undefined,
new Headers({ "Retry-After": "2" }),
),
);
const manager = new TestMembershipManager({}, room, client, () => undefined);
manager.join([focus], focusActive, delayEventSendError);

for (let i = 0; i < 10; i++) {
await jest.advanceTimersByTimeAsync(2000);
}
expect(delayEventSendError).toHaveBeenCalled();
});
// !FailsForLegacy because legacy does not have a retry limit and no mechanism to communicate unrecoverable errors.
it("throws, when reaching maximum number of retires !FailsForLegacy", async () => {
const delayEventRestartError = jest.fn();
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValue(
new MatrixError(
{ errcode: "M_LIMIT_EXCEEDED" },
429,
undefined,
undefined,
new Headers({ "Retry-After": "1" }),
),
);
const manager = new TestMembershipManager({}, room, client, () => undefined);
manager.join([focus], focusActive, delayEventRestartError);

for (let i = 0; i < 10; i++) {
await jest.advanceTimersByTimeAsync(1000);
}
expect(delayEventRestartError).toHaveBeenCalled();
});
it("falls back to using pure state events when some error occurs while sending delayed events !FailsForLegacy", async () => {
const unrecoverableError = jest.fn();
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(new HTTPError("unknown", 601));
const manager = new TestMembershipManager({}, room, client, () => undefined);
manager.join([focus], focusActive, unrecoverableError);
await waitForMockCall(client.sendStateEvent);
expect(unrecoverableError).not.toHaveBeenCalledWith();
expect(client.sendStateEvent).toHaveBeenCalled();
});
it("retries before failing in case its a network error !FailsForLegacy", async () => {
const unrecoverableError = jest.fn();
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(new HTTPError("unknown", 501));
const manager = new TestMembershipManager(
{ callMemberEventRetryDelayMinimum: 1000, maximumNetworkErrorRetryCount: 7 },
room,
client,
() => undefined,
);
manager.join([focus], focusActive, unrecoverableError);
for (let retries = 0; retries < 7; retries++) {
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(retries + 1);
await jest.advanceTimersByTimeAsync(1000);
}
expect(unrecoverableError).toHaveBeenCalled();
expect(unrecoverableError.mock.lastCall![0].message).toMatch(
"The MembershipManager shut down because of the end condition",
);
expect(client.sendStateEvent).not.toHaveBeenCalled();
});
it("falls back to using pure state events when UnsupportedEndpointError encountered for delayed events !FailsForLegacy", async () => {
const unrecoverableError = jest.fn();
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(
new UnsupportedEndpointError("not supported", "sendDelayedStateEvent"),
);
const manager = new TestMembershipManager({}, room, client, () => undefined);
manager.join([focus], focusActive, unrecoverableError);
await jest.advanceTimersByTimeAsync(1);

expect(unrecoverableError).not.toHaveBeenCalledWith();
expect(client.sendStateEvent).toHaveBeenCalled();
});
});
});
2 changes: 1 addition & 1 deletion spec/unit/matrixrtc/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const membershipTemplate: SessionMembershipData = {
call_id: "",
device_id: "AAAAAAA",
scope: "m.room",
focus_active: { type: "livekit", livekit_service_url: "https://lk.url" },
focus_active: { type: "livekit", focus_selection: "oldest_membership" },
foci_preferred: [
{
livekit_alias: "!alias:something.org",
Expand Down
12 changes: 8 additions & 4 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ import {
validateAuthMetadataAndKeys,
} from "./oidc/index.ts";
import { type EmptyObject } from "./@types/common.ts";
import { UnsupportedEndpointError } from "./errors.ts";

export type Store = IStore;

Expand Down Expand Up @@ -3351,7 +3352,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
txnId?: string,
): Promise<SendDelayedEventResponse> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw Error("Server does not support the delayed events API");
throw new UnsupportedEndpointError("Server does not support the delayed events API", "sendDelayedEvent");
}

this.addThreadRelationIfNeeded(content, threadId, roomId);
Expand All @@ -3374,7 +3375,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
opts: IRequestOpts = {},
): Promise<SendDelayedEventResponse> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw Error("Server does not support the delayed events API");
throw new UnsupportedEndpointError(
"Server does not support the delayed events API",
"sendDelayedStateEvent",
);
}

const pathParams = {
Expand All @@ -3398,7 +3402,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// eslint-disable-next-line
public async _unstable_getDelayedEvents(fromToken?: string): Promise<DelayedEventInfo> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw Error("Server does not support the delayed events API");
throw new UnsupportedEndpointError("Server does not support the delayed events API", "getDelayedEvents");
}

const queryDict = fromToken ? { from: fromToken } : undefined;
Expand All @@ -3420,7 +3424,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
requestOptions: IRequestOpts = {},
): Promise<EmptyObject> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw Error("Server does not support the delayed events API");
throw new UnsupportedEndpointError("Server does not support the delayed events API", "updateDelayedEvent");
}

const path = utils.encodeUri("/delayed_events/$delayId", {
Expand Down
Loading