Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
bf14e37
Implement Sticky Events MSC
Half-Shot Oct 1, 2025
aef07fc
Renames
Half-Shot Oct 1, 2025
dc69e8b
lint
Half-Shot Oct 1, 2025
437a37a
some review work
Half-Shot Oct 1, 2025
831a87e
Update for support for 4-ples
Half-Shot Oct 2, 2025
bec246d
fix lint
Half-Shot Oct 2, 2025
3019576
pull through method
Half-Shot Oct 2, 2025
dae2f39
Fix the mistake
Half-Shot Oct 2, 2025
48a8e37
More tests to appease SC
Half-Shot Oct 2, 2025
bf2835b
Merge branch 'develop' into hs/sticky-events
Half-Shot Oct 2, 2025
79aa439
Cleaner code
Half-Shot Oct 2, 2025
7b7f74d
Review cleanup
Half-Shot Oct 3, 2025
ffdca00
Refactors based on review.
Half-Shot Oct 7, 2025
5601a0d
lint
Half-Shot Oct 7, 2025
ba557ed
Merge remote-tracking branch 'origin/develop' into hs/sticky-events
Half-Shot Oct 7, 2025
b06d5df
Add sticky event support to the js-sdk
toger5 Sep 23, 2025
cd2914c
use sticky events for matrixRTC
Half-Shot Sep 26, 2025
d6c99de
make sticky events a non breaking change (default to state events. us…
toger5 Sep 24, 2025
8dd68c0
review
toger5 Sep 25, 2025
adcb2bb
Refactor all of this away to it's own accumulator and class.
Half-Shot Sep 26, 2025
510c2c3
Add tests
Half-Shot Sep 26, 2025
ab310df
tidyup
Half-Shot Sep 26, 2025
caf916a
more test cleaning
Half-Shot Sep 26, 2025
953b7d7
lint
Half-Shot Sep 29, 2025
d376e94
Updates and tests
Half-Shot Sep 29, 2025
0d38f14
fix filter
Half-Shot Sep 29, 2025
207bab8
fix filter with lint
Half-Shot Sep 29, 2025
0b1fae5
Add timer tests
Half-Shot Sep 29, 2025
791b119
Add tests for MatrixRTCSessionManager
Half-Shot Sep 30, 2025
488d3ae
Listen for sticky events on MatrixRTCSessionManager
Half-Shot Sep 30, 2025
d132e00
fix logic on filtering out state events
Half-Shot Sep 30, 2025
5f092e1
lint
Half-Shot Sep 30, 2025
c332955
more lint
Half-Shot Sep 30, 2025
b686dbb
tweaks
Half-Shot Sep 30, 2025
e5027c7
Add logging in areas
Half-Shot Sep 30, 2025
3e8115d
more debugging
Half-Shot Sep 30, 2025
f8f2c6d
much more logging
Half-Shot Sep 30, 2025
32f9343
remove more logging
Half-Shot Sep 30, 2025
d4b2252
Finish supporting new MSC
Half-Shot Sep 30, 2025
8d449a6
a line
Half-Shot Sep 30, 2025
4d13031
reconnect the bits to RTC
Half-Shot Oct 3, 2025
af8c329
fixup more bits
Half-Shot Oct 3, 2025
42cdbf7
fixup testrs
Half-Shot Oct 3, 2025
6bac0ad
Ensure consistent order
Half-Shot Oct 3, 2025
673b53b
lint
Half-Shot Oct 3, 2025
54f4539
fix log line
Half-Shot Oct 6, 2025
71d3e73
remove extra bit of code
Half-Shot Oct 6, 2025
946c20a
revert changes to room-sticky-events.ts
Half-Shot Oct 6, 2025
9283ec5
fixup mocks again
Half-Shot Oct 7, 2025
8b9bb74
lint
Half-Shot Oct 7, 2025
0e7ba51
fix
Half-Shot Oct 7, 2025
06fd1bf
cleanup
Half-Shot Oct 7, 2025
08b8675
fix paths
Half-Shot Oct 7, 2025
b4aed2c
tweak test
Half-Shot Oct 7, 2025
f0bde5f
Merge branch 'develop' into toger5/sticky-events
toger5 Oct 7, 2025
b6a782b
fixup
Half-Shot Oct 7, 2025
2ce019a
Add more tests for coverage
Half-Shot Oct 7, 2025
8f157da
Merge branch 'develop' into toger5/sticky-events
toger5 Oct 9, 2025
d70e4f7
Small improvements
toger5 Oct 9, 2025
2042f75
review
toger5 Oct 9, 2025
679652f
Document better
Half-Shot Oct 13, 2025
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
445 changes: 334 additions & 111 deletions spec/unit/matrixrtc/MatrixRTCSession.spec.ts

Large diffs are not rendered by default.

247 changes: 128 additions & 119 deletions spec/unit/matrixrtc/MatrixRTCSessionManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,136 +14,145 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { ClientEvent, EventTimeline, MatrixClient } from "../../../src";
import { ClientEvent, EventTimeline, MatrixClient, type Room } from "../../../src";
import { RoomStateEvent } from "../../../src/models/room-state";
import { MatrixRTCSessionManager, MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
import { makeMockRoom, membershipTemplate, mockRoomState } from "./mocks";
import { makeMockRoom, type MembershipData, membershipTemplate, mockRoomState, mockRTCEvent } from "./mocks";
import { logger } from "../../../src/logger";

describe("MatrixRTCSessionManager", () => {
let client: MatrixClient;

beforeEach(() => {
client = new MatrixClient({ baseUrl: "base_url" });
client.matrixRTC.start();
});

afterEach(() => {
client.stopClient();
client.matrixRTC.stop();
});

it("Fires event when session starts", () => {
const onStarted = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);

try {
const room1 = makeMockRoom([membershipTemplate]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);

client.emit(ClientEvent.Room, room1);
expect(onStarted).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
} finally {
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
describe.each([{ eventKind: "sticky" }, { eventKind: "memberState" }])(
"MatrixRTCSessionManager ($eventKind)",
({ eventKind }) => {
let client: MatrixClient;

function sendLeaveMembership(room: Room, membershipData: MembershipData[]): void {
if (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);
} else {
membershipData.splice(0, 1, { user_id: membershipTemplate.user_id });
client.emit(ClientEvent.Event, mockRTCEvent(membershipData[0], room.roomId, 10000));
}
}
});

it("Doesn't fire event if unrelated sessions starts", () => {
const onStarted = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);

try {
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other" }]);
beforeEach(() => {
client = new MatrixClient({ baseUrl: "base_url" });
client.matrixRTC.start();
});

afterEach(() => {
client.stopClient();
client.matrixRTC.stop();
});

it("Fires event when session starts", () => {
const onStarted = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);

try {
const room1 = makeMockRoom([membershipTemplate], eventKind === "sticky");
jest.spyOn(client, "getRooms").mockReturnValue([room1]);

client.emit(ClientEvent.Room, room1);
expect(onStarted).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
} finally {
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
}
});

it("Doesn't fire event if unrelated sessions starts", () => {
const onStarted = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);

try {
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other" }], eventKind === "sticky");
jest.spyOn(client, "getRooms").mockReturnValue([room1]);

client.emit(ClientEvent.Room, room1);
expect(onStarted).not.toHaveBeenCalled();
} finally {
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
}
});

it("Fires event when session ends", () => {
const onEnded = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
const membershipData: MembershipData[] = [membershipTemplate];
const room1 = makeMockRoom(membershipData, eventKind === "sticky");
jest.spyOn(client, "getRooms").mockReturnValue([room1]);

jest.spyOn(client, "getRoom").mockReturnValue(room1);
client.emit(ClientEvent.Room, room1);
expect(onStarted).not.toHaveBeenCalled();
} finally {
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
}
});

it("Fires event when session ends", () => {
const onEnded = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
const room1 = makeMockRoom([membershipTemplate]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);

client.emit(ClientEvent.Room, room1);

mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);

const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
client.emit(RoomStateEvent.Events, membEvent, roomState, null);

expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
});

it("Fires correctly with for with custom sessionDescription", () => {
const onStarted = jest.fn();
const onEnded = jest.fn();
// create a session manager with a custom session description
const sessionManager = new MatrixRTCSessionManager(logger, client, { id: "test", application: "m.notCall" });

// manually start the session manager (its not the default one started by the client)
sessionManager.start();
sessionManager.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
sessionManager.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);

try {
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other" }]);
sendLeaveMembership(room1, membershipData);

expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
});

it("Fires correctly with custom sessionDescription", () => {
const onStarted = jest.fn();
const onEnded = jest.fn();
// create a session manager with a custom session description
const sessionManager = new MatrixRTCSessionManager(logger, client, {
id: "test",
application: "m.notCall",
});

// manually start the session manager (its not the default one started by the client)
sessionManager.start();
sessionManager.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
sessionManager.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);

try {
// Create a session for applicaation m.other, we ignore this session ecause it lacks a call_id
const room1MembershipData: MembershipData[] = [{ ...membershipTemplate, application: "m.other" }];
const room1 = makeMockRoom(room1MembershipData, eventKind === "sticky");
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
client.emit(ClientEvent.Room, room1);
expect(onStarted).not.toHaveBeenCalled();
onStarted.mockClear();

// Create a session for applicaation m.notCall. We expect this call to be tracked because it has a call_id
const room2MembershipData: MembershipData[] = [
{ ...membershipTemplate, application: "m.notCall", call_id: "test" },
];
const room2 = makeMockRoom(room2MembershipData, eventKind === "sticky");
jest.spyOn(client, "getRooms").mockReturnValue([room1, room2]);
client.emit(ClientEvent.Room, room2);
expect(onStarted).toHaveBeenCalled();
onStarted.mockClear();

// Stop room1's RTC session. Tracked.
jest.spyOn(client, "getRoom").mockReturnValue(room2);
sendLeaveMembership(room2, room2MembershipData);
expect(onEnded).toHaveBeenCalled();
onEnded.mockClear();

// Stop room1's RTC session. Not tracked.
jest.spyOn(client, "getRoom").mockReturnValue(room1);
sendLeaveMembership(room1, room1MembershipData);
expect(onEnded).not.toHaveBeenCalled();
} finally {
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
}
});

it("Doesn't fire event if unrelated sessions ends", () => {
const onEnded = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
const membership: MembershipData[] = [{ ...membershipTemplate, application: "m.other_app" }];
const room1 = makeMockRoom(membership, eventKind === "sticky");
jest.spyOn(client, "getRooms").mockReturnValue([room1]);

client.emit(ClientEvent.Room, room1);
expect(onStarted).not.toHaveBeenCalled();
onStarted.mockClear();

const room2 = makeMockRoom([{ ...membershipTemplate, application: "m.notCall", call_id: "test" }]);
jest.spyOn(client, "getRooms").mockReturnValue([room1, room2]);

client.emit(ClientEvent.Room, room2);
expect(onStarted).toHaveBeenCalled();
onStarted.mockClear();

mockRoomState(room2, [{ user_id: membershipTemplate.user_id }]);
jest.spyOn(client, "getRoom").mockReturnValue(room2);

const roomState = room2.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
expect(onEnded).toHaveBeenCalled();
onEnded.mockClear();

mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);

const roomStateOther = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEventOther = roomStateOther.getStateEvents("org.matrix.msc3401.call.member")[0];
client.emit(RoomStateEvent.Events, membEventOther, roomStateOther, null);
expect(onEnded).not.toHaveBeenCalled();
} finally {
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
}
});

it("Doesn't fire event if unrelated sessions ends", () => {
const onEnded = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other_app" }]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);

client.emit(ClientEvent.Room, room1);

mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
client.emit(ClientEvent.Room, room1);

const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
sendLeaveMembership(room1, membership);

expect(onEnded).not.toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
});
});
expect(onEnded).not.toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
});
},
);
Loading
Loading