Skip to content

Commit 0dbc652

Browse files
committed
Make membership manager only be responsible for sessions starts
Signed-off-by: Timo K <[email protected]>
1 parent 2042f75 commit 0dbc652

File tree

4 files changed

+89
-68
lines changed

4 files changed

+89
-68
lines changed

spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { RoomStateEvent } from "../../../src/models/room-state";
1919
import { MatrixRTCSessionManager, MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
2020
import { makeMockRoom, type MembershipData, membershipTemplate, mockRoomState, mockRTCEvent } from "./mocks";
2121
import { logger } from "../../../src/logger";
22+
import { RoomStickyEventsEvent } from "../../../src/models/room-sticky-events";
2223

2324
describe.each([{ eventKind: "sticky" }, { eventKind: "memberState" }])(
2425
"MatrixRTCSessionManager ($eventKind)",
@@ -30,10 +31,15 @@ describe.each([{ eventKind: "sticky" }, { eventKind: "memberState" }])(
3031
mockRoomState(room, [{ user_id: membershipTemplate.user_id }]);
3132
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
3233
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
33-
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
34+
roomState.emit(RoomStateEvent.Events, membEvent, roomState, null);
3435
} else {
35-
membershipData.splice(0, 1, { user_id: membershipTemplate.user_id });
36-
client.emit(ClientEvent.Event, mockRTCEvent(membershipData[0], room.roomId, 10000));
36+
const previousData = membershipData.splice(0, 1, {
37+
user_id: membershipTemplate.user_id,
38+
msc4354_sticky_key: membershipTemplate.msc4354_sticky_key,
39+
})[0];
40+
const current = mockRTCEvent(membershipData[0], room.roomId, 10000);
41+
const previous = mockRTCEvent(previousData, room.roomId, 10000);
42+
room.emit(RoomStickyEventsEvent.Update, [], [{ current, previous }], []);
3743
}
3844
}
3945

spec/unit/matrixrtc/mocks.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,20 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { EventEmitter } from "stream";
1817
import { type Mocked } from "jest-mock";
1918

20-
import { EventType, type Room, RoomEvent, type MatrixClient, type MatrixEvent } from "../../../src";
19+
import {
20+
EventType,
21+
type Room,
22+
RoomEvent,
23+
type MatrixClient,
24+
type MatrixEvent,
25+
Direction,
26+
TypedEventEmitter,
27+
} from "../../../src";
2128
import { CallMembership, type SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
2229
import { secureRandomString } from "../../../src/randomstring";
30+
import { type StickyMatrixEvent } from "../../../src/models/room-sticky-events";
2331

2432
export type MembershipData = (SessionMembershipData | {}) & { user_id: string };
2533

@@ -81,7 +89,7 @@ export function makeMockRoom(
8189
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
8290
const roomState = makeMockRoomState(useStickyEvents ? [] : membershipData, roomId);
8391
const ts = Date.now();
84-
const room = Object.assign(new EventEmitter(), {
92+
const room = Object.assign(new TypedEventEmitter(), {
8593
roomId: roomId,
8694
hasMembershipState: jest.fn().mockReturnValue(true),
8795
getLiveTimeline: jest.fn().mockReturnValue({
@@ -102,14 +110,13 @@ export function makeMockRoom(
102110

103111
function makeMockRoomState(membershipData: MembershipData[], roomId: string) {
104112
const events = membershipData.map((m) => mockRTCEvent(m, roomId));
113+
105114
const keysAndEvents = events.map((e) => {
106115
const data = e.getContent() as SessionMembershipData;
107116
return [`_${e.sender?.userId}_${data.device_id}`];
108117
});
109118

110-
return {
111-
on: jest.fn(),
112-
off: jest.fn(),
119+
return Object.assign(new TypedEventEmitter(), {
113120
getStateEvents: (_: string, stateKey: string) => {
114121
if (stateKey !== undefined) return keysAndEvents.find(([k]) => k === stateKey)?.[1];
115122
return events;
@@ -128,11 +135,17 @@ function makeMockRoomState(membershipData: MembershipData[], roomId: string) {
128135
},
129136
],
130137
]),
131-
};
138+
});
132139
}
133140

134141
export function mockRoomState(room: Room, membershipData: MembershipData[]): void {
135-
room.getLiveTimeline().getState = jest.fn().mockReturnValue(makeMockRoomState(membershipData, room.roomId));
142+
const prevState = room.getLiveTimeline().getState(Direction.Forward)!;
143+
const newState = makeMockRoomState(membershipData, room.roomId);
144+
room.getLiveTimeline().getState = jest
145+
.fn()
146+
.mockReturnValue(
147+
Object.assign(prevState, { events: newState.events, getStateEvents: newState.getStateEvents }),
148+
);
136149
}
137150

138151
export function makeMockEvent(
@@ -160,7 +173,7 @@ export function mockRTCEvent(
160173
roomId: string,
161174
stickyDuration?: number,
162175
timestamp?: number,
163-
): MatrixEvent {
176+
): StickyMatrixEvent {
164177
return {
165178
...makeMockEvent(
166179
EventType.GroupCallMemberPrefix,
@@ -171,7 +184,7 @@ export function mockRTCEvent(
171184
!stickyDuration && "device_id" in membershipData ? `_${sender}_${membershipData.device_id}` : "",
172185
),
173186
unstableStickyExpiresAt: stickyDuration,
174-
} as unknown as MatrixEvent;
187+
} as unknown as StickyMatrixEvent;
175188
}
176189

177190
export function mockCallMembership(membershipData: MembershipData, roomId: string): CallMembership {

src/matrixrtc/MatrixRTCSession.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export type MatrixRTCSessionEventHandlerMap = {
7575
[MatrixRTCSessionEvent.MembershipsChanged]: (
7676
oldMemberships: CallMembership[],
7777
newMemberships: CallMembership[],
78+
session: MatrixRTCSession,
7879
) => void;
7980
[MatrixRTCSessionEvent.JoinStateChanged]: (isJoined: boolean) => void;
8081
[MatrixRTCSessionEvent.EncryptionKeyChanged]: (
@@ -483,11 +484,14 @@ export class MatrixRTCSession extends TypedEventEmitter<
483484
* this class.
484485
* Outside of tests this most likely will be a full room, however.
485486
* @deprecated Relying on a full Room object being available here is an anti-pattern. You should be tracking
486-
* the room object in your own code and passing it in when needed.
487+
* the room object in your own code and passing it in when needed. use roomId instead.
487488
*/
488489
public get room(): Room {
489490
return this.roomSubset as Room;
490491
}
492+
public get roomId(): string {
493+
return this.roomSubset.roomId;
494+
}
491495

492496
/**
493497
* This constructs a room session. When using MatrixRTC inside the js-sdk this is expected
@@ -532,8 +536,8 @@ export class MatrixRTCSession extends TypedEventEmitter<
532536
super();
533537
this.logger = rootLogger.getChild(`[MatrixRTCSession ${roomSubset.roomId}]`);
534538
const roomState = this.roomSubset.getLiveTimeline().getState(EventTimeline.FORWARDS);
535-
// TODO: double check if this is actually needed. Should be covered by refreshRoom in MatrixRTCSessionManager
536539
roomState?.on(RoomStateEvent.Members, this.onRoomMemberUpdate);
540+
roomState?.on(RoomStateEvent.Events, this.onRoomStateUpdate);
537541
this.roomSubset.on(RoomStickyEventsEvent.Update, this.onStickyEventUpdate);
538542

539543
this.setExpiryTimer();
@@ -557,6 +561,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
557561
}
558562
const roomState = this.roomSubset.getLiveTimeline().getState(EventTimeline.FORWARDS);
559563
roomState?.off(RoomStateEvent.Members, this.onRoomMemberUpdate);
564+
roomState?.off(RoomStateEvent.Events, this.onRoomStateUpdate);
560565
this.roomSubset.off(RoomStickyEventsEvent.Update, this.onStickyEventUpdate);
561566
}
562567

@@ -857,6 +862,13 @@ export class MatrixRTCSession extends TypedEventEmitter<
857862
this.recalculateSessionMembers();
858863
}
859864
};
865+
/**
866+
* Call this when a sticky event update has occured.
867+
*/
868+
private readonly onRoomStateUpdate = (event: MatrixEvent): void => {
869+
if (event.getType() !== EventType.GroupCallMemberPrefix) return;
870+
this.recalculateSessionMembers();
871+
};
860872

861873
/**
862874
* Call this when something changed that may impacts the current MatrixRTC members in this session.
@@ -885,7 +897,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
885897
`Memberships for call in room ${this.roomSubset.roomId} have changed: emitting (${this.memberships.length} members)`,
886898
);
887899
logDurationSync(this.logger, "emit MatrixRTCSessionEvent.MembershipsChanged", () => {
888-
this.emit(MatrixRTCSessionEvent.MembershipsChanged, oldMemberships, this.memberships);
900+
this.emit(MatrixRTCSessionEvent.MembershipsChanged, oldMemberships, this.memberships, this);
889901
});
890902

891903
void this.membershipManager?.onRTCSessionMemberUpdate(this.memberships);

src/matrixrtc/MatrixRTCSessionManager.ts

Lines changed: 42 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
2020
import { type Room } from "../models/room.ts";
2121
import { RoomStateEvent } from "../models/room-state.ts";
2222
import { type MatrixEvent } from "../models/event.ts";
23-
import { MatrixRTCSession, type SlotDescription } from "./MatrixRTCSession.ts";
23+
import { MatrixRTCSession, MatrixRTCSessionEvent, type SlotDescription } from "./MatrixRTCSession.ts";
2424
import { EventType } from "../@types/event.ts";
25+
import { type CallMembership } from "./CallMembership.ts";
2526

2627
export enum MatrixRTCSessionManagerEvents {
2728
// A member has joined the MatrixRTC session, creating an active session in a room where there wasn't previously
@@ -66,10 +67,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
6667
// We shouldn't need to null-check here, but matrix-client.spec.ts mocks getRooms
6768
// returning nothing, and breaks tests if you change it to return an empty array :'(
6869
for (const room of this.client.getRooms() ?? []) {
69-
const session = MatrixRTCSession.sessionForRoom(this.client, room, this.slotDescription);
70-
if (session.memberships.length > 0) {
71-
this.roomSessions.set(room.roomId, session);
72-
}
70+
this.createSessionIfNeeded(room);
7371
}
7472

7573
this.client.on(ClientEvent.Room, this.onRoom);
@@ -79,10 +77,10 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
7977

8078
public stop(): void {
8179
for (const sess of this.roomSessions.values()) {
80+
sess.off(MatrixRTCSessionEvent.MembershipsChanged, this.onRtcMembershipChange);
8281
void sess.stop();
8382
}
8483
this.roomSessions.clear();
85-
8684
this.client.off(ClientEvent.Room, this.onRoom);
8785
this.client.off(ClientEvent.Event, this.onEvent);
8886
this.client.off(RoomStateEvent.Events, this.onRoomState);
@@ -98,67 +96,59 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
9896

9997
/**
10098
* Gets the main MatrixRTC session for a room, returning an empty session
101-
* if no members are currently participating
99+
* if no members are currently participating.
100+
* Getting the session will create it if needed (and setup listeners and emit a SessionStarted event if it has members)
102101
*/
103102
public getRoomSession(room: Room): MatrixRTCSession {
104-
if (!this.roomSessions.has(room.roomId)) {
105-
this.roomSessions.set(
106-
room.roomId,
107-
MatrixRTCSession.sessionForRoom(this.client, room, this.slotDescription),
108-
);
109-
}
110-
103+
this.createSessionIfNeeded(room);
111104
return this.roomSessions.get(room.roomId)!;
112105
}
113106

114-
private onRoom = (room: Room): void => {
115-
this.refreshRoom(room);
107+
private createRoomSession(room: Room): MatrixRTCSession {
108+
const sess = MatrixRTCSession.sessionForSlot(this.client, room, this.slotDescription);
109+
this.roomSessions.set(room.roomId, sess);
110+
111+
sess.on(MatrixRTCSessionEvent.MembershipsChanged, this.onRtcMembershipChange);
112+
this.logger.trace(`Session started for ${room.roomId} (${sess.memberships.length} members)`);
113+
if (sess.memberships.length > 0) this.emit(MatrixRTCSessionManagerEvents.SessionStarted, room.roomId, sess);
114+
return sess;
115+
}
116+
117+
private readonly onRtcMembershipChange = (
118+
oldM: CallMembership[],
119+
newM: CallMembership[],
120+
session: MatrixRTCSession,
121+
): void => {
122+
if (oldM.length > 0 && newM.length === 0) {
123+
this.logger.trace(`Session ended for ${session.roomId}`);
124+
this.emit(MatrixRTCSessionManagerEvents.SessionEnded, session.roomId, session);
125+
} else if (oldM.length === 0 && newM.length > 0) {
126+
this.logger.trace(`Session started for ${session.roomId}`);
127+
this.emit(MatrixRTCSessionManagerEvents.SessionStarted, session.roomId, session);
128+
}
116129
};
117130

131+
// Possible cases in which we need to create a session if one doesn't already exist:
132+
private createSessionIfNeeded(room: Room): void {
133+
if (!this.roomSessions.has(room.roomId)) this.createRoomSession(room);
134+
}
135+
private onRoom = (room: Room): void => {
136+
this.createSessionIfNeeded(room);
137+
};
118138
private readonly onEvent = (event: MatrixEvent): void => {
119-
if (!event.unstableStickyExpiresAt) return; // Not sticky, not interested.
120-
121139
if (event.getType() !== EventType.GroupCallMemberPrefix) return;
140+
if (!event.unstableStickyExpiresAt) return; // Not sticky, not interested.
122141

123142
const room = this.client.getRoom(event.getRoomId());
124143
if (!room) return;
125144

126-
this.refreshRoom(room);
145+
this.createSessionIfNeeded(room);
127146
};
128-
129147
private readonly onRoomState = (event: MatrixEvent): void => {
130-
if (event.getType() !== EventType.GroupCallMemberPrefix) {
131-
return;
132-
}
133-
const room = this.client.getRoom(event.getRoomId());
134-
if (!room) {
135-
this.logger.error(`Got room state event for unknown room ${event.getRoomId()}!`);
136-
return;
137-
}
148+
if (event.getType() !== EventType.GroupCallMemberPrefix) return;
138149

139-
this.refreshRoom(room);
150+
const room = this.client.getRoom(event.getRoomId());
151+
if (!room) return;
152+
this.createSessionIfNeeded(room);
140153
};
141-
142-
private refreshRoom(room: Room): void {
143-
const isNewSession = !this.roomSessions.has(room.roomId);
144-
const session = this.getRoomSession(room);
145-
146-
const wasActiveAndKnown = session.memberships.length > 0 && !isNewSession;
147-
// This needs to be here and the event listener cannot be setup in the MatrixRTCSession,
148-
// because we need the update to happen between:
149-
// wasActiveAndKnown = session.memberships.length > 0 and
150-
// nowActive = session.memberships.length
151-
// Alternatively we would need to setup some event emission when the RTC session ended.
152-
session.onRTCSessionMemberUpdate();
153-
154-
const nowActive = session.memberships.length > 0;
155-
156-
if (wasActiveAndKnown && !nowActive) {
157-
this.logger.trace(`Session ended for ${room.roomId} (${session.memberships.length} members)`);
158-
this.emit(MatrixRTCSessionManagerEvents.SessionEnded, room.roomId, this.roomSessions.get(room.roomId)!);
159-
} else if (!wasActiveAndKnown && nowActive) {
160-
this.logger.trace(`Session started for ${room.roomId} (${session.memberships.length} members)`);
161-
this.emit(MatrixRTCSessionManagerEvents.SessionStarted, room.roomId, this.roomSessions.get(room.roomId)!);
162-
}
163-
}
164154
}

0 commit comments

Comments
 (0)