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 { 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 {
|
function makeMockEvent(originTs = 0): MatrixEvent {
|
||||||
return {
|
return {
|
||||||
@@ -74,6 +75,18 @@ describe("CallMembership", () => {
|
|||||||
expect(membership.createdTs()).toEqual(67890);
|
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", () => {
|
it("returns preferred foci", () => {
|
||||||
const fakeEvent = makeMockEvent();
|
const fakeEvent = makeMockEvent();
|
||||||
const mockFocus = { type: "this_is_a_mock_focus" };
|
const mockFocus = { type: "this_is_a_mock_focus" };
|
||||||
@@ -85,29 +98,26 @@ describe("CallMembership", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: re-enable this test when expiry is implemented
|
describe("expiry calculation", () => {
|
||||||
// eslint-disable-next-line jest/no-commented-out-tests
|
let fakeEvent: MatrixEvent;
|
||||||
// describe("expiry calculation", () => {
|
let membership: CallMembership;
|
||||||
// let fakeEvent: MatrixEvent;
|
|
||||||
// let membership: CallMembership;
|
|
||||||
|
|
||||||
// beforeEach(() => {
|
beforeEach(() => {
|
||||||
// // server origin timestamp for this event is 1000
|
// server origin timestamp for this event is 1000
|
||||||
// fakeEvent = makeMockEvent(1000);
|
fakeEvent = makeMockEvent(1000);
|
||||||
// membership = new CallMembership(fakeEvent!, membershipTemplate);
|
membership = new CallMembership(fakeEvent!, membershipTemplate);
|
||||||
|
|
||||||
// jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
// });
|
});
|
||||||
|
|
||||||
// afterEach(() => {
|
afterEach(() => {
|
||||||
// jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
// });
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line jest/no-commented-out-tests
|
it("calculates time until expiry", () => {
|
||||||
// it("calculates time until expiry", () => {
|
jest.setSystemTime(2000);
|
||||||
// jest.setSystemTime(2000);
|
// should be using absolute expiry time
|
||||||
// // should be using absolute expiry time
|
expect(membership.getMsUntilExpiry()).toEqual(DEFAULT_EXPIRE_DURATION - 1000);
|
||||||
// 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 { encodeBase64, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
|
||||||
import { KnownMembership } from "../../../src/@types/membership";
|
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 { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
|
||||||
import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
|
import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
|
||||||
import { randomString } from "../../../src/randomstring";
|
import { randomString } from "../../../src/randomstring";
|
||||||
@@ -57,21 +57,19 @@ describe("MatrixRTCSession", () => {
|
|||||||
expect(sess?.callId).toEqual("");
|
expect(sess?.callId).toEqual("");
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: re-enable this test when expiry is implemented
|
it("ignores expired memberships events", () => {
|
||||||
// eslint-disable-next-line jest/no-commented-out-tests
|
jest.useFakeTimers();
|
||||||
// it("ignores expired memberships events", () => {
|
const expiredMembership = Object.assign({}, membershipTemplate);
|
||||||
// jest.useFakeTimers();
|
expiredMembership.expires = 1000;
|
||||||
// const expiredMembership = Object.assign({}, membershipTemplate);
|
expiredMembership.device_id = "EXPIRED";
|
||||||
// expiredMembership.expires = 1000;
|
const mockRoom = makeMockRoom([membershipTemplate, expiredMembership]);
|
||||||
// expiredMembership.device_id = "EXPIRED";
|
|
||||||
// const mockRoom = makeMockRoom([membershipTemplate, expiredMembership]);
|
|
||||||
|
|
||||||
// jest.advanceTimersByTime(2000);
|
jest.advanceTimersByTime(2000);
|
||||||
// sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||||
// expect(sess?.memberships.length).toEqual(1);
|
expect(sess?.memberships.length).toEqual(1);
|
||||||
// expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
|
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
|
||||||
// jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
// });
|
});
|
||||||
|
|
||||||
it("ignores memberships events of members not in the room", () => {
|
it("ignores memberships events of members not in the room", () => {
|
||||||
const mockRoom = makeMockRoom(membershipTemplate);
|
const mockRoom = makeMockRoom(membershipTemplate);
|
||||||
@@ -80,19 +78,17 @@ describe("MatrixRTCSession", () => {
|
|||||||
expect(sess?.memberships.length).toEqual(0);
|
expect(sess?.memberships.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: re-enable this test when expiry is implemented
|
it("honours created_ts", () => {
|
||||||
// eslint-disable-next-line jest/no-commented-out-tests
|
jest.useFakeTimers();
|
||||||
// it("honours created_ts", () => {
|
jest.setSystemTime(500);
|
||||||
// jest.useFakeTimers();
|
const expiredMembership = Object.assign({}, membershipTemplate);
|
||||||
// jest.setSystemTime(500);
|
expiredMembership.created_ts = 500;
|
||||||
// const expiredMembership = Object.assign({}, membershipTemplate);
|
expiredMembership.expires = 1000;
|
||||||
// expiredMembership.created_ts = 500;
|
const mockRoom = makeMockRoom([expiredMembership]);
|
||||||
// expiredMembership.expires = 1000;
|
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||||
// const mockRoom = makeMockRoom([expiredMembership]);
|
expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500);
|
||||||
// sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
jest.useRealTimers();
|
||||||
// expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500);
|
});
|
||||||
// jest.useRealTimers();
|
|
||||||
// });
|
|
||||||
|
|
||||||
it("returns empty session if no membership events are present", () => {
|
it("returns empty session if no membership events are present", () => {
|
||||||
const mockRoom = makeMockRoom([]);
|
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", () => {
|
describe("joining", () => {
|
||||||
let mockRoom: Room;
|
let mockRoom: Room;
|
||||||
let sendStateEventMock: jest.Mock;
|
let sendStateEventMock: jest.Mock;
|
||||||
@@ -323,6 +368,68 @@ describe("MatrixRTCSession", () => {
|
|||||||
expect(sess!.isJoined()).toEqual(true);
|
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", () => {
|
describe("calls", () => {
|
||||||
const activeFocusConfig = { type: "livekit", livekit_service_url: "https://active.url" };
|
const activeFocusConfig = { type: "livekit", livekit_service_url: "https://active.url" };
|
||||||
const activeFocus = { type: "livekit", focus_selection: "oldest_membership" };
|
const activeFocus = { type: "livekit", focus_selection: "oldest_membership" };
|
||||||
|
@@ -19,6 +19,13 @@ import { deepCompare } from "../utils.ts";
|
|||||||
import { Focus } from "./focus.ts";
|
import { Focus } from "./focus.ts";
|
||||||
import { isLivekitFocusActive } from "./LivekitFocus.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";
|
type CallScope = "m.room" | "m.user";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -154,32 +161,26 @@ export class CallMembership {
|
|||||||
* Gets the absolute expiry timestamp of the membership.
|
* 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
|
* @returns The absolute expiry time of the membership as a unix timestamp in milliseconds or undefined if not applicable
|
||||||
*/
|
*/
|
||||||
public getAbsoluteExpiry(): number | undefined {
|
public getAbsoluteExpiry(): number {
|
||||||
// TODO: implement this in a future PR. Something like:
|
|
||||||
// TODO: calculate this from the MatrixRTCSession join configuration directly
|
// TODO: calculate this from the MatrixRTCSession join configuration directly
|
||||||
// return this.createdTs() + (this.membershipData.expires ?? DEFAULT_EXPIRE_DURATION);
|
return this.createdTs() + (this.membershipData.expires ?? DEFAULT_EXPIRE_DURATION);
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns The number of milliseconds until the membership expires or undefined if applicable
|
* @returns The number of milliseconds until the membership expires or undefined if applicable
|
||||||
*/
|
*/
|
||||||
public getMsUntilExpiry(): number | undefined {
|
public getMsUntilExpiry(): number {
|
||||||
// TODO: implement this in a future PR. Something like:
|
// Assume that local clock is sufficiently in sync with other clocks in the distributed system.
|
||||||
// return this.getAbsoluteExpiry() - Date.now();
|
// 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 undefined;
|
return this.getAbsoluteExpiry() - Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns true if the membership has expired, otherwise false
|
* @returns true if the membership has expired, otherwise false
|
||||||
*/
|
*/
|
||||||
public isExpired(): boolean {
|
public isExpired(): boolean {
|
||||||
// TODO: implement this in a future PR. Something like:
|
return this.getMsUntilExpiry() <= 0;
|
||||||
// return this.getMsUntilExpiry() <= 0;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPreferredFoci(): Focus[] {
|
public getPreferredFoci(): Focus[] {
|
||||||
|
@@ -21,7 +21,7 @@ import { Room } from "../models/room.ts";
|
|||||||
import { MatrixClient } from "../client.ts";
|
import { MatrixClient } from "../client.ts";
|
||||||
import { EventType } from "../@types/event.ts";
|
import { EventType } from "../@types/event.ts";
|
||||||
import { UpdateDelayedEventAction } from "../@types/requests.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 { RoomStateEvent } from "../models/room-state.ts";
|
||||||
import { Focus } from "./focus.ts";
|
import { Focus } from "./focus.ts";
|
||||||
import { secureRandomBase64Url } from "../randomstring.ts";
|
import { secureRandomBase64Url } from "../randomstring.ts";
|
||||||
@@ -33,8 +33,6 @@ import { MatrixEvent } from "../models/event.ts";
|
|||||||
import { isLivekitFocusActive } from "./LivekitFocus.ts";
|
import { isLivekitFocusActive } from "./LivekitFocus.ts";
|
||||||
import { sleep } from "../utils.ts";
|
import { sleep } from "../utils.ts";
|
||||||
|
|
||||||
const DEFAULT_EXPIRE_DURATION = 1000 * 60 * 60 * 4; // 4 hours
|
|
||||||
|
|
||||||
const logger = rootLogger.getChild("MatrixRTCSession");
|
const logger = rootLogger.getChild("MatrixRTCSession");
|
||||||
|
|
||||||
const getParticipantId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`;
|
const getParticipantId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`;
|
||||||
|
Reference in New Issue
Block a user