1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-23 17:02:25 +03:00

Split membership manager into legacy variant to improve readability

This commit is contained in:
Half-Shot
2025-11-03 12:07:33 +00:00
parent 4ca30bed25
commit 3e5c1ee6f5
3 changed files with 795 additions and 769 deletions

View File

@@ -31,8 +31,9 @@ import {
type Transport, type Transport,
} from "../../../src/matrixrtc"; } from "../../../src/matrixrtc";
import { makeMockClient, makeMockRoom, sessionMembershipTemplate, mockCallMembership, type MockClient } from "./mocks"; import { makeMockClient, makeMockRoom, sessionMembershipTemplate, mockCallMembership, type MockClient } from "./mocks";
import { MembershipManager, StickyEventMembershipManager } from "../../../src/matrixrtc/MembershipManager.ts"; import { LegacyMembershipManager, StickyEventMembershipManager } from "../../../src/matrixrtc/MembershipManager.ts";
import { SessionMembershipData } from "src/matrixrtc/membership/legacy.ts"; import { SessionMembershipData } from "src/matrixrtc/membership/legacy.ts";
import { RtcMembershipData } from "src/matrixrtc/membership/rtc.ts";
/** /**
* Create a promise that will resolve once a mocked method is called. * Create a promise that will resolve once a mocked method is called.
@@ -90,27 +91,29 @@ describe("MembershipManager", () => {
jest.useFakeTimers(); jest.useFakeTimers();
client = makeMockClient("@alice:example.org", "AAAAAAA"); client = makeMockClient("@alice:example.org", "AAAAAAA");
room = makeMockRoom([sessionMembershipTemplate]); room = makeMockRoom([sessionMembershipTemplate]);
// Provide a default mock that is like the default "non error" server behaviour.
(client._unstable_sendDelayedStateEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue(undefined);
(client._unstable_sendStickyEvent as Mock<any>).mockResolvedValue({ event_id: "id" });
(client._unstable_sendStickyDelayedEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
(client.sendStateEvent as Mock<any>).mockResolvedValue({ event_id: "id" });
}); });
afterEach(() => { afterEach(() => {
jest.useRealTimers(); jest.useRealTimers();
// There is no need to clean up mocks since we will recreate the client. // There is no need to clean up mocks since we will recreate the client.
}); });
describe("LegacyMembershipManager", () => {
beforeEach(() => {
// Provide a default mock that is like the default "non error" server behaviour.
(client._unstable_sendDelayedStateEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue(undefined);
(client.sendStateEvent as Mock<any>).mockResolvedValue({ event_id: "id" });
})
describe("isActivated()", () => { describe("isActivated()", () => {
it("defaults to false", () => { it("defaults to false", () => {
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
expect(manager.isActivated()).toEqual(false); expect(manager.isActivated()).toEqual(false);
}); });
it("returns true after join()", () => { it("returns true after join()", () => {
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
manager.join([]); manager.join([]);
expect(manager.isActivated()).toEqual(true); expect(manager.isActivated()).toEqual(true);
}); });
@@ -124,7 +127,7 @@ describe("MembershipManager", () => {
const updateDelayedEventHandle = createAsyncHandle<void>(client._unstable_updateDelayedEvent as Mock); const updateDelayedEventHandle = createAsyncHandle<void>(client._unstable_updateDelayedEvent as Mock);
// Test // Test
const memberManager = new MembershipManager(undefined, room, client, callSession); const memberManager = new LegacyMembershipManager(undefined, room, client, callSession);
memberManager.join([focus], undefined); memberManager.join([focus], undefined);
// expects // expects
await waitForMockCall(client.sendStateEvent, Promise.resolve({ event_id: "id" })); await waitForMockCall(client.sendStateEvent, Promise.resolve({ event_id: "id" }));
@@ -154,7 +157,7 @@ describe("MembershipManager", () => {
}); });
it("reschedules delayed leave event if sending state cancels it", async () => { it("reschedules delayed leave event if sending state cancels it", async () => {
const memberManager = new MembershipManager(undefined, room, client, callSession); const memberManager = new LegacyMembershipManager(undefined, room, client, callSession);
const waitForSendState = waitForMockCall(client.sendStateEvent); const waitForSendState = waitForMockCall(client.sendStateEvent);
const waitForUpdateDelaye = waitForMockCallOnce( const waitForUpdateDelaye = waitForMockCallOnce(
client._unstable_updateDelayedEvent, client._unstable_updateDelayedEvent,
@@ -223,7 +226,7 @@ describe("MembershipManager", () => {
return Promise.reject(error); return Promise.reject(error);
}); });
}); });
const manager = new MembershipManager( const manager = new LegacyMembershipManager(
{ {
delayedLeaveEventDelayMs: 9000, delayedLeaveEventDelayMs: 9000,
}, },
@@ -286,7 +289,7 @@ describe("MembershipManager", () => {
describe("delayed leave event", () => { describe("delayed leave event", () => {
it("does not try again to schedule a delayed leave event if not supported", () => { it("does not try again to schedule a delayed leave event if not supported", () => {
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent as Mock); const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent as Mock);
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
manager.join([focus]); manager.join([focus]);
delayedHandle.reject?.( delayedHandle.reject?.(
new UnsupportedDelayedEventsEndpointError( new UnsupportedDelayedEventsEndpointError(
@@ -298,14 +301,14 @@ describe("MembershipManager", () => {
}); });
it("does try to schedule a delayed leave event again if rate limited", async () => { it("does try to schedule a delayed leave event again if rate limited", async () => {
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent as Mock); const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent as Mock);
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
manager.join([focus]); manager.join([focus]);
delayedHandle.reject?.(new HTTPError("rate limited", 429, undefined)); delayedHandle.reject?.(new HTTPError("rate limited", 429, undefined));
await jest.advanceTimersByTimeAsync(5000); await jest.advanceTimersByTimeAsync(5000);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2); expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
}); });
it("uses delayedLeaveEventDelayMs from config", () => { it("uses delayedLeaveEventDelayMs from config", () => {
const manager = new MembershipManager({ delayedLeaveEventDelayMs: 123456 }, room, client, callSession); const manager = new LegacyMembershipManager({ delayedLeaveEventDelayMs: 123456 }, room, client, callSession);
manager.join([focus]); manager.join([focus]);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith( expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
room.roomId, room.roomId,
@@ -319,7 +322,7 @@ describe("MembershipManager", () => {
it("rejoins if delayed event is not found (404)", async () => { it("rejoins if delayed event is not found (404)", async () => {
const RESTART_DELAY = 15000; const RESTART_DELAY = 15000;
const manager = new MembershipManager( const manager = new LegacyMembershipManager(
{ delayedLeaveEventRestartMs: RESTART_DELAY }, { delayedLeaveEventRestartMs: RESTART_DELAY },
room, room,
client, client,
@@ -357,7 +360,7 @@ describe("MembershipManager", () => {
}); });
it("uses membershipEventExpiryMs from config", async () => { it("uses membershipEventExpiryMs from config", async () => {
const manager = new MembershipManager( const manager = new LegacyMembershipManager(
{ membershipEventExpiryMs: 1234567 }, { membershipEventExpiryMs: 1234567 },
room, room,
client, client,
@@ -387,7 +390,7 @@ describe("MembershipManager", () => {
}); });
it("does nothing if join called when already joined", async () => { it("does nothing if join called when already joined", async () => {
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
manager.join([focus]); manager.join([focus]);
await waitForMockCall(client.sendStateEvent); await waitForMockCall(client.sendStateEvent);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1); expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
@@ -399,7 +402,7 @@ describe("MembershipManager", () => {
describe("leave()", () => { describe("leave()", () => {
// TODO add rate limit cases. // TODO add rate limit cases.
it("resolves delayed leave event when leave is called", async () => { it("resolves delayed leave event when leave is called", async () => {
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
manager.join([focus]); manager.join([focus]);
await jest.advanceTimersByTimeAsync(1); await jest.advanceTimersByTimeAsync(1);
await manager.leave(); await manager.leave();
@@ -407,7 +410,7 @@ describe("MembershipManager", () => {
expect(client.sendStateEvent).toHaveBeenCalled(); expect(client.sendStateEvent).toHaveBeenCalled();
}); });
it("send leave event when leave is called and resolving delayed leave fails", async () => { it("send leave event when leave is called and resolving delayed leave fails", async () => {
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
manager.join([focus]); manager.join([focus]);
await jest.advanceTimersByTimeAsync(1); await jest.advanceTimersByTimeAsync(1);
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValue("unknown"); (client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValue("unknown");
@@ -422,7 +425,7 @@ describe("MembershipManager", () => {
); );
}); });
it("does nothing if not joined", () => { it("does nothing if not joined", () => {
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
expect(async () => await manager.leave()).not.toThrow(); expect(async () => await manager.leave()).not.toThrow();
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled(); expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
expect(client.sendStateEvent).not.toHaveBeenCalled(); expect(client.sendStateEvent).not.toHaveBeenCalled();
@@ -431,7 +434,7 @@ describe("MembershipManager", () => {
describe("onRTCSessionMemberUpdate()", () => { describe("onRTCSessionMemberUpdate()", () => {
it("does nothing if not joined", async () => { it("does nothing if not joined", async () => {
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
await manager.onRTCSessionMemberUpdate([mockCallMembership(sessionMembershipTemplate, room.roomId)]); await manager.onRTCSessionMemberUpdate([mockCallMembership(sessionMembershipTemplate, room.roomId)]);
await jest.advanceTimersToNextTimerAsync(); await jest.advanceTimersToNextTimerAsync();
expect(client.sendStateEvent).not.toHaveBeenCalled(); expect(client.sendStateEvent).not.toHaveBeenCalled();
@@ -439,7 +442,7 @@ describe("MembershipManager", () => {
expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled(); expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled();
}); });
it("does nothing if own membership still present", async () => { it("does nothing if own membership still present", async () => {
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
manager.join([focus], focusActive); manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1); await jest.advanceTimersByTimeAsync(1);
const myMembership = (client.sendStateEvent as Mock).mock.calls[0][2]; const myMembership = (client.sendStateEvent as Mock).mock.calls[0][2];
@@ -463,7 +466,7 @@ describe("MembershipManager", () => {
expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled(); expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled();
}); });
it("recreates membership if it is missing", async () => { it("recreates membership if it is missing", async () => {
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
manager.join([focus], focusActive); manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1); await jest.advanceTimersByTimeAsync(1);
// clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate` // clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
@@ -481,7 +484,7 @@ describe("MembershipManager", () => {
}); });
it("updates the UpdateExpiry entry in the action scheduler", async () => { it("updates the UpdateExpiry entry in the action scheduler", async () => {
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
manager.join([focus], focusActive); manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1); await jest.advanceTimersByTimeAsync(1);
// clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate` // clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
@@ -510,7 +513,7 @@ describe("MembershipManager", () => {
// TODO: Not sure about this name // TODO: Not sure about this name
describe("background timers", () => { describe("background timers", () => {
it("sends only one keep-alive for delayed leave event per `delayedLeaveEventRestartMs`", async () => { it("sends only one keep-alive for delayed leave event per `delayedLeaveEventRestartMs`", async () => {
const manager = new MembershipManager( const manager = new LegacyMembershipManager(
{ delayedLeaveEventRestartMs: 10_000, delayedLeaveEventDelayMs: 30_000 }, { delayedLeaveEventRestartMs: 10_000, delayedLeaveEventDelayMs: 30_000 },
room, room,
client, client,
@@ -541,7 +544,7 @@ describe("MembershipManager", () => {
// the expiration logic still makes sense. // the expiration logic still makes sense.
// TODO: Add git commit when we removed it. // TODO: Add git commit when we removed it.
async function testExpires(expire: number, headroom?: number) { async function testExpires(expire: number, headroom?: number) {
const manager = new MembershipManager( const manager = new LegacyMembershipManager(
{ membershipEventExpiryMs: expire, membershipEventExpiryHeadroomMs: headroom }, { membershipEventExpiryMs: expire, membershipEventExpiryHeadroomMs: headroom },
room, room,
client, client,
@@ -570,14 +573,14 @@ describe("MembershipManager", () => {
describe("status updates", () => { describe("status updates", () => {
it("starts 'Disconnected'", () => { it("starts 'Disconnected'", () => {
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
expect(manager.status).toBe(Status.Disconnected); expect(manager.status).toBe(Status.Disconnected);
}); });
it("emits 'Connection' and 'Connected' after join", async () => { it("emits 'Connection' and 'Connected' after join", async () => {
const handleDelayedEvent = createAsyncHandle<void>(client._unstable_sendDelayedStateEvent); const handleDelayedEvent = createAsyncHandle<void>(client._unstable_sendDelayedStateEvent);
const handleStateEvent = createAsyncHandle<void>(client.sendStateEvent); const handleStateEvent = createAsyncHandle<void>(client.sendStateEvent);
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
expect(manager.status).toBe(Status.Disconnected); expect(manager.status).toBe(Status.Disconnected);
const connectEmit = jest.fn(); const connectEmit = jest.fn();
manager.on(MembershipManagerEvent.StatusChanged, connectEmit); manager.on(MembershipManagerEvent.StatusChanged, connectEmit);
@@ -591,7 +594,7 @@ describe("MembershipManager", () => {
expect(connectEmit).toHaveBeenCalledWith(Status.Connecting, Status.Connected); expect(connectEmit).toHaveBeenCalledWith(Status.Connecting, Status.Connected);
}); });
it("emits 'Disconnecting' and 'Disconnected' after leave", async () => { it("emits 'Disconnecting' and 'Disconnected' after leave", async () => {
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
const connectEmit = jest.fn(); const connectEmit = jest.fn();
manager.on(MembershipManagerEvent.StatusChanged, connectEmit); manager.on(MembershipManagerEvent.StatusChanged, connectEmit);
manager.join([focus], focusActive); manager.join([focus], focusActive);
@@ -607,7 +610,7 @@ describe("MembershipManager", () => {
it("sends retry if call membership event is still valid at time of retry", async () => { it("sends retry if call membership event is still valid at time of retry", async () => {
const handle = createAsyncHandle(client._unstable_sendDelayedStateEvent); const handle = createAsyncHandle(client._unstable_sendDelayedStateEvent);
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
manager.join([focus], focusActive); manager.join([focus], focusActive);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1); expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
@@ -634,7 +637,7 @@ describe("MembershipManager", () => {
new Headers({ "Retry-After": "1" }), new Headers({ "Retry-After": "1" }),
), ),
); );
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
// Should call _unstable_sendDelayedStateEvent but not sendStateEvent because of the // Should call _unstable_sendDelayedStateEvent but not sendStateEvent because of the
// RateLimit error. // RateLimit error.
manager.join([focus], focusActive); manager.join([focus], focusActive);
@@ -654,7 +657,7 @@ describe("MembershipManager", () => {
it("abandons retry loop if leave() was called before sending state event", async () => { it("abandons retry loop if leave() was called before sending state event", async () => {
const handle = createAsyncHandle(client._unstable_sendDelayedStateEvent); const handle = createAsyncHandle(client._unstable_sendDelayedStateEvent);
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
manager.join([focus], focusActive); manager.join([focus], focusActive);
handle.reject?.( handle.reject?.(
new MatrixError( new MatrixError(
@@ -689,7 +692,7 @@ describe("MembershipManager", () => {
new Headers({ "Retry-After": "1" }), new Headers({ "Retry-After": "1" }),
), ),
); );
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
manager.join([focus], focusActive); manager.join([focus], focusActive);
// Hit rate limit // Hit rate limit
@@ -722,7 +725,7 @@ describe("MembershipManager", () => {
new Headers({ "Retry-After": "2" }), new Headers({ "Retry-After": "2" }),
), ),
); );
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
manager.join([focus], focusActive, delayEventSendError); manager.join([focus], focusActive, delayEventSendError);
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
@@ -742,7 +745,7 @@ describe("MembershipManager", () => {
new Headers({ "Retry-After": "1" }), new Headers({ "Retry-After": "1" }),
), ),
); );
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
manager.join([focus], focusActive, delayEventRestartError); manager.join([focus], focusActive, delayEventRestartError);
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
@@ -753,7 +756,7 @@ describe("MembershipManager", () => {
it("falls back to using pure state events when some error occurs while sending delayed events", async () => { it("falls back to using pure state events when some error occurs while sending delayed events", async () => {
const unrecoverableError = jest.fn(); const unrecoverableError = jest.fn();
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(new HTTPError("unknown", 601)); (client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(new HTTPError("unknown", 601));
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
manager.join([focus], focusActive, unrecoverableError); manager.join([focus], focusActive, unrecoverableError);
await waitForMockCall(client.sendStateEvent); await waitForMockCall(client.sendStateEvent);
expect(unrecoverableError).not.toHaveBeenCalledWith(); expect(unrecoverableError).not.toHaveBeenCalledWith();
@@ -762,7 +765,7 @@ describe("MembershipManager", () => {
it("retries before failing in case its a network error", async () => { it("retries before failing in case its a network error", async () => {
const unrecoverableError = jest.fn(); const unrecoverableError = jest.fn();
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(new HTTPError("unknown", 501)); (client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(new HTTPError("unknown", 501));
const manager = new MembershipManager( const manager = new LegacyMembershipManager(
{ networkErrorRetryMs: 1000, maximumNetworkErrorRetryCount: 7 }, { networkErrorRetryMs: 1000, maximumNetworkErrorRetryCount: 7 },
room, room,
client, client,
@@ -784,7 +787,7 @@ describe("MembershipManager", () => {
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue( (client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(
new UnsupportedDelayedEventsEndpointError("not supported", "sendDelayedStateEvent"), new UnsupportedDelayedEventsEndpointError("not supported", "sendDelayedStateEvent"),
); );
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
manager.join([focus], focusActive, unrecoverableError); manager.join([focus], focusActive, unrecoverableError);
await jest.advanceTimersByTimeAsync(1); await jest.advanceTimersByTimeAsync(1);
@@ -794,7 +797,7 @@ describe("MembershipManager", () => {
}); });
describe("probablyLeft", () => { describe("probablyLeft", () => {
it("emits probablyLeft when the membership manager could not hear back from the server for the duration of the delayed event", async () => { it("emits probablyLeft when the membership manager could not hear back from the server for the duration of the delayed event", async () => {
const manager = new MembershipManager( const manager = new LegacyMembershipManager(
{ delayedLeaveEventDelayMs: 10000 }, { delayedLeaveEventDelayMs: 10000 },
room, room,
client, client,
@@ -852,7 +855,7 @@ describe("MembershipManager", () => {
describe("updateCallIntent()", () => { describe("updateCallIntent()", () => {
it("should fail if the user has not joined the call", async () => { it("should fail if the user has not joined the call", async () => {
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
// After joining we want our own focus to be the one we select. // After joining we want our own focus to be the one we select.
try { try {
await manager.updateCallIntent("video"); await manager.updateCallIntent("video");
@@ -861,7 +864,7 @@ describe("MembershipManager", () => {
}); });
it("can adjust the intent", async () => { it("can adjust the intent", async () => {
const manager = new MembershipManager({}, room, client, callSession); const manager = new LegacyMembershipManager({}, room, client, callSession);
manager.join([]); manager.join([]);
expect(manager.isActivated()).toEqual(true); expect(manager.isActivated()).toEqual(true);
const membership = mockCallMembership({ ...sessionMembershipTemplate, user_id: client.getUserId()! }, room.roomId); const membership = mockCallMembership({ ...sessionMembershipTemplate, user_id: client.getUserId()! }, room.roomId);
@@ -874,7 +877,7 @@ describe("MembershipManager", () => {
}); });
it("does nothing if the intent doesn't change", async () => { it("does nothing if the intent doesn't change", async () => {
const manager = new MembershipManager({ callIntent: "video" }, room, client, callSession); const manager = new LegacyMembershipManager({ callIntent: "video" }, room, client, callSession);
manager.join([]); manager.join([]);
expect(manager.isActivated()).toEqual(true); expect(manager.isActivated()).toEqual(true);
const membership = mockCallMembership( const membership = mockCallMembership(
@@ -886,6 +889,7 @@ describe("MembershipManager", () => {
expect(client.sendStateEvent).toHaveBeenCalledTimes(0); expect(client.sendStateEvent).toHaveBeenCalledTimes(0);
}); });
}); });
});
describe("StickyEventMembershipManager", () => { describe("StickyEventMembershipManager", () => {
beforeEach(() => { beforeEach(() => {
@@ -914,15 +918,15 @@ describe("MembershipManager", () => {
{ {
application: { type: "m.call" }, application: { type: "m.call" },
member: { member: {
user_id: "@alice:example.org", claimed_user_id: "@alice:example.org",
id: "_@alice:example.org_AAAAAAA_m.call", id: "_@alice:example.org_AAAAAAA_m.call",
device_id: "AAAAAAA", claimed_device_id: "AAAAAAA",
}, },
slot_id: "m.call#", slot_id: "m.call#",
rtc_transports: [focus], rtc_transports: [focus],
versions: [], versions: [],
msc4354_sticky_key: "_@alice:example.org_AAAAAAA_m.call", msc4354_sticky_key: "_@alice:example.org_AAAAAAA_m.call",
}, } satisfies RtcMembershipData,
); );
updateDelayedEventHandle.resolve?.(); updateDelayedEventHandle.resolve?.();
@@ -949,7 +953,7 @@ it("Should prefix log with MembershipManager used", () => {
const client = makeMockClient("@alice:example.org", "AAAAAAA"); const client = makeMockClient("@alice:example.org", "AAAAAAA");
const room = makeMockRoom([sessionMembershipTemplate]); const room = makeMockRoom([sessionMembershipTemplate]);
const membershipManager = new MembershipManager(undefined, room, client, callSession); const membershipManager = new LegacyMembershipManager(undefined, room, client, callSession);
const spy = jest.spyOn(console, "error"); const spy = jest.spyOn(console, "error");
// Double join // Double join

View File

@@ -24,7 +24,7 @@ import { KnownMembership } from "../@types/membership.ts";
import { type ISendEventResponse } from "../@types/requests.ts"; import { type ISendEventResponse } from "../@types/requests.ts";
import { CallMembership } from "./CallMembership.ts"; import { CallMembership } from "./CallMembership.ts";
import { RoomStateEvent } from "../models/room-state.ts"; import { RoomStateEvent } from "../models/room-state.ts";
import { MembershipManager, StickyEventMembershipManager } from "./MembershipManager.ts"; import { LegacyMembershipManager, StickyEventMembershipManager } from "./MembershipManager.ts";
import { EncryptionManager, type IEncryptionManager } from "./EncryptionManager.ts"; import { EncryptionManager, type IEncryptionManager } from "./EncryptionManager.ts";
import { deepCompare, logDurationSync } from "../utils.ts"; import { deepCompare, logDurationSync } from "../utils.ts";
import { import {
@@ -618,7 +618,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
this.slotDescription, this.slotDescription,
this.logger, this.logger,
) )
: new MembershipManager(joinConfig, this.roomSubset, this.client, this.slotDescription, this.logger); : new LegacyMembershipManager(joinConfig, this.roomSubset, this.client, this.slotDescription, this.logger);
this.reEmitter.reEmit(this.membershipManager!, [ this.reEmitter.reEmit(this.membershipManager!, [
MembershipManagerEvent.ProbablyLeft, MembershipManagerEvent.ProbablyLeft,

View File

@@ -29,9 +29,13 @@ import { type Room } from "../models/room.ts";
import { import {
type CallMembership, type CallMembership,
DEFAULT_EXPIRE_DURATION, DEFAULT_EXPIRE_DURATION,
type RtcMembershipData,
type SessionMembershipData,
} from "./CallMembership.ts"; } from "./CallMembership.ts";
import type {
RtcMembershipData,
} from "./membership/rtc.ts"
import type {
SessionMembershipData,
} from "./membership/legacy.ts"
import { type Transport, isMyMembership, type RTCCallIntent, Status, SlotDescription } from "./types.ts"; import { type Transport, isMyMembership, type RTCCallIntent, Status, SlotDescription } from "./types.ts";
import { type MembershipConfig, type SessionConfig } from "./MatrixRTCSession.ts"; import { type MembershipConfig, type SessionConfig } from "./MatrixRTCSession.ts";
import { ActionScheduler, type ActionUpdate } from "./MembershipManagerActionScheduler.ts"; import { ActionScheduler, type ActionUpdate } from "./MembershipManagerActionScheduler.ts";
@@ -181,7 +185,7 @@ type MembershipManagerClient = Pick<
* - Stop the timer for the delay refresh * - Stop the timer for the delay refresh
* - Stop the timer for updating the state event * - Stop the timer for updating the state event
*/ */
export class MembershipManager export abstract class MembershipManager<MembershipData extends SessionMembershipData|RtcMembershipData>
extends TypedEventEmitter<MembershipManagerEvent, MembershipManagerEventHandlerMap> extends TypedEventEmitter<MembershipManagerEvent, MembershipManagerEventHandlerMap>
implements IMembershipManager implements IMembershipManager
{ {
@@ -382,7 +386,7 @@ export class MembershipManager
protected memberId: string; protected memberId: string;
protected rtcTransport?: Transport; protected rtcTransport?: Transport;
/** @deprecated This will be removed in favor or rtcTransport becoming a list of actively used transports */ /** @deprecated This will be removed in favor or rtcTransport becoming a list of actively used transports */
private fociPreferred?: Transport[]; protected fociPreferred?: Transport[];
// Config: // Config:
private delayedLeaveEventDelayMsOverride?: number; private delayedLeaveEventDelayMsOverride?: number;
@@ -473,15 +477,7 @@ export class MembershipManager
} }
} }
// an abstraction to switch between sending state or a sticky event protected abstract clientSendDelayedDisconnectMembership: () => Promise<SendDelayedEventResponse>;
protected clientSendDelayedDisconnectMembership: () => Promise<SendDelayedEventResponse> = () =>
this.client._unstable_sendDelayedStateEvent(
this.room.roomId,
{ delay: this.delayedLeaveEventDelayMs },
EventType.GroupCallMemberPrefix,
{},
this.memberId,
);
// HANDLERS (used in the membershipLoopHandler) // HANDLERS (used in the membershipLoopHandler)
private async sendOrResendDelayedLeaveEvent(): Promise<ActionUpdate> { private async sendOrResendDelayedLeaveEvent(): Promise<ActionUpdate> {
@@ -669,16 +665,9 @@ export class MembershipManager
}); });
} }
protected clientSendMembership: ( protected abstract clientSendMembership: (
myMembership: RtcMembershipData | SessionMembershipData | EmptyObject, myMembership: MembershipData | EmptyObject,
) => Promise<ISendEventResponse> = (myMembership) => { ) => Promise<ISendEventResponse>;
return this.client.sendStateEvent(
this.room.roomId,
EventType.GroupCallMemberPrefix,
myMembership as EmptyObject | SessionMembershipData,
this.memberId,
);
};
private async sendJoinEvent(): Promise<ActionUpdate> { private async sendJoinEvent(): Promise<ActionUpdate> {
return await this.clientSendMembership(this.makeMyMembership(this.membershipEventExpiryMs)) return await this.clientSendMembership(this.makeMyMembership(this.membershipEventExpiryMs))
@@ -771,30 +760,7 @@ export class MembershipManager
/** /**
* Constructs our own membership * Constructs our own membership
*/ */
protected makeMyMembership(expires: number): SessionMembershipData | RtcMembershipData { protected abstract makeMyMembership(expires: number): MembershipData;
const ownMembership = this.ownMembership;
const focusObjects =
this.rtcTransport === undefined
? {
focus_active: { type: "livekit", focus_selection: "oldest_membership" } as const,
foci_preferred: this.fociPreferred ?? [],
}
: {
focus_active: { type: "livekit", focus_selection: "multi_sfu" } as const,
foci_preferred: [this.rtcTransport, ...(this.fociPreferred ?? [])],
};
return {
"application": this.slotDescription.application,
"call_id": this.slotDescription.id,
"scope": "m.room",
"device_id": this.deviceId,
expires,
"m.call.intent": this.callIntent,
...focusObjects,
...(ownMembership !== undefined ? { created_ts: ownMembership.createdTs() } : undefined),
};
}
// Error checks and handlers // Error checks and handlers
@@ -1020,11 +986,65 @@ export class MembershipManager
} }
} }
/**
* Handles sending membership for MSC3401 RTC events.
*/
export class LegacyMembershipManager extends MembershipManager<SessionMembershipData> {
protected clientSendDelayedDisconnectMembership: () => Promise<SendDelayedEventResponse> = () =>
this.client._unstable_sendDelayedStateEvent(
this.room.roomId,
{ delay: this.delayedLeaveEventDelayMs },
EventType.GroupCallMemberPrefix,
{},
this.memberId,
);
protected makeMyMembership(expires: number): SessionMembershipData {
const ownMembership = this.ownMembership;
const focusObjects =
this.rtcTransport === undefined
? {
focus_active: { type: "livekit", focus_selection: "oldest_membership" } as const,
foci_preferred: this.fociPreferred ?? [],
}
: {
focus_active: { type: "livekit", focus_selection: "multi_sfu" } as const,
foci_preferred: [this.rtcTransport, ...(this.fociPreferred ?? [])],
};
return {
"application": this.slotDescription.application,
"call_id": this.slotDescription.id ?? "",
"scope": "m.room",
"device_id": this.deviceId,
expires,
"m.call.intent": this.callIntent,
...focusObjects,
...(ownMembership !== undefined ? { created_ts: ownMembership.createdTs() } : undefined),
};
}
protected clientSendMembership: (
myMembership: SessionMembershipData | EmptyObject,
) => Promise<ISendEventResponse> = (myMembership) => {
return this.client.sendStateEvent(
this.room.roomId,
EventType.GroupCallMemberPrefix,
myMembership,
this.memberId,
);
};
}
/** /**
* Implementation of the Membership manager that uses sticky events * Implementation of the Membership manager that uses sticky events
* rather than state events. * rather than state events.
*
* This exclusively sends RTCMembershipData
*/ */
export class StickyEventMembershipManager extends MembershipManager { export class StickyEventMembershipManager extends MembershipManager<RtcMembershipData> {
public constructor( public constructor(
joinConfig: (SessionConfig & MembershipConfig) | undefined, joinConfig: (SessionConfig & MembershipConfig) | undefined,
room: Pick<Room, "getLiveTimeline" | "roomId" | "getVersion">, room: Pick<Room, "getLiveTimeline" | "roomId" | "getVersion">,
@@ -1036,6 +1056,8 @@ export class StickyEventMembershipManager extends MembershipManager {
super(joinConfig, room, clientWithSticky, sessionDescription, parentLogger); super(joinConfig, room, clientWithSticky, sessionDescription, parentLogger);
} }
protected readonly eventType = EventType.RTCMembership;
protected clientSendDelayedDisconnectMembership: () => Promise<SendDelayedEventResponse> = () => protected clientSendDelayedDisconnectMembership: () => Promise<SendDelayedEventResponse> = () =>
this.clientWithSticky._unstable_sendStickyDelayedEvent( this.clientWithSticky._unstable_sendStickyDelayedEvent(
this.room.roomId, this.room.roomId,
@@ -1047,7 +1069,7 @@ export class StickyEventMembershipManager extends MembershipManager {
); );
protected clientSendMembership: ( protected clientSendMembership: (
myMembership: RtcMembershipData | SessionMembershipData | EmptyObject, myMembership: RtcMembershipData | EmptyObject,
) => Promise<ISendEventResponse> = (myMembership) => { ) => Promise<ISendEventResponse> = (myMembership) => {
return this.clientWithSticky._unstable_sendStickyEvent( return this.clientWithSticky._unstable_sendStickyEvent(
this.room.roomId, this.room.roomId,
@@ -1066,7 +1088,7 @@ export class StickyEventMembershipManager extends MembershipManager {
return super.actionUpdateFromErrors(e, t, StickyEventMembershipManager.nameMap.get(m) ?? "unknown"); return super.actionUpdateFromErrors(e, t, StickyEventMembershipManager.nameMap.get(m) ?? "unknown");
} }
protected makeMyMembership(expires: number): SessionMembershipData | RtcMembershipData { protected makeMyMembership(_expires: number): RtcMembershipData {
const ownMembership = this.ownMembership; const ownMembership = this.ownMembership;
const relationObject = ownMembership?.eventId const relationObject = ownMembership?.eventId
@@ -1079,7 +1101,7 @@ export class StickyEventMembershipManager extends MembershipManager {
}, },
slot_id: slotDescriptionToId(this.slotDescription), slot_id: slotDescriptionToId(this.slotDescription),
rtc_transports: this.rtcTransport ? [this.rtcTransport] : [], rtc_transports: this.rtcTransport ? [this.rtcTransport] : [],
member: { device_id: this.deviceId, user_id: this.client.getUserId()!, id: this.memberId }, member: { claimed_device_id: this.deviceId, claimed_user_id: this.client.getUserId()!, id: this.memberId },
versions: [], versions: [],
...relationObject, ...relationObject,
}; };