Skip to content

Commit ffb228b

Browse files
toger5hughns
andauthored
MatrixRTC: refactor MatrixRTCSession MemberManager API (#4610)
* update join and leave internal api. * rename onMembershipUpdate and triggerCallMembershipEventUpdate to onMembershipsUpdate This makes it more clear that we do not talk about our own membership but all memberships in the session * cleanup MembershipManager - add comments and interface how to test this class. - sort methods by public/private - make triggerCallMembershipEventUpdate private * docstrings for getFocusInUse and getActiveFocus * simplify tests and make them only use MembershipManagerInterface methods. This allows to exchange the membershipManager with a different implementation. * convert interface to abstract class. * review (implement interface, make interface internal, dont change public api.) * Make the interface an actual interface. The actual constructor of the class now contains the `Pick` to define what it needs from the client. * move update condition into MembershipManager * renaming public api * Update src/matrixrtc/MatrixRTCSession.ts Co-authored-by: Hugh Nimmo-Smith <[email protected]> * Update src/matrixrtc/MatrixRTCSession.ts Co-authored-by: Hugh Nimmo-Smith <[email protected]> --------- Co-authored-by: Hugh Nimmo-Smith <[email protected]>
1 parent bed4e95 commit ffb228b

File tree

4 files changed

+174
-100
lines changed

4 files changed

+174
-100
lines changed

spec/unit/matrixrtc/MatrixRTCSession.spec.ts

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ limitations under the License.
1717
import { encodeBase64, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
1818
import { KnownMembership } from "../../../src/@types/membership";
1919
import { DEFAULT_EXPIRE_DURATION, SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
20-
import { MembershipManager } from "../../../src/matrixrtc/MembershipManager";
2120
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
2221
import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
2322
import { randomString } from "../../../src/randomstring";
23+
import { flushPromises } from "../../test-utils/flushPromises";
2424
import { makeMockRoom, makeMockRoomState, membershipTemplate } from "./mocks";
2525

2626
const mockFocus = { type: "mock" };
@@ -236,16 +236,15 @@ describe("MatrixRTCSession", () => {
236236
});
237237

238238
async function testSession(membershipData: SessionMembershipData): Promise<void> {
239-
const makeNewMembershipSpy = jest.spyOn(MembershipManager.prototype as any, "makeNewMembership");
240239
sess = MatrixRTCSession.roomSessionForRoom(client, makeMockRoom(membershipData));
241240

242241
sess.joinRoomSession([mockFocus], mockFocus, joinSessionConfig);
243242
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 500))]);
244243

245-
expect(makeNewMembershipSpy).toHaveBeenCalledTimes(1);
244+
expect(sendStateEventMock).toHaveBeenCalledTimes(1);
246245

247246
await Promise.race([sentDelayedState, new Promise((resolve) => setTimeout(resolve, 500))]);
248-
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
247+
expect(sendDelayedStateMock).toHaveBeenCalledTimes(1);
249248
}
250249

251250
it("sends events", async () => {
@@ -323,9 +322,11 @@ describe("MatrixRTCSession", () => {
323322
let sendStateEventMock: jest.Mock;
324323
let sendDelayedStateMock: jest.Mock;
325324
let sendEventMock: jest.Mock;
325+
let updateDelayedEventMock: jest.Mock;
326326

327327
let sentStateEvent: Promise<void>;
328328
let sentDelayedState: Promise<void>;
329+
let updatedDelayedEvent: Promise<void>;
329330

330331
beforeEach(() => {
331332
sentStateEvent = new Promise((resolve) => {
@@ -339,12 +340,15 @@ describe("MatrixRTCSession", () => {
339340
};
340341
});
341342
});
343+
updatedDelayedEvent = new Promise((r) => {
344+
updateDelayedEventMock = jest.fn(r);
345+
});
342346
sendEventMock = jest.fn();
343347
client.sendStateEvent = sendStateEventMock;
344348
client._unstable_sendDelayedStateEvent = sendDelayedStateMock;
345349
client.sendEvent = sendEventMock;
346350

347-
client._unstable_updateDelayedEvent = jest.fn();
351+
client._unstable_updateDelayedEvent = updateDelayedEventMock;
348352

349353
mockRoom = makeMockRoom([]);
350354
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
@@ -482,19 +486,7 @@ describe("MatrixRTCSession", () => {
482486
membershipServerSideExpiryTimeout: 9000,
483487
});
484488

485-
// needed to advance the mock timers properly
486-
// depends on myMembershipManager being created
487-
const scheduledDelayDisconnection = new Promise<void>((resolve) => {
488-
const membershipManager = (sess as any).membershipManager;
489-
const originalFn: () => void = membershipManager.scheduleDelayDisconnection;
490-
membershipManager.scheduleDelayDisconnection = jest.fn(() => {
491-
originalFn.call(membershipManager);
492-
resolve();
493-
});
494-
});
495-
496489
await sendDelayedStateExceedAttempt.then(); // needed to resolve after the send attempt catches
497-
498490
await sendDelayedStateAttempt;
499491
const callProps = (d: number) => {
500492
return [mockRoom!.roomId, { delay: d }, "org.matrix.msc3401.call.member", {}, userStateKey];
@@ -525,11 +517,13 @@ describe("MatrixRTCSession", () => {
525517
await sentDelayedState;
526518

527519
// should have prepared the heartbeat to keep delaying the leave event while still connected
528-
await scheduledDelayDisconnection;
529-
// should have tried updating the delayed leave to test that it wasn't replaced by own state
520+
await updatedDelayedEvent;
530521
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
531-
// should update delayed disconnect
522+
523+
// ensures that we reach the code that schedules the timeout for the next delay update before we advance the timers.
524+
await flushPromises();
532525
jest.advanceTimersByTime(5000);
526+
// should update delayed disconnect
533527
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(2);
534528

535529
jest.useRealTimers();
@@ -561,7 +555,7 @@ describe("MatrixRTCSession", () => {
561555

562556
const onMembershipsChanged = jest.fn();
563557
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
564-
sess.onMembershipUpdate();
558+
sess.onRTCSessionMemberUpdate();
565559

566560
expect(onMembershipsChanged).not.toHaveBeenCalled();
567561
});
@@ -574,7 +568,7 @@ describe("MatrixRTCSession", () => {
574568
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
575569

576570
mockRoom.getLiveTimeline().getState = jest.fn().mockReturnValue(makeMockRoomState([], mockRoom.roomId));
577-
sess.onMembershipUpdate();
571+
sess.onRTCSessionMemberUpdate();
578572

579573
expect(onMembershipsChanged).toHaveBeenCalled();
580574
});
@@ -763,7 +757,7 @@ describe("MatrixRTCSession", () => {
763757
mockRoom.getLiveTimeline().getState = jest
764758
.fn()
765759
.mockReturnValue(makeMockRoomState([membershipTemplate], mockRoom.roomId));
766-
sess.onMembershipUpdate();
760+
sess.onRTCSessionMemberUpdate();
767761

768762
// member2 re-joins which should trigger an immediate re-send
769763
const keysSentPromise2 = new Promise<EncryptionKeysEventContent>((resolve) => {
@@ -772,7 +766,7 @@ describe("MatrixRTCSession", () => {
772766
mockRoom.getLiveTimeline().getState = jest
773767
.fn()
774768
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId));
775-
sess.onMembershipUpdate();
769+
sess.onRTCSessionMemberUpdate();
776770
// but, that immediate resend is throttled so we need to wait a bit
777771
jest.advanceTimersByTime(1000);
778772
const { keys } = await keysSentPromise2;
@@ -825,7 +819,7 @@ describe("MatrixRTCSession", () => {
825819
mockRoom.getLiveTimeline().getState = jest
826820
.fn()
827821
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId));
828-
sess.onMembershipUpdate();
822+
sess.onRTCSessionMemberUpdate();
829823

830824
await keysSentPromise2;
831825

@@ -879,7 +873,7 @@ describe("MatrixRTCSession", () => {
879873
sendEventMock.mockClear();
880874

881875
// these should be a no-op:
882-
sess.onMembershipUpdate();
876+
sess.onRTCSessionMemberUpdate();
883877
expect(sendEventMock).toHaveBeenCalledTimes(0);
884878
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
885879
} finally {
@@ -933,7 +927,7 @@ describe("MatrixRTCSession", () => {
933927
sendEventMock.mockClear();
934928

935929
// this should be a no-op:
936-
sess.onMembershipUpdate();
930+
sess.onRTCSessionMemberUpdate();
937931
expect(sendEventMock).toHaveBeenCalledTimes(0);
938932

939933
// advance time to avoid key throttling
@@ -947,7 +941,7 @@ describe("MatrixRTCSession", () => {
947941
});
948942

949943
// this should re-send the key
950-
sess.onMembershipUpdate();
944+
sess.onRTCSessionMemberUpdate();
951945

952946
await keysSentPromise2;
953947

@@ -1010,7 +1004,7 @@ describe("MatrixRTCSession", () => {
10101004
mockRoom.getLiveTimeline().getState = jest
10111005
.fn()
10121006
.mockReturnValue(makeMockRoomState([membershipTemplate], mockRoom.roomId));
1013-
sess.onMembershipUpdate();
1007+
sess.onRTCSessionMemberUpdate();
10141008

10151009
jest.advanceTimersByTime(10000);
10161010

@@ -1055,7 +1049,7 @@ describe("MatrixRTCSession", () => {
10551049
);
10561050
}
10571051

1058-
sess!.onMembershipUpdate();
1052+
sess!.onRTCSessionMemberUpdate();
10591053

10601054
// advance time to avoid key throttling
10611055
jest.advanceTimersByTime(10000);
@@ -1096,7 +1090,7 @@ describe("MatrixRTCSession", () => {
10961090
mockRoom.getLiveTimeline().getState = jest
10971091
.fn()
10981092
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId));
1099-
sess.onMembershipUpdate();
1093+
sess.onRTCSessionMemberUpdate();
11001094

11011095
await new Promise((resolve) => {
11021096
realSetTimeout(resolve);

src/matrixrtc/MatrixRTCSession.ts

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { decodeBase64, encodeUnpaddedBase64 } from "../base64.ts";
2929
import { KnownMembership } from "../@types/membership.ts";
3030
import { MatrixError, safeGetRetryAfterMs } from "../http-api/errors.ts";
3131
import { MatrixEvent } from "../models/event.ts";
32-
import { MembershipManager } from "./MembershipManager.ts";
32+
import { LegacyMembershipManager, IMembershipManager } from "./MembershipManager.ts";
3333

3434
const logger = rootLogger.getChild("MatrixRTCSession");
3535

@@ -132,7 +132,7 @@ export type JoinSessionConfig = MembershipConfig & EncryptionConfig;
132132
* This class doesn't deal with media at all, just membership & properties of a session.
133133
*/
134134
export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, MatrixRTCSessionEventHandlerMap> {
135-
private membershipManager?: MembershipManager;
135+
private membershipManager?: IMembershipManager;
136136

137137
// The session Id of the call, this is the call_id of the call Member event.
138138
private _callId: string | undefined;
@@ -283,7 +283,8 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
283283
super();
284284
this._callId = memberships[0]?.callId;
285285
const roomState = this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
286-
roomState?.on(RoomStateEvent.Members, this.onMembershipUpdate);
286+
// TODO: double check if this is actually needed. Should be covered by refreshRoom in MatrixRTCSessionManager
287+
roomState?.on(RoomStateEvent.Members, this.onRoomMemberUpdate);
287288
this.setExpiryTimer();
288289
}
289290

@@ -299,14 +300,13 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
299300
* Performs cleanup & removes timers for client shutdown
300301
*/
301302
public async stop(): Promise<void> {
302-
await this.membershipManager?.leaveRoomSession(1000);
303+
await this.membershipManager?.leave(1000);
303304
if (this.expiryTimeout) {
304305
clearTimeout(this.expiryTimeout);
305306
this.expiryTimeout = undefined;
306307
}
307-
this.membershipManager?.stop();
308308
const roomState = this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
309-
roomState?.off(RoomStateEvent.Members, this.onMembershipUpdate);
309+
roomState?.off(RoomStateEvent.Members, this.onRoomMemberUpdate);
310310
}
311311

312312
/**
@@ -324,24 +324,21 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
324324
* @param joinConfig - Additional configuration for the joined session.
325325
*/
326326
public joinRoomSession(fociPreferred: Focus[], fociActive?: Focus, joinConfig?: JoinSessionConfig): void {
327+
this.joinConfig = joinConfig;
327328
if (this.isJoined()) {
328329
logger.info(`Already joined to session in room ${this.room.roomId}: ignoring join call`);
329330
return;
330331
} else {
331-
this.membershipManager = new MembershipManager(joinConfig, this.room, this.client, () =>
332+
this.membershipManager = new LegacyMembershipManager(joinConfig, this.room, this.client, () =>
332333
this.getOldestMembership(),
333334
);
334335
}
335-
this.joinConfig = joinConfig;
336+
this.membershipManager!.join(fociPreferred, fociActive);
336337
this.manageMediaKeys = joinConfig?.manageMediaKeys ?? this.manageMediaKeys;
337-
// TODO: it feels wrong to be doing `setJoined()` and then `joinRoomSession()` non-atomically
338-
// A new api between MembershipManager and the session will need to be defined.
339-
this.membershipManager.setJoined(fociPreferred, fociActive);
340338
if (joinConfig?.manageMediaKeys) {
341339
this.makeNewSenderKey();
342340
this.requestSendCurrentKey();
343341
}
344-
this.membershipManager.joinRoomSession();
345342
this.emit(MatrixRTCSessionEvent.JoinStateChanged, true);
346343
}
347344

@@ -383,12 +380,17 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
383380

384381
logger.info(`Leaving call session in room ${this.room.roomId}`);
385382
this.joinConfig = undefined;
386-
this.membershipManager!.setLeft();
387383
this.manageMediaKeys = false;
384+
const leavePromise = this.membershipManager!.leave(timeout);
388385
this.emit(MatrixRTCSessionEvent.JoinStateChanged, false);
389-
return await this.membershipManager!.leaveRoomSession(timeout);
386+
return await leavePromise;
390387
}
391388

389+
/**
390+
* Get the active focus from the current CallMemberState event
391+
* @returns The focus that is currently in use to connect to this session. This is undefined
392+
* if the client is not connected to this session.
393+
*/
392394
public getActiveFocus(): Focus | undefined {
393395
return this.membershipManager?.getActiveFocus();
394396
}
@@ -650,14 +652,21 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
650652
}
651653

652654
if (soonestExpiry != undefined) {
653-
this.expiryTimeout = setTimeout(this.onMembershipUpdate, soonestExpiry);
655+
this.expiryTimeout = setTimeout(this.onRTCSessionMemberUpdate, soonestExpiry);
654656
}
655657
}
656658

657659
public getOldestMembership(): CallMembership | undefined {
658660
return this.memberships[0];
659661
}
660662

663+
/**
664+
* This method is used when the user is not yet connected to the Session but wants to know what focus
665+
* the users in the session are using to make a decision how it wants/should connect.
666+
*
667+
* See also `getActiveFocus`
668+
* @returns The focus which should be used when joining this session.
669+
*/
661670
public getFocusInUse(): Focus | undefined {
662671
const oldestMembership = this.getOldestMembership();
663672
if (oldestMembership?.getFocusSelection() === "oldest_membership") {
@@ -746,11 +755,35 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
746755
m.sender === this.client.getUserId() && m.deviceId === this.client.getDeviceId();
747756

748757
/**
758+
* @deprecated use onRoomMemberUpdate or onRTCSessionMemberUpdate instead. this should be called when any membership in the call is updated
759+
* the old name might have implied to only need to call this when your own membership changes.
760+
*/
761+
public onMembershipUpdate = (): void => {
762+
this.recalculateSessionMembers();
763+
};
764+
765+
/**
766+
* Call this when the Matrix room members have changed.
767+
*/
768+
public onRoomMemberUpdate = (): void => {
769+
this.recalculateSessionMembers();
770+
};
771+
772+
/**
773+
* Call this when something changed that may impacts the current MatrixRTC members in this session.
774+
*/
775+
public onRTCSessionMemberUpdate = (): void => {
776+
this.recalculateSessionMembers();
777+
};
778+
779+
/**
780+
* Call this when anything that could impact rtc memberships has changed: Room Members or RTC members.
781+
*
749782
* Examines the latest call memberships and handles any encryption key sending or rotation that is needed.
750783
*
751784
* This function should be called when the room members or call memberships might have changed.
752785
*/
753-
public onMembershipUpdate = (): void => {
786+
private recalculateSessionMembers = (): void => {
754787
const oldMemberships = this.memberships;
755788
this.memberships = MatrixRTCSession.callMembershipsForRoom(this.room);
756789

@@ -764,11 +797,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
764797
logger.info(`Memberships for call in room ${this.room.roomId} have changed: emitting`);
765798
this.emit(MatrixRTCSessionEvent.MembershipsChanged, oldMemberships, this.memberships);
766799

767-
if (this.isJoined() && !this.memberships.some(this.isMyMembership)) {
768-
logger.warn("Missing own membership: force re-join");
769-
// TODO: Should this be awaited? And is there anything to tell the focus?
770-
this.membershipManager?.triggerCallMembershipEventUpdate();
771-
}
800+
this.membershipManager?.onRTCSessionMemberUpdate(this.memberships);
772801
}
773802

774803
if (this.manageMediaKeys && this.isJoined()) {

src/matrixrtc/MatrixRTCSessionManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
153153

154154
const wasActiveAndKnown = sess.memberships.length > 0 && !isNewSession;
155155

156-
sess.onMembershipUpdate();
156+
sess.onRTCSessionMemberUpdate();
157157

158158
const nowActive = sess.memberships.length > 0;
159159

0 commit comments

Comments
 (0)