Skip to content

Commit 5a65c84

Browse files
authored
MatrixRTC MembershipManger: remove redundant sendDelayedEventAction and expose status (#4747)
* Remove redundant sendDelayedEventAction We do already have the state `hasMemberEvent` that allows to distinguish the two cases. No need to create two dedicated actions. * fix missing return * Make membership manager an event emitter to inform about status updates. - deprecate isJoined (replaced by isActivated) - move Interface types to types.ts * add tests for status updates. * lint * test "reschedules delayed leave event" in case the delayed event gets canceled * review * fix types * prettier * fix legacy membership manager
1 parent 2090319 commit 5a65c84

File tree

7 files changed

+304
-175
lines changed

7 files changed

+304
-175
lines changed

spec/unit/matrixrtc/MembershipManager.spec.ts

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ limitations under the License.
2020
import { type MockedFunction, type Mock } from "jest-mock";
2121

2222
import { EventType, HTTPError, MatrixError, UnsupportedDelayedEventsEndpointError, type Room } from "../../../src";
23-
import { type Focus, type LivekitFocusActive, type SessionMembershipData } from "../../../src/matrixrtc";
23+
import {
24+
MembershipManagerEvent,
25+
Status,
26+
type Focus,
27+
type LivekitFocusActive,
28+
type SessionMembershipData,
29+
} from "../../../src/matrixrtc";
2430
import { LegacyMembershipManager } from "../../../src/matrixrtc/LegacyMembershipManager";
2531
import { makeMockClient, makeMockRoom, membershipTemplate, mockCallMembership, type MockClient } from "./mocks";
2632
import { MembershipManager } from "../../../src/matrixrtc/NewMembershipManager";
@@ -34,6 +40,14 @@ function waitForMockCall(method: MockedFunction<any>, returnVal?: Promise<any>)
3440
});
3541
});
3642
}
43+
function waitForMockCallOnce(method: MockedFunction<any>, returnVal?: Promise<any>) {
44+
return new Promise<void>((resolve) => {
45+
method.mockImplementationOnce(() => {
46+
resolve();
47+
return returnVal ?? Promise.resolve();
48+
});
49+
});
50+
}
3751

3852
function createAsyncHandle(method: MockedFunction<any>) {
3953
const { reject, resolve, promise } = defer();
@@ -78,16 +92,16 @@ describe.each([
7892
// There is no need to clean up mocks since we will recreate the client.
7993
});
8094

81-
describe("isJoined()", () => {
95+
describe("isActivated()", () => {
8296
it("defaults to false", () => {
8397
const manager = new TestMembershipManager({}, room, client, () => undefined);
84-
expect(manager.isJoined()).toEqual(false);
98+
expect(manager.isActivated()).toEqual(false);
8599
});
86100

87101
it("returns true after join()", () => {
88102
const manager = new TestMembershipManager({}, room, client, () => undefined);
89103
manager.join([]);
90-
expect(manager.isJoined()).toEqual(true);
104+
expect(manager.isActivated()).toEqual(true);
91105
});
92106
});
93107

@@ -125,6 +139,23 @@ describe.each([
125139
{},
126140
"_@alice:example.org_AAAAAAA",
127141
);
142+
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
143+
});
144+
145+
it("reschedules delayed leave event if sending state cancels it", async () => {
146+
const memberManager = new TestMembershipManager(undefined, room, client, () => undefined);
147+
const waitForSendState = waitForMockCall(client.sendStateEvent);
148+
const waitForUpdateDelaye = waitForMockCallOnce(
149+
client._unstable_updateDelayedEvent,
150+
Promise.reject(new MatrixError({ errcode: "M_NOT_FOUND" })),
151+
);
152+
memberManager.join([focus], focusActive);
153+
await waitForSendState;
154+
await waitForUpdateDelaye;
155+
await jest.advanceTimersByTimeAsync(1);
156+
// Once for the initial event and once because of the errcode: "M_NOT_FOUND"
157+
// Different to "sends a membership event and schedules delayed leave when joining a call" where its only called once (1)
158+
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
128159
});
129160

130161
describe("does not prefix the state key with _ for rooms that support user-owned state events", () => {
@@ -505,7 +536,39 @@ describe.each([
505536
await testExpires(10_000, 1_000);
506537
});
507538
});
539+
describe("status updates", () => {
540+
it("starts 'Disconnected' !FailsForLegacy", () => {
541+
const manager = new TestMembershipManager({}, room, client, () => undefined);
542+
expect(manager.status).toBe(Status.Disconnected);
543+
});
544+
it("emits 'Connection' and 'Connected' after join !FailsForLegacy", async () => {
545+
const handleDelayedEvent = createAsyncHandle(client._unstable_sendDelayedStateEvent);
546+
const handleStateEvent = createAsyncHandle(client.sendStateEvent);
508547

548+
const manager = new TestMembershipManager({}, room, client, () => undefined);
549+
expect(manager.status).toBe(Status.Disconnected);
550+
const connectEmit = jest.fn();
551+
manager.on(MembershipManagerEvent.StatusChanged, connectEmit);
552+
manager.join([focus], focusActive);
553+
expect(manager.status).toBe(Status.Connecting);
554+
handleDelayedEvent.resolve();
555+
await jest.advanceTimersByTimeAsync(1);
556+
expect(connectEmit).toHaveBeenCalledWith(Status.Disconnected, Status.Connecting);
557+
handleStateEvent.resolve();
558+
await jest.advanceTimersByTimeAsync(1);
559+
expect(connectEmit).toHaveBeenCalledWith(Status.Connecting, Status.Connected);
560+
});
561+
it("emits 'Disconnecting' and 'Disconnected' after leave !FailsForLegacy", async () => {
562+
const manager = new TestMembershipManager({}, room, client, () => undefined);
563+
const connectEmit = jest.fn();
564+
manager.on(MembershipManagerEvent.StatusChanged, connectEmit);
565+
manager.join([focus], focusActive);
566+
await jest.advanceTimersByTimeAsync(1);
567+
await manager.leave();
568+
expect(connectEmit).toHaveBeenCalledWith(Status.Connected, Status.Disconnecting);
569+
expect(connectEmit).toHaveBeenCalledWith(Status.Disconnecting, Status.Disconnected);
570+
});
571+
});
509572
describe("server error handling", () => {
510573
// Types of server error: 429 rate limit with no retry-after header, 429 with retry-after, 50x server error (maybe retry every second), connection/socket timeout
511574
describe("retries sending delayed leave event", () => {

src/matrixrtc/LegacyMembershipManager.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { type Focus } from "./focus.ts";
2727
import { isLivekitFocusActive } from "./LivekitFocus.ts";
2828
import { type MembershipConfig } from "./MatrixRTCSession.ts";
2929
import { type EmptyObject } from "../@types/common.ts";
30-
import { type IMembershipManager } from "./NewMembershipManager.ts";
30+
import { type IMembershipManager, type MembershipManagerEvent, Status } from "./types.ts";
3131

3232
/**
3333
* This internal class is used by the MatrixRTCSession to manage the local user's own membership of the session.
@@ -103,9 +103,35 @@ export class LegacyMembershipManager implements IMembershipManager {
103103
private getOldestMembership: () => CallMembership | undefined,
104104
) {}
105105

106+
public off(
107+
event: MembershipManagerEvent.StatusChanged,
108+
listener: (oldStatus: Status, newStatus: Status) => void,
109+
): this {
110+
logger.error("off is not implemented on LegacyMembershipManager");
111+
return this;
112+
}
113+
114+
public on(
115+
event: MembershipManagerEvent.StatusChanged,
116+
listener: (oldStatus: Status, newStatus: Status) => void,
117+
): this {
118+
logger.error("on is not implemented on LegacyMembershipManager");
119+
return this;
120+
}
121+
106122
public isJoined(): boolean {
107123
return this.relativeExpiry !== undefined;
108124
}
125+
public isActivated(): boolean {
126+
return this.isJoined();
127+
}
128+
/**
129+
* Unimplemented
130+
* @returns Status.Unknown
131+
*/
132+
public get status(): Status {
133+
return Status.Unknown;
134+
}
109135

110136
public join(fociPreferred: Focus[], fociActive?: Focus): void {
111137
this.ownFocusActive = fociActive;

src/matrixrtc/MatrixRTCSession.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ import { RoomStateEvent } from "../models/room-state.ts";
2525
import { type Focus } from "./focus.ts";
2626
import { KnownMembership } from "../@types/membership.ts";
2727
import { type MatrixEvent } from "../models/event.ts";
28-
import { MembershipManager, type IMembershipManager } from "./NewMembershipManager.ts";
28+
import { MembershipManager } from "./NewMembershipManager.ts";
2929
import { EncryptionManager, type IEncryptionManager, type Statistics } from "./EncryptionManager.ts";
3030
import { LegacyMembershipManager } from "./LegacyMembershipManager.ts";
3131
import { logDurationSync } from "../utils.ts";
32+
import type { IMembershipManager } from "./types.ts";
3233

3334
const logger = rootLogger.getChild("MatrixRTCSession");
3435

0 commit comments

Comments
 (0)