Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 19 additions & 18 deletions spec/unit/matrixrtc/MatrixRTCSession.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ describe("MatrixRTCSession", () => {
sess!.joinRoomSession([mockFocus], mockFocus, { notificationType: "ring" });
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 5000))]);
mockRoomState(mockRoom, [{ ...membershipTemplate, user_id: client.getUserId()! }]);
sess!.onRTCSessionMemberUpdate();
(sess as any).recalculateSessionMembers();
const ownMembershipId = sess?.memberships[0].eventId;

expect(client.sendEvent).toHaveBeenCalledWith(mockRoom!.roomId, EventType.RTCNotification, {
Expand Down Expand Up @@ -638,7 +638,7 @@ describe("MatrixRTCSession", () => {
},
]);

sess!.onRTCSessionMemberUpdate();
(sess as any).recalculateSessionMembers();
const ownMembershipId = sess?.memberships[0].eventId;
expect(sess!.getConsensusCallIntent()).toEqual("audio");

Expand Down Expand Up @@ -689,13 +689,13 @@ describe("MatrixRTCSession", () => {
it("doesn't send a notification when joining an existing call", async () => {
// Add another member to the call so that it is considered an existing call
mockRoomState(mockRoom, [membershipTemplate]);
sess!.onRTCSessionMemberUpdate();
(sess as any).recalculateSessionMembers();

// Simulate a join, including the update to the room state
sess!.joinRoomSession([mockFocus], mockFocus, { notificationType: "ring" });
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 5000))]);
mockRoomState(mockRoom, [membershipTemplate, { ...membershipTemplate, user_id: client.getUserId()! }]);
sess!.onRTCSessionMemberUpdate();
(sess as any).recalculateSessionMembers();

expect(client.sendEvent).not.toHaveBeenCalled();
});
Expand All @@ -707,9 +707,9 @@ describe("MatrixRTCSession", () => {
// But this time we want to simulate a race condition in which we receive a state event
// from someone else, starting the call before our own state event has been sent
mockRoomState(mockRoom, [membershipTemplate]);
sess!.onRTCSessionMemberUpdate();
(sess as any).recalculateSessionMembers();
mockRoomState(mockRoom, [membershipTemplate, { ...membershipTemplate, user_id: client.getUserId()! }]);
sess!.onRTCSessionMemberUpdate();
(sess as any).recalculateSessionMembers();

// We assume that the responsibility to send a notification, if any, lies with the other
// participant that won the race
Expand All @@ -724,7 +724,7 @@ describe("MatrixRTCSession", () => {

const onMembershipsChanged = jest.fn();
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
sess.onRTCSessionMemberUpdate();
(sess as any).recalculateSessionMembers();

expect(onMembershipsChanged).not.toHaveBeenCalled();
});
Expand All @@ -737,7 +737,7 @@ describe("MatrixRTCSession", () => {
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);

mockRoomState(mockRoom, []);
sess.onRTCSessionMemberUpdate();
(sess as any).recalculateSessionMembers();

expect(onMembershipsChanged).toHaveBeenCalled();
});
Expand Down Expand Up @@ -921,14 +921,14 @@ describe("MatrixRTCSession", () => {

// member2 leaves triggering key rotation
mockRoomState(mockRoom, [membershipTemplate]);
sess.onRTCSessionMemberUpdate();
(sess as any).recalculateSessionMembers();

// member2 re-joins which should trigger an immediate re-send
const keysSentPromise2 = new Promise<EncryptionKeysEventContent>((resolve) => {
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
});
mockRoomState(mockRoom, [membershipTemplate, member2]);
sess.onRTCSessionMemberUpdate();
(sess as any).recalculateSessionMembers();
// but, that immediate resend is throttled so we need to wait a bit
jest.advanceTimersByTime(1000);
const { keys } = await keysSentPromise2;
Expand Down Expand Up @@ -979,7 +979,7 @@ describe("MatrixRTCSession", () => {
});

mockRoomState(mockRoom, [membershipTemplate, member2]);
sess.onRTCSessionMemberUpdate();
(sess as any).recalculateSessionMembers();

await keysSentPromise2;

Expand Down Expand Up @@ -1026,7 +1026,7 @@ describe("MatrixRTCSession", () => {
sendEventMock.mockClear();

// these should be a no-op:
sess.onRTCSessionMemberUpdate();
(sess as any).recalculateSessionMembers();
expect(sendEventMock).toHaveBeenCalledTimes(0);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
} finally {
Expand Down Expand Up @@ -1071,7 +1071,7 @@ describe("MatrixRTCSession", () => {
sendEventMock.mockClear();

// this should be a no-op:
sess.onRTCSessionMemberUpdate();
(sess as any).recalculateSessionMembers();
expect(sendEventMock).toHaveBeenCalledTimes(0);

// advance time to avoid key throttling
Expand All @@ -1086,7 +1086,7 @@ describe("MatrixRTCSession", () => {
});

// this should re-send the key
sess.onRTCSessionMemberUpdate();
(sess as any).recalculateSessionMembers();

await keysSentPromise2;

Expand Down Expand Up @@ -1144,7 +1144,7 @@ describe("MatrixRTCSession", () => {
});

mockRoomState(mockRoom, [membershipTemplate]);
sess.onRTCSessionMemberUpdate();
(sess as any).recalculateSessionMembers();

jest.advanceTimersByTime(KEY_DELAY);
expect(sendKeySpy).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -1211,7 +1211,7 @@ describe("MatrixRTCSession", () => {
mockRoomState(mockRoom, members.slice(0, membersToTest - i));
}

sess!.onRTCSessionMemberUpdate();
(sess as any).recalculateSessionMembers();

// advance time to avoid key throttling
jest.advanceTimersByTime(10000);
Expand Down Expand Up @@ -1250,7 +1250,7 @@ describe("MatrixRTCSession", () => {
});

mockRoomState(mockRoom, [membershipTemplate, member2]);
sess.onRTCSessionMemberUpdate();
(sess as any).recalculateSessionMembers();

await new Promise((resolve) => {
realSetTimeout(resolve);
Expand All @@ -1277,7 +1277,8 @@ describe("MatrixRTCSession", () => {
manageMediaKeys: true,
useExperimentalToDeviceTransport: true,
});
sess.onRTCSessionMemberUpdate();

(sess as any).recalculateSessionMembers();

await keySentPromise;

Expand Down
12 changes: 9 additions & 3 deletions spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { RoomStateEvent } from "../../../src/models/room-state";
import { MatrixRTCSessionManager, MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
import { makeMockRoom, type MembershipData, membershipTemplate, mockRoomState, mockRTCEvent } from "./mocks";
import { logger } from "../../../src/logger";
import { RoomStickyEventsEvent } from "../../../src/models/room-sticky-events";

describe.each([{ eventKind: "sticky" }, { eventKind: "memberState" }])(
"MatrixRTCSessionManager ($eventKind)",
Expand All @@ -30,10 +31,15 @@ describe.each([{ eventKind: "sticky" }, { eventKind: "memberState" }])(
mockRoomState(room, [{ user_id: membershipTemplate.user_id }]);
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
roomState.emit(RoomStateEvent.Events, membEvent, roomState, null);
} else {
membershipData.splice(0, 1, { user_id: membershipTemplate.user_id });
client.emit(ClientEvent.Event, mockRTCEvent(membershipData[0], room.roomId, 10000));
const previousData = membershipData.splice(0, 1, {
user_id: membershipTemplate.user_id,
msc4354_sticky_key: membershipTemplate.msc4354_sticky_key,
})[0];
const current = mockRTCEvent(membershipData[0], room.roomId, 10000);
const previous = mockRTCEvent(previousData, room.roomId, 10000);
room.emit(RoomStickyEventsEvent.Update, [], [{ current, previous }], []);
}
}

Expand Down
33 changes: 23 additions & 10 deletions spec/unit/matrixrtc/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

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

import { EventType, type Room, RoomEvent, type MatrixClient, type MatrixEvent } from "../../../src";
import {
EventType,
type Room,
RoomEvent,
type MatrixClient,
type MatrixEvent,
Direction,
TypedEventEmitter,
} from "../../../src";
import { CallMembership, type SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
import { secureRandomString } from "../../../src/randomstring";
import { type StickyMatrixEvent } from "../../../src/models/room-sticky-events";

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

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

function makeMockRoomState(membershipData: MembershipData[], roomId: string) {
const events = membershipData.map((m) => mockRTCEvent(m, roomId));

const keysAndEvents = events.map((e) => {
const data = e.getContent() as SessionMembershipData;
return [`_${e.sender?.userId}_${data.device_id}`];
});

return {
on: jest.fn(),
off: jest.fn(),
return Object.assign(new TypedEventEmitter(), {
getStateEvents: (_: string, stateKey: string) => {
if (stateKey !== undefined) return keysAndEvents.find(([k]) => k === stateKey)?.[1];
return events;
Expand All @@ -128,11 +135,17 @@ function makeMockRoomState(membershipData: MembershipData[], roomId: string) {
},
],
]),
};
});
}

export function mockRoomState(room: Room, membershipData: MembershipData[]): void {
room.getLiveTimeline().getState = jest.fn().mockReturnValue(makeMockRoomState(membershipData, room.roomId));
const prevState = room.getLiveTimeline().getState(Direction.Forward)!;
const newState = makeMockRoomState(membershipData, room.roomId);
room.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(
Object.assign(prevState, { events: newState.events, getStateEvents: newState.getStateEvents }),
);
}

export function makeMockEvent(
Expand Down Expand Up @@ -160,7 +173,7 @@ export function mockRTCEvent(
roomId: string,
stickyDuration?: number,
timestamp?: number,
): MatrixEvent {
): StickyMatrixEvent {
return {
...makeMockEvent(
EventType.GroupCallMemberPrefix,
Expand All @@ -171,7 +184,7 @@ export function mockRTCEvent(
!stickyDuration && "device_id" in membershipData ? `_${sender}_${membershipData.device_id}` : "",
),
unstableStickyExpiresAt: stickyDuration,
} as unknown as MatrixEvent;
} as unknown as StickyMatrixEvent;
}

export function mockCallMembership(membershipData: MembershipData, roomId: string): CallMembership {
Expand Down
26 changes: 19 additions & 7 deletions src/matrixrtc/MatrixRTCSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export type MatrixRTCSessionEventHandlerMap = {
[MatrixRTCSessionEvent.MembershipsChanged]: (
oldMemberships: CallMembership[],
newMemberships: CallMembership[],
session: MatrixRTCSession,
) => void;
[MatrixRTCSessionEvent.JoinStateChanged]: (isJoined: boolean) => void;
[MatrixRTCSessionEvent.EncryptionKeyChanged]: (
Expand Down Expand Up @@ -483,11 +484,14 @@ export class MatrixRTCSession extends TypedEventEmitter<
* this class.
* Outside of tests this most likely will be a full room, however.
* @deprecated Relying on a full Room object being available here is an anti-pattern. You should be tracking
* the room object in your own code and passing it in when needed.
* the room object in your own code and passing it in when needed. use roomId instead.
*/
public get room(): Room {
return this.roomSubset as Room;
}
public get roomId(): string {
return this.roomSubset.roomId;
}

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

this.setExpiryTimer();
Expand All @@ -557,6 +561,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
}
const roomState = this.roomSubset.getLiveTimeline().getState(EventTimeline.FORWARDS);
roomState?.off(RoomStateEvent.Members, this.onRoomMemberUpdate);
roomState?.off(RoomStateEvent.Events, this.onRoomStateUpdate);
this.roomSubset.off(RoomStickyEventsEvent.Update, this.onStickyEventUpdate);
}

Expand Down Expand Up @@ -771,7 +776,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
}

if (soonestExpiry != undefined) {
this.expiryTimeout = setTimeout(this.onRTCSessionMemberUpdate, soonestExpiry);
this.expiryTimeout = setTimeout(this.recalculateSessionMembers, soonestExpiry);
}
}

Expand Down Expand Up @@ -857,14 +862,21 @@ export class MatrixRTCSession extends TypedEventEmitter<
this.recalculateSessionMembers();
}
};

/**
* Call this when something changed that may impacts the current MatrixRTC members in this session.
* Call this when a sticky event update has occured.
*/
public onRTCSessionMemberUpdate = (): void => {
private readonly onRoomStateUpdate = (event: MatrixEvent): void => {
if (event.getType() !== EventType.GroupCallMemberPrefix) return;
this.recalculateSessionMembers();
};

// /**
// * Call this when something changed that may impacts the current MatrixRTC members in this session.
// */
// public onRTCSessionMemberUpdate = (): void => {
// this.recalculateSessionMembers();
// };

/**
* Call this when anything that could impact rtc memberships has changed: Room Members or RTC members.
*
Expand All @@ -885,7 +897,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
`Memberships for call in room ${this.roomSubset.roomId} have changed: emitting (${this.memberships.length} members)`,
);
logDurationSync(this.logger, "emit MatrixRTCSessionEvent.MembershipsChanged", () => {
this.emit(MatrixRTCSessionEvent.MembershipsChanged, oldMemberships, this.memberships);
this.emit(MatrixRTCSessionEvent.MembershipsChanged, oldMemberships, this.memberships, this);
});

void this.membershipManager?.onRTCSessionMemberUpdate(this.memberships);
Expand Down
Loading
Loading