You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-07 23:02:56 +03:00
MatrixRTC: Implement expiry logic for CallMembership and additional test coverage (#4587)
* remove all legacy call related code and adjust tests. We actually had a bit of tests just for legacy and not for session events. All those tests got ported over so we do not remove any tests. * dont adjust tests but remove legacy tests * Remove deprecated CallMembership.getLocalExpiry() * Remove references to legacy in test case names * Clean up SessionMembershipData tsdoc * Remove CallMembership.expires * Use correct expire duration. * make expiration methods not return optional values and update docstring * add docs to `SessionMembershipData` * Add new tests for session type member events that before only existed for legacy member events. This reverts commit 795a3cffb61d672941c49e8139eb1d7b15c87d73. * remove code we do not need yet. * Cleanup --------- Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
This commit is contained in:
@@ -15,7 +15,8 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixEvent } from "../../../src";
|
||||
import { CallMembership, SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
|
||||
import { CallMembership, SessionMembershipData, DEFAULT_EXPIRE_DURATION } from "../../../src/matrixrtc/CallMembership";
|
||||
import { membershipTemplate } from "./mocks";
|
||||
|
||||
function makeMockEvent(originTs = 0): MatrixEvent {
|
||||
return {
|
||||
@@ -74,6 +75,18 @@ describe("CallMembership", () => {
|
||||
expect(membership.createdTs()).toEqual(67890);
|
||||
});
|
||||
|
||||
it("considers memberships unexpired if local age low enough", () => {
|
||||
const fakeEvent = makeMockEvent(1000);
|
||||
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION - 1));
|
||||
expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(false);
|
||||
});
|
||||
|
||||
it("considers memberships expired if local age large enough", () => {
|
||||
const fakeEvent = makeMockEvent(1000);
|
||||
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION + 1));
|
||||
expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(true);
|
||||
});
|
||||
|
||||
it("returns preferred foci", () => {
|
||||
const fakeEvent = makeMockEvent();
|
||||
const mockFocus = { type: "this_is_a_mock_focus" };
|
||||
@@ -85,29 +98,26 @@ describe("CallMembership", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: re-enable this test when expiry is implemented
|
||||
// eslint-disable-next-line jest/no-commented-out-tests
|
||||
// describe("expiry calculation", () => {
|
||||
// let fakeEvent: MatrixEvent;
|
||||
// let membership: CallMembership;
|
||||
describe("expiry calculation", () => {
|
||||
let fakeEvent: MatrixEvent;
|
||||
let membership: CallMembership;
|
||||
|
||||
// beforeEach(() => {
|
||||
// // server origin timestamp for this event is 1000
|
||||
// fakeEvent = makeMockEvent(1000);
|
||||
// membership = new CallMembership(fakeEvent!, membershipTemplate);
|
||||
beforeEach(() => {
|
||||
// server origin timestamp for this event is 1000
|
||||
fakeEvent = makeMockEvent(1000);
|
||||
membership = new CallMembership(fakeEvent!, membershipTemplate);
|
||||
|
||||
// jest.useFakeTimers();
|
||||
// });
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
// afterEach(() => {
|
||||
// jest.useRealTimers();
|
||||
// });
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/no-commented-out-tests
|
||||
// it("calculates time until expiry", () => {
|
||||
// jest.setSystemTime(2000);
|
||||
// // should be using absolute expiry time
|
||||
// expect(membership.getMsUntilExpiry()).toEqual(DEFAULT_EXPIRE_DURATION - 1000);
|
||||
// });
|
||||
// });
|
||||
it("calculates time until expiry", () => {
|
||||
jest.setSystemTime(2000);
|
||||
// should be using absolute expiry time
|
||||
expect(membership.getMsUntilExpiry()).toEqual(DEFAULT_EXPIRE_DURATION - 1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import { encodeBase64, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
|
||||
import { KnownMembership } from "../../../src/@types/membership";
|
||||
import { SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
|
||||
import { SessionMembershipData, DEFAULT_EXPIRE_DURATION } from "../../../src/matrixrtc/CallMembership";
|
||||
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
|
||||
import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
|
||||
import { randomString } from "../../../src/randomstring";
|
||||
@@ -57,21 +57,19 @@ describe("MatrixRTCSession", () => {
|
||||
expect(sess?.callId).toEqual("");
|
||||
});
|
||||
|
||||
// TODO: re-enable this test when expiry is implemented
|
||||
// eslint-disable-next-line jest/no-commented-out-tests
|
||||
// it("ignores expired memberships events", () => {
|
||||
// jest.useFakeTimers();
|
||||
// const expiredMembership = Object.assign({}, membershipTemplate);
|
||||
// expiredMembership.expires = 1000;
|
||||
// expiredMembership.device_id = "EXPIRED";
|
||||
// const mockRoom = makeMockRoom([membershipTemplate, expiredMembership]);
|
||||
it("ignores expired memberships events", () => {
|
||||
jest.useFakeTimers();
|
||||
const expiredMembership = Object.assign({}, membershipTemplate);
|
||||
expiredMembership.expires = 1000;
|
||||
expiredMembership.device_id = "EXPIRED";
|
||||
const mockRoom = makeMockRoom([membershipTemplate, expiredMembership]);
|
||||
|
||||
// jest.advanceTimersByTime(2000);
|
||||
// sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
// expect(sess?.memberships.length).toEqual(1);
|
||||
// expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
|
||||
// jest.useRealTimers();
|
||||
// });
|
||||
jest.advanceTimersByTime(2000);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
expect(sess?.memberships.length).toEqual(1);
|
||||
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("ignores memberships events of members not in the room", () => {
|
||||
const mockRoom = makeMockRoom(membershipTemplate);
|
||||
@@ -80,19 +78,17 @@ describe("MatrixRTCSession", () => {
|
||||
expect(sess?.memberships.length).toEqual(0);
|
||||
});
|
||||
|
||||
// TODO: re-enable this test when expiry is implemented
|
||||
// eslint-disable-next-line jest/no-commented-out-tests
|
||||
// it("honours created_ts", () => {
|
||||
// jest.useFakeTimers();
|
||||
// jest.setSystemTime(500);
|
||||
// const expiredMembership = Object.assign({}, membershipTemplate);
|
||||
// expiredMembership.created_ts = 500;
|
||||
// expiredMembership.expires = 1000;
|
||||
// const mockRoom = makeMockRoom([expiredMembership]);
|
||||
// sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
// expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500);
|
||||
// jest.useRealTimers();
|
||||
// });
|
||||
it("honours created_ts", () => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(500);
|
||||
const expiredMembership = Object.assign({}, membershipTemplate);
|
||||
expiredMembership.created_ts = 500;
|
||||
expiredMembership.expires = 1000;
|
||||
const mockRoom = makeMockRoom([expiredMembership]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("returns empty session if no membership events are present", () => {
|
||||
const mockRoom = makeMockRoom([]);
|
||||
@@ -273,6 +269,55 @@ describe("MatrixRTCSession", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getsActiveFocus", () => {
|
||||
const firstPreferredFocus = {
|
||||
type: "livekit",
|
||||
livekit_service_url: "https://active.url",
|
||||
livekit_alias: "!active:active.url",
|
||||
};
|
||||
it("gets the correct active focus with oldest_membership", () => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(3000);
|
||||
const mockRoom = makeMockRoom([
|
||||
Object.assign({}, membershipTemplate, {
|
||||
device_id: "foo",
|
||||
created_ts: 500,
|
||||
foci_preferred: [firstPreferredFocus],
|
||||
}),
|
||||
Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }),
|
||||
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
|
||||
]);
|
||||
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
|
||||
sess.joinRoomSession([{ type: "livekit", livekit_service_url: "htts://test.org" }], {
|
||||
type: "livekit",
|
||||
focus_selection: "oldest_membership",
|
||||
});
|
||||
expect(sess.getActiveFocus()).toBe(firstPreferredFocus);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
it("does not provide focus if the selection method is unknown", () => {
|
||||
const mockRoom = makeMockRoom([
|
||||
Object.assign({}, membershipTemplate, {
|
||||
device_id: "foo",
|
||||
created_ts: 500,
|
||||
foci_preferred: [firstPreferredFocus],
|
||||
}),
|
||||
Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }),
|
||||
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
|
||||
]);
|
||||
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
|
||||
sess.joinRoomSession([{ type: "livekit", livekit_service_url: "htts://test.org" }], {
|
||||
type: "livekit",
|
||||
focus_selection: "unknown",
|
||||
});
|
||||
expect(sess.getActiveFocus()).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("joining", () => {
|
||||
let mockRoom: Room;
|
||||
let sendStateEventMock: jest.Mock;
|
||||
@@ -323,6 +368,68 @@ describe("MatrixRTCSession", () => {
|
||||
expect(sess!.isJoined()).toEqual(true);
|
||||
});
|
||||
|
||||
it("sends a membership event when joining a call", async () => {
|
||||
const realSetTimeout = setTimeout;
|
||||
jest.useFakeTimers();
|
||||
sess!.joinRoomSession([mockFocus], mockFocus);
|
||||
await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
mockRoom!.roomId,
|
||||
EventType.GroupCallMemberPrefix,
|
||||
{
|
||||
application: "m.call",
|
||||
scope: "m.room",
|
||||
call_id: "",
|
||||
device_id: "AAAAAAA",
|
||||
expires: DEFAULT_EXPIRE_DURATION,
|
||||
foci_preferred: [mockFocus],
|
||||
focus_active: {
|
||||
focus_selection: "oldest_membership",
|
||||
type: "livekit",
|
||||
},
|
||||
},
|
||||
"_@alice:example.org_AAAAAAA",
|
||||
);
|
||||
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
|
||||
// Because we actually want to send the state
|
||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
||||
// For checking if the delayed event is still there or got removed while sending the state.
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
// For scheduling the delayed event
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
||||
// This returns no error so we do not check if we reschedule the event again. this is done in another test.
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("uses membershipExpiryTimeout from join config", async () => {
|
||||
const realSetTimeout = setTimeout;
|
||||
jest.useFakeTimers();
|
||||
sess!.joinRoomSession([mockFocus], mockFocus, { membershipExpiryTimeout: 60000 });
|
||||
await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
mockRoom!.roomId,
|
||||
EventType.GroupCallMemberPrefix,
|
||||
{
|
||||
application: "m.call",
|
||||
scope: "m.room",
|
||||
call_id: "",
|
||||
device_id: "AAAAAAA",
|
||||
expires: 60000,
|
||||
foci_preferred: [mockFocus],
|
||||
focus_active: {
|
||||
focus_selection: "oldest_membership",
|
||||
type: "livekit",
|
||||
},
|
||||
},
|
||||
|
||||
"_@alice:example.org_AAAAAAA",
|
||||
);
|
||||
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe("calls", () => {
|
||||
const activeFocusConfig = { type: "livekit", livekit_service_url: "https://active.url" };
|
||||
const activeFocus = { type: "livekit", focus_selection: "oldest_membership" };
|
||||
|
@@ -19,6 +19,13 @@ import { deepCompare } from "../utils.ts";
|
||||
import { Focus } from "./focus.ts";
|
||||
import { isLivekitFocusActive } from "./LivekitFocus.ts";
|
||||
|
||||
/**
|
||||
* The default duration in milliseconds that a membership is considered valid for.
|
||||
* Ordinarily the client responsible for the session will update the membership before it expires.
|
||||
* We use this duration as the fallback case where stale sessions are present for some reason.
|
||||
*/
|
||||
export const DEFAULT_EXPIRE_DURATION = 1000 * 60 * 60 * 4;
|
||||
|
||||
type CallScope = "m.room" | "m.user";
|
||||
|
||||
/**
|
||||
@@ -154,32 +161,26 @@ export class CallMembership {
|
||||
* Gets the absolute expiry timestamp of the membership.
|
||||
* @returns The absolute expiry time of the membership as a unix timestamp in milliseconds or undefined if not applicable
|
||||
*/
|
||||
public getAbsoluteExpiry(): number | undefined {
|
||||
// TODO: implement this in a future PR. Something like:
|
||||
public getAbsoluteExpiry(): number {
|
||||
// TODO: calculate this from the MatrixRTCSession join configuration directly
|
||||
// return this.createdTs() + (this.membershipData.expires ?? DEFAULT_EXPIRE_DURATION);
|
||||
|
||||
return undefined;
|
||||
return this.createdTs() + (this.membershipData.expires ?? DEFAULT_EXPIRE_DURATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The number of milliseconds until the membership expires or undefined if applicable
|
||||
*/
|
||||
public getMsUntilExpiry(): number | undefined {
|
||||
// TODO: implement this in a future PR. Something like:
|
||||
// return this.getAbsoluteExpiry() - Date.now();
|
||||
|
||||
return undefined;
|
||||
public getMsUntilExpiry(): number {
|
||||
// Assume that local clock is sufficiently in sync with other clocks in the distributed system.
|
||||
// We used to try and adjust for the local clock being skewed, but there are cases where this is not accurate.
|
||||
// The current implementation allows for the local clock to be -infinity to +MatrixRTCSession.MEMBERSHIP_EXPIRY_TIME/2
|
||||
return this.getAbsoluteExpiry() - Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if the membership has expired, otherwise false
|
||||
*/
|
||||
public isExpired(): boolean {
|
||||
// TODO: implement this in a future PR. Something like:
|
||||
// return this.getMsUntilExpiry() <= 0;
|
||||
|
||||
return false;
|
||||
return this.getMsUntilExpiry() <= 0;
|
||||
}
|
||||
|
||||
public getPreferredFoci(): Focus[] {
|
||||
|
@@ -21,7 +21,7 @@ import { Room } from "../models/room.ts";
|
||||
import { MatrixClient } from "../client.ts";
|
||||
import { EventType } from "../@types/event.ts";
|
||||
import { UpdateDelayedEventAction } from "../@types/requests.ts";
|
||||
import { CallMembership, SessionMembershipData } from "./CallMembership.ts";
|
||||
import { CallMembership, DEFAULT_EXPIRE_DURATION, SessionMembershipData } from "./CallMembership.ts";
|
||||
import { RoomStateEvent } from "../models/room-state.ts";
|
||||
import { Focus } from "./focus.ts";
|
||||
import { secureRandomBase64Url } from "../randomstring.ts";
|
||||
@@ -33,8 +33,6 @@ import { MatrixEvent } from "../models/event.ts";
|
||||
import { isLivekitFocusActive } from "./LivekitFocus.ts";
|
||||
import { sleep } from "../utils.ts";
|
||||
|
||||
const DEFAULT_EXPIRE_DURATION = 1000 * 60 * 60 * 4; // 4 hours
|
||||
|
||||
const logger = rootLogger.getChild("MatrixRTCSession");
|
||||
|
||||
const getParticipantId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`;
|
||||
|
Reference in New Issue
Block a user