1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-25 05:23:13 +03:00

Updates and tests

This commit is contained in:
Half-Shot
2025-09-29 12:36:05 +01:00
parent 953b7d7dea
commit d376e942c9
4 changed files with 275 additions and 220 deletions

View File

@@ -18,7 +18,6 @@ import { encodeBase64, EventType, MatrixClient, type MatrixError, type MatrixEve
import { KnownMembership } from "../../../src/@types/membership"; import { KnownMembership } from "../../../src/@types/membership";
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession"; import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
import { Status, type EncryptionKeysEventContent } from "../../../src/matrixrtc/types"; import { Status, type EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
import { secureRandomString } from "../../../src/randomstring";
import { makeMockEvent, makeMockRoom, membershipTemplate, makeKey, type MembershipData, mockRoomState } from "./mocks"; import { makeMockEvent, makeMockRoom, membershipTemplate, makeKey, type MembershipData, mockRoomState } from "./mocks";
import { RTCEncryptionManager } from "../../../src/matrixrtc/RTCEncryptionManager.ts"; import { RTCEncryptionManager } from "../../../src/matrixrtc/RTCEncryptionManager.ts";
@@ -47,91 +46,111 @@ describe("MatrixRTCSession", () => {
sess = undefined; sess = undefined;
}); });
describe("roomSessionForRoom", () => { describe.each([
it("creates a room-scoped session from room state", () => { {
const mockRoom = makeMockRoom([membershipTemplate]); listenForStickyEvents: true,
listenForMemberStateEvents: true,
testCreateSticky: false,
},
{
listenForStickyEvents: false,
listenForMemberStateEvents: true,
testCreateSticky: false,
},
{
listenForStickyEvents: true,
listenForMemberStateEvents: true,
testCreateSticky: true,
},
{
listenForStickyEvents: true,
listenForMemberStateEvents: false,
testCreateSticky: true,
},
])(
"roomSessionForRoom listenForSticky=$listenForStickyEvents listenForMemberStateEvents=$listenForStickyEvents testCreateSticky=$testCreateSticky",
(testConfig) => {
it("creates a room-scoped session from room state", () => {
const mockRoom = makeMockRoom([membershipTemplate], testConfig.testCreateSticky);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession); sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession, testConfig);
expect(sess?.memberships.length).toEqual(1); expect(sess?.memberships.length).toEqual(1);
expect(sess?.memberships[0].sessionDescription.id).toEqual(""); expect(sess?.memberships[0].sessionDescription.id).toEqual("");
expect(sess?.memberships[0].scope).toEqual("m.room"); expect(sess?.memberships[0].scope).toEqual("m.room");
expect(sess?.memberships[0].application).toEqual("m.call"); expect(sess?.memberships[0].application).toEqual("m.call");
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA"); expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
expect(sess?.memberships[0].isExpired()).toEqual(false); expect(sess?.memberships[0].isExpired()).toEqual(false);
expect(sess?.sessionDescription.id).toEqual(""); expect(sess?.sessionDescription.id).toEqual("");
});
it("ignores memberships where application is not m.call", () => {
const testMembership = Object.assign({}, membershipTemplate, {
application: "not-m.call",
}); });
const mockRoom = makeMockRoom([testMembership]);
const sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
expect(sess?.memberships).toHaveLength(0);
});
it("ignores memberships where callId is not empty", () => { it("ignores memberships where application is not m.call", () => {
const testMembership = Object.assign({}, membershipTemplate, { const testMembership = Object.assign({}, membershipTemplate, {
call_id: "not-empty", application: "not-m.call",
scope: "m.room", });
const mockRoom = makeMockRoom([testMembership], testConfig.testCreateSticky);
const sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession, testConfig);
expect(sess?.memberships).toHaveLength(0);
}); });
const mockRoom = makeMockRoom([testMembership]);
const sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
expect(sess?.memberships).toHaveLength(0);
});
it("ignores expired memberships events", () => { it("ignores memberships where callId is not empty", () => {
jest.useFakeTimers(); const testMembership = Object.assign({}, membershipTemplate, {
const expiredMembership = Object.assign({}, membershipTemplate); call_id: "not-empty",
expiredMembership.expires = 1000; scope: "m.room",
expiredMembership.device_id = "EXPIRED"; });
const mockRoom = makeMockRoom([membershipTemplate, expiredMembership]); const mockRoom = makeMockRoom([testMembership], testConfig.testCreateSticky);
const sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession, testConfig);
expect(sess?.memberships).toHaveLength(0);
});
jest.advanceTimersByTime(2000); it("ignores expired memberships events", () => {
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession); jest.useFakeTimers();
expect(sess?.memberships.length).toEqual(1); const expiredMembership = Object.assign({}, membershipTemplate);
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA"); expiredMembership.expires = 1000;
jest.useRealTimers(); expiredMembership.device_id = "EXPIRED";
}); const mockRoom = makeMockRoom([membershipTemplate, expiredMembership], testConfig.testCreateSticky);
it("ignores memberships events of members not in the room", () => { jest.advanceTimersByTime(2000);
const mockRoom = makeMockRoom([membershipTemplate]); sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession, testConfig);
mockRoom.hasMembershipState = (state) => state === KnownMembership.Join; expect(sess?.memberships.length).toEqual(1);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession); expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
expect(sess?.memberships.length).toEqual(0); jest.useRealTimers();
}); });
it("honours created_ts", () => { it("ignores memberships events of members not in the room", () => {
jest.useFakeTimers(); const mockRoom = makeMockRoom([membershipTemplate], testConfig.testCreateSticky);
jest.setSystemTime(500); mockRoom.hasMembershipState = (state) => state === KnownMembership.Join;
const expiredMembership = Object.assign({}, membershipTemplate); sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession, testConfig);
expiredMembership.created_ts = 500; expect(sess?.memberships.length).toEqual(0);
expiredMembership.expires = 1000; });
const mockRoom = makeMockRoom([expiredMembership]);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500);
jest.useRealTimers();
});
it("returns empty session if no membership events are present", () => { it("honours created_ts", () => {
const mockRoom = makeMockRoom([]); jest.useFakeTimers();
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession); jest.setSystemTime(500);
expect(sess?.memberships).toHaveLength(0); const expiredMembership = Object.assign({}, membershipTemplate);
}); expiredMembership.created_ts = 500;
expiredMembership.expires = 1000;
const mockRoom = makeMockRoom([expiredMembership], testConfig.testCreateSticky);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession, testConfig);
expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500);
jest.useRealTimers();
});
it("safely ignores events with no memberships section", () => { it("returns empty session if no membership events are present", () => {
const roomId = secureRandomString(8); const mockRoom = makeMockRoom([], testConfig.testCreateSticky);
const event = { sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession, testConfig);
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix), expect(sess?.memberships).toHaveLength(0);
getContent: jest.fn().mockReturnValue({}), });
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getTs: jest.fn().mockReturnValue(1000), it("safely ignores events with no memberships section", () => {
getLocalAge: jest.fn().mockReturnValue(0), const event = {
}; getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
const mockRoom = { getContent: jest.fn().mockReturnValue({}),
...makeMockRoom([]), getSender: jest.fn().mockReturnValue("@mock:user.example"),
roomId, getTs: jest.fn().mockReturnValue(1000),
getLiveTimeline: jest.fn().mockReturnValue({ getLocalAge: jest.fn().mockReturnValue(0),
};
const mockRoom = makeMockRoom([]);
mockRoom.getLiveTimeline = jest.fn().mockReturnValue({
getState: jest.fn().mockReturnValue({ getState: jest.fn().mockReturnValue({
on: jest.fn(), on: jest.fn(),
off: jest.fn(), off: jest.fn(),
@@ -148,25 +167,21 @@ describe("MatrixRTCSession", () => {
], ],
]), ]),
}), }),
}), });
}; sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession, testConfig);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom as unknown as Room, callSession); expect(sess.memberships).toHaveLength(0);
expect(sess.memberships).toHaveLength(0); });
});
it("safely ignores events with junk memberships section", () => { it("safely ignores events with junk memberships section", () => {
const roomId = secureRandomString(8); const event = {
const event = { getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix), getContent: jest.fn().mockReturnValue({ memberships: ["i am a fish"] }),
getContent: jest.fn().mockReturnValue({ memberships: ["i am a fish"] }), getSender: jest.fn().mockReturnValue("@mock:user.example"),
getSender: jest.fn().mockReturnValue("@mock:user.example"), getTs: jest.fn().mockReturnValue(1000),
getTs: jest.fn().mockReturnValue(1000), getLocalAge: jest.fn().mockReturnValue(0),
getLocalAge: jest.fn().mockReturnValue(0), };
}; const mockRoom = makeMockRoom([]);
const mockRoom = { mockRoom.getLiveTimeline = jest.fn().mockReturnValue({
...makeMockRoom([]),
roomId,
getLiveTimeline: jest.fn().mockReturnValue({
getState: jest.fn().mockReturnValue({ getState: jest.fn().mockReturnValue({
on: jest.fn(), on: jest.fn(),
off: jest.fn(), off: jest.fn(),
@@ -183,28 +198,28 @@ describe("MatrixRTCSession", () => {
], ],
]), ]),
}), }),
}), });
}; sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession, testConfig);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom as unknown as Room, callSession); expect(sess.memberships).toHaveLength(0);
expect(sess.memberships).toHaveLength(0); });
});
it("ignores memberships with no device_id", () => { it("ignores memberships with no device_id", () => {
const testMembership = Object.assign({}, membershipTemplate); const testMembership = Object.assign({}, membershipTemplate);
(testMembership.device_id as string | undefined) = undefined; (testMembership.device_id as string | undefined) = undefined;
const mockRoom = makeMockRoom([testMembership]); const mockRoom = makeMockRoom([testMembership]);
const sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession); const sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession, testConfig);
expect(sess.memberships).toHaveLength(0); expect(sess.memberships).toHaveLength(0);
}); });
it("ignores memberships with no call_id", () => { it("ignores memberships with no call_id", () => {
const testMembership = Object.assign({}, membershipTemplate); const testMembership = Object.assign({}, membershipTemplate);
(testMembership.call_id as string | undefined) = undefined; (testMembership.call_id as string | undefined) = undefined;
const mockRoom = makeMockRoom([testMembership]); const mockRoom = makeMockRoom([testMembership]);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession); sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession, testConfig);
expect(sess.memberships).toHaveLength(0); expect(sess.memberships).toHaveLength(0);
}); });
}); },
);
describe("getOldestMembership", () => { describe("getOldestMembership", () => {
it("returns the oldest membership event", () => { it("returns the oldest membership event", () => {

View File

@@ -74,10 +74,11 @@ export function makeMockClient(userId: string, deviceId: string): MockClient {
export function makeMockRoom( export function makeMockRoom(
membershipData: MembershipData[], membershipData: MembershipData[],
useStickyEvents = false,
): Room & { emitTimelineEvent: (event: MatrixEvent) => void } { ): Room & { emitTimelineEvent: (event: MatrixEvent) => void } {
const roomId = secureRandomString(8); const roomId = secureRandomString(8);
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()` // Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
const roomState = makeMockRoomState(membershipData, roomId); const roomState = makeMockRoomState(useStickyEvents ? [] : membershipData, roomId);
const room = Object.assign(new EventEmitter(), { const room = Object.assign(new EventEmitter(), {
roomId: roomId, roomId: roomId,
hasMembershipState: jest.fn().mockReturnValue(true), hasMembershipState: jest.fn().mockReturnValue(true),
@@ -85,7 +86,9 @@ export function makeMockRoom(
getState: jest.fn().mockReturnValue(roomState), getState: jest.fn().mockReturnValue(roomState),
}), }),
getVersion: jest.fn().mockReturnValue("default"), getVersion: jest.fn().mockReturnValue("default"),
unstableGetStickyEvents: jest.fn().mockReturnValue([]), unstableGetStickyEvents: jest
.fn()
.mockReturnValue(useStickyEvents ? membershipData.map((m) => mockRTCEvent(m, roomId)) : []),
}) as unknown as Room; }) as unknown as Room;
return Object.assign(room, { return Object.assign(room, {
emitTimelineEvent: (event: MatrixEvent) => emitTimelineEvent: (event: MatrixEvent) =>

View File

@@ -25,7 +25,7 @@ import { type ISendEventResponse } from "../@types/requests.ts";
import { CallMembership } from "./CallMembership.ts"; import { CallMembership } from "./CallMembership.ts";
import { RoomStateEvent } from "../models/room-state.ts"; import { RoomStateEvent } from "../models/room-state.ts";
import { type Focus } from "./focus.ts"; import { type Focus } from "./focus.ts";
import { MembershipManager } from "./MembershipManager.ts"; import { MembershipManager, StickyEventMembershipManager } from "./MembershipManager.ts";
import { EncryptionManager, type IEncryptionManager } from "./EncryptionManager.ts"; import { EncryptionManager, type IEncryptionManager } from "./EncryptionManager.ts";
import { deepCompare, logDurationSync } from "../utils.ts"; import { deepCompare, logDurationSync } from "../utils.ts";
import { import {
@@ -117,14 +117,6 @@ export interface SessionDescription {
// - we use a `Ms` postfix if the option is a duration to avoid using words like: // - we use a `Ms` postfix if the option is a duration to avoid using words like:
// `time`, `duration`, `delay`, `timeout`... that might be mistaken/confused with technical terms. // `time`, `duration`, `delay`, `timeout`... that might be mistaken/confused with technical terms.
export interface MembershipConfig { export interface MembershipConfig {
/**
* Use the new Manager.
*
* Default: `false`.
* @deprecated does nothing anymore we always default to the new membership manager.
*/
useNewMembershipManager?: boolean;
/** /**
* The timeout (in milliseconds) after we joined the call, that our membership should expire * The timeout (in milliseconds) after we joined the call, that our membership should expire
* unless we have explicitly updated it. * unless we have explicitly updated it.
@@ -188,10 +180,11 @@ export interface MembershipConfig {
delayedLeaveEventRestartLocalTimeoutMs?: number; delayedLeaveEventRestartLocalTimeoutMs?: number;
/** /**
* If the membership manager should publish its own membership via sticky events or via the room state. * Send membership using sticky events rather than state events.
* @default false (room state) *
* **WARNING**: This is an unstable feature and not all clients will support it.
*/ */
useStickyEvents?: boolean; unstableSendStickyEvents?: boolean;
} }
export interface EncryptionConfig { export interface EncryptionConfig {
@@ -237,6 +230,11 @@ export interface EncryptionConfig {
} }
export type JoinSessionConfig = SessionConfig & MembershipConfig & EncryptionConfig; export type JoinSessionConfig = SessionConfig & MembershipConfig & EncryptionConfig;
interface SessionMembershipsForRoomOpts {
listenForStickyEvents: boolean;
listenForMemberStateEvents: boolean;
}
/** /**
* A MatrixRTCSession manages the membership & properties of a MatrixRTC session. * A MatrixRTCSession manages the membership & properties of a MatrixRTC session.
* This class doesn't deal with media at all, just membership & properties of a session. * This class doesn't deal with media at all, just membership & properties of a session.
@@ -315,18 +313,21 @@ export class MatrixRTCSession extends TypedEventEmitter<
sessionDescription: SessionDescription, sessionDescription: SessionDescription,
// default both true this implied we combine sticky and state events for the final call state // default both true this implied we combine sticky and state events for the final call state
// (prefer sticky events in case of a duplicate) // (prefer sticky events in case of a duplicate)
useStickyEvents: boolean = true, { listenForStickyEvents, listenForMemberStateEvents }: SessionMembershipsForRoomOpts = {
useStateEvents: boolean = true, listenForStickyEvents: true,
listenForMemberStateEvents: true,
},
): CallMembership[] { ): CallMembership[] {
const logger = rootLogger.getChild(`[MatrixRTCSession ${room.roomId}]`); const logger = rootLogger.getChild(`[MatrixRTCSession ${room.roomId}]`);
let callMemberEvents = [] as MatrixEvent[]; let callMemberEvents = [] as MatrixEvent[];
if (useStickyEvents) { if (listenForStickyEvents) {
logger.info("useStickyEvents");
// prefill with sticky events // prefill with sticky events
callMemberEvents = Array.from(room.unstableGetStickyEvents()).filter( callMemberEvents = Array.from(room.unstableGetStickyEvents()).filter(
(e) => e.getType() === EventType.GroupCallMemberPrefix, (e) => e.getType() === EventType.GroupCallMemberPrefix,
); );
} }
if (useStateEvents) { if (listenForMemberStateEvents) {
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS); const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
if (!roomState) { if (!roomState) {
logger.warn("Couldn't get state for room " + room.roomId); logger.warn("Couldn't get state for room " + room.roomId);
@@ -337,7 +338,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
callMemberStateEvents.filter((e) => callMemberStateEvents.filter((e) =>
callMemberEvents.some((stickyEvent) => stickyEvent.getContent().state_key === e.getStateKey()), callMemberEvents.some((stickyEvent) => stickyEvent.getContent().state_key === e.getStateKey()),
); );
callMemberEvents.concat(callMemberStateEvents); callMemberEvents = callMemberEvents.concat(callMemberStateEvents);
} }
const callMemberships: CallMembership[] = []; const callMemberships: CallMembership[] = [];
@@ -406,8 +407,16 @@ export class MatrixRTCSession extends TypedEventEmitter<
* *
* @deprecated Use `MatrixRTCSession.sessionForRoom` with sessionDescription `{ id: "", application: "m.call" }` instead. * @deprecated Use `MatrixRTCSession.sessionForRoom` with sessionDescription `{ id: "", application: "m.call" }` instead.
*/ */
public static roomSessionForRoom(client: MatrixClient, room: Room): MatrixRTCSession { public static roomSessionForRoom(
const callMemberships = MatrixRTCSession.sessionMembershipsForRoom(room, { id: "", application: "m.call" }); client: MatrixClient,
room: Room,
opts?: SessionMembershipsForRoomOpts,
): MatrixRTCSession {
const callMemberships = MatrixRTCSession.sessionMembershipsForRoom(
room,
{ id: "", application: "m.call" },
opts,
);
return new MatrixRTCSession(client, room, callMemberships, { id: "", application: "m.call" }); return new MatrixRTCSession(client, room, callMemberships, { id: "", application: "m.call" });
} }
@@ -420,8 +429,9 @@ export class MatrixRTCSession extends TypedEventEmitter<
client: MatrixClient, client: MatrixClient,
room: Room, room: Room,
sessionDescription: SessionDescription, sessionDescription: SessionDescription,
opts?: SessionMembershipsForRoomOpts,
): MatrixRTCSession { ): MatrixRTCSession {
const callMemberships = MatrixRTCSession.sessionMembershipsForRoom(room, sessionDescription); const callMemberships = MatrixRTCSession.sessionMembershipsForRoom(room, sessionDescription, opts);
return new MatrixRTCSession(client, room, callMemberships, sessionDescription); return new MatrixRTCSession(client, room, callMemberships, sessionDescription);
} }
@@ -507,6 +517,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
roomState?.off(RoomStateEvent.Members, this.onRoomMemberUpdate); roomState?.off(RoomStateEvent.Members, this.onRoomMemberUpdate);
this.roomSubset.off(RoomEvent.StickyEvents, this.onStickyEventUpdate); this.roomSubset.off(RoomEvent.StickyEvents, this.onStickyEventUpdate);
} }
private reEmitter = new TypedReEmitter< private reEmitter = new TypedReEmitter<
MatrixRTCSessionEvent | RoomAndToDeviceEvents | MembershipManagerEvent, MatrixRTCSessionEvent | RoomAndToDeviceEvents | MembershipManagerEvent,
MatrixRTCSessionEventHandlerMap & RoomAndToDeviceEventsHandlerMap & MembershipManagerEventHandlerMap MatrixRTCSessionEventHandlerMap & RoomAndToDeviceEventsHandlerMap & MembershipManagerEventHandlerMap
@@ -532,15 +543,23 @@ export class MatrixRTCSession extends TypedEventEmitter<
return; return;
} else { } else {
// Create MembershipManager and pass the RTCSession logger (with room id info) // Create MembershipManager and pass the RTCSession logger (with room id info)
this.membershipManager = joinConfig?.unstableSendStickyEvents
this.membershipManager = new MembershipManager( ? new StickyEventMembershipManager(
joinConfig, joinConfig,
this.roomSubset, this.roomSubset,
this.client, this.client,
() => this.getOldestMembership(), () => this.getOldestMembership(),
this.sessionDescription, this.sessionDescription,
this.logger, this.logger,
); )
: new MembershipManager(
joinConfig,
this.roomSubset,
this.client,
() => this.getOldestMembership(),
this.sessionDescription,
this.logger,
);
this.reEmitter.reEmit(this.membershipManager!, [ this.reEmitter.reEmit(this.membershipManager!, [
MembershipManagerEvent.ProbablyLeft, MembershipManagerEvent.ProbablyLeft,
@@ -790,7 +809,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
this.recalculateSessionMembers(); this.recalculateSessionMembers();
}; };
private onStickyEventUpdate = (added: MatrixEvent[], _removed: MatrixEvent[], room: Room): void => { private onStickyEventUpdate = (added: MatrixEvent[], _removed: MatrixEvent[]): void => {
if ([...added, ..._removed].some((e) => e.getType() === EventType.GroupCallMemberPrefix)) { if ([...added, ..._removed].some((e) => e.getType() === EventType.GroupCallMemberPrefix)) {
this.recalculateSessionMembers(); this.recalculateSessionMembers();
} }

View File

@@ -144,6 +144,18 @@ export interface MembershipManagerState {
probablyLeft: boolean; probablyLeft: boolean;
} }
function createInsertActionUpdate(type: MembershipActionType, offset?: number): ActionUpdate {
return {
insert: [{ ts: Date.now() + (offset ?? 0), type }],
};
}
function createReplaceActionUpdate(type: MembershipActionType, offset?: number): ActionUpdate {
return {
replace: [{ ts: Date.now() + (offset ?? 0), type }],
};
}
/** /**
* This class is responsible for sending all events relating to the own membership of a matrixRTC call. * This class is responsible for sending all events relating to the own membership of a matrixRTC call.
* It has the following tasks: * It has the following tasks:
@@ -313,7 +325,7 @@ export class MembershipManager
*/ */
public constructor( public constructor(
private joinConfig: (SessionConfig & MembershipConfig) | undefined, private joinConfig: (SessionConfig & MembershipConfig) | undefined,
private room: Pick<Room, "getLiveTimeline" | "roomId" | "getVersion">, protected room: Pick<Room, "roomId" | "getVersion">,
private client: Pick< private client: Pick<
MatrixClient, MatrixClient,
| "getUserId" | "getUserId"
@@ -321,8 +333,6 @@ export class MembershipManager
| "sendStateEvent" | "sendStateEvent"
| "_unstable_sendDelayedStateEvent" | "_unstable_sendDelayedStateEvent"
| "_unstable_updateDelayedEvent" | "_unstable_updateDelayedEvent"
| "_unstable_sendStickyEvent"
| "_unstable_sendStickyDelayedEvent"
>, >,
private getOldestMembership: () => CallMembership | undefined, private getOldestMembership: () => CallMembership | undefined,
public readonly sessionDescription: SessionDescription, public readonly sessionDescription: SessionDescription,
@@ -380,7 +390,7 @@ export class MembershipManager
} }
// Membership Event static parameters: // Membership Event static parameters:
private deviceId: string; private deviceId: string;
private stateKey: string; protected stateKey: string;
private fociPreferred?: Focus[]; private fociPreferred?: Focus[];
private focusActive?: Focus; private focusActive?: Focus;
@@ -403,7 +413,7 @@ export class MembershipManager
this.membershipEventExpiryHeadroomMs this.membershipEventExpiryHeadroomMs
); );
} }
private get delayedLeaveEventDelayMs(): number { protected get delayedLeaveEventDelayMs(): number {
return this.delayedLeaveEventDelayMsOverride ?? this.joinConfig?.delayedLeaveEventDelayMs ?? 8_000; return this.delayedLeaveEventDelayMsOverride ?? this.joinConfig?.delayedLeaveEventDelayMs ?? 8_000;
} }
private get delayedLeaveEventRestartMs(): number { private get delayedLeaveEventRestartMs(): number {
@@ -420,10 +430,6 @@ export class MembershipManager
return this.joinConfig?.delayedLeaveEventRestartLocalTimeoutMs ?? 2000; return this.joinConfig?.delayedLeaveEventRestartLocalTimeoutMs ?? 2000;
} }
private get useStickyEvents(): boolean {
return this.joinConfig?.useStickyEvents ?? false;
}
// LOOP HANDLER: // LOOP HANDLER:
private async membershipLoopHandler(type: MembershipActionType): Promise<ActionUpdate> { private async membershipLoopHandler(type: MembershipActionType): Promise<ActionUpdate> {
switch (type) { switch (type) {
@@ -479,27 +485,16 @@ export class MembershipManager
} }
// an abstraction to switch between sending state or a sticky event // an abstraction to switch between sending state or a sticky event
private clientSendDelayedEvent: (myMembership: EmptyObject) => Promise<SendDelayedEventResponse> = ( protected clientSendDelayedEvent: (myMembership: EmptyObject) => Promise<SendDelayedEventResponse> = (
myMembership, myMembership,
) => ) =>
this.useStickyEvents this.client._unstable_sendDelayedStateEvent(
? this.client._unstable_sendStickyDelayedEvent( this.room.roomId,
this.room.roomId, { delay: this.delayedLeaveEventDelayMs },
STICK_DURATION_MS, EventType.GroupCallMemberPrefix,
{ delay: this.delayedLeaveEventDelayMs }, myMembership,
null, this.stateKey,
EventType.GroupCallMemberPrefix, );
Object.assign(myMembership, { sticky_key: this.stateKey }),
)
: this.client._unstable_sendDelayedStateEvent(
this.room.roomId,
{ delay: this.delayedLeaveEventDelayMs },
EventType.GroupCallMemberPrefix,
myMembership,
this.stateKey,
);
private sendDelayedEventMethodName: () => string = () =>
this.useStickyEvents ? "_unstable_sendStickyDelayedEvent" : "_unstable_sendDelayedStateEvent";
// HANDLERS (used in the membershipLoopHandler) // HANDLERS (used in the membershipLoopHandler)
private async sendOrResendDelayedLeaveEvent(): Promise<ActionUpdate> { private async sendOrResendDelayedLeaveEvent(): Promise<ActionUpdate> {
@@ -531,7 +526,7 @@ export class MembershipManager
if (this.manageMaxDelayExceededSituation(e)) { if (this.manageMaxDelayExceededSituation(e)) {
return createInsertActionUpdate(repeatActionType); return createInsertActionUpdate(repeatActionType);
} }
const update = this.actionUpdateFromErrors(e, repeatActionType, this.sendDelayedEventMethodName()); const update = this.actionUpdateFromErrors(e, repeatActionType, "_unstable_sendDelayedStateEvent");
if (update) return update; if (update) return update;
if (this.state.hasMemberStateEvent) { if (this.state.hasMemberStateEvent) {
@@ -687,25 +682,10 @@ export class MembershipManager
}); });
} }
private clientSendMembership: (myMembership: SessionMembershipData | EmptyObject) => Promise<ISendEventResponse> = ( protected clientSendMembership: (myMembership: SessionMembershipData | EmptyObject) => Promise<ISendEventResponse> =
myMembership, (myMembership) =>
) => this.client.sendStateEvent(this.room.roomId, EventType.GroupCallMemberPrefix, myMembership, this.stateKey);
this.useStickyEvents
? this.client._unstable_sendStickyEvent(
this.room.roomId,
STICK_DURATION_MS,
null,
EventType.GroupCallMemberPrefix,
Object.assign(myMembership, { sticky_key: this.stateKey }),
)
: this.client.sendStateEvent(
this.room.roomId,
EventType.GroupCallMemberPrefix,
myMembership,
this.stateKey,
);
private sendMembershipMethodName: () => string = () =>
this.useStickyEvents ? "_unstable_sendStickyEvent" : "sendStateEvent";
private async sendJoinEvent(): Promise<ActionUpdate> { private async sendJoinEvent(): Promise<ActionUpdate> {
return await this.clientSendMembership(this.makeMyMembership(this.membershipEventExpiryMs)) return await this.clientSendMembership(this.makeMyMembership(this.membershipEventExpiryMs))
.then(() => { .then(() => {
@@ -739,11 +719,7 @@ export class MembershipManager
}; };
}) })
.catch((e) => { .catch((e) => {
const update = this.actionUpdateFromErrors( const update = this.actionUpdateFromErrors(e, MembershipActionType.SendJoinEvent, "sendStateEvent");
e,
MembershipActionType.SendJoinEvent,
this.sendMembershipMethodName(),
);
if (update) return update; if (update) return update;
throw e; throw e;
}); });
@@ -768,11 +744,7 @@ export class MembershipManager
}; };
}) })
.catch((e) => { .catch((e) => {
const update = this.actionUpdateFromErrors( const update = this.actionUpdateFromErrors(e, MembershipActionType.UpdateExpiry, "sendStateEvent");
e,
MembershipActionType.UpdateExpiry,
this.sendMembershipMethodName(),
);
if (update) return update; if (update) return update;
throw e; throw e;
@@ -786,11 +758,7 @@ export class MembershipManager
return { replace: [] }; return { replace: [] };
}) })
.catch((e) => { .catch((e) => {
const update = this.actionUpdateFromErrors( const update = this.actionUpdateFromErrors(e, MembershipActionType.SendLeaveEvent, "sendStateEvent");
e,
MembershipActionType.SendLeaveEvent,
this.sendMembershipMethodName(),
);
if (update) return update; if (update) return update;
throw e; throw e;
}); });
@@ -857,7 +825,7 @@ export class MembershipManager
return false; return false;
} }
private actionUpdateFromErrors( protected actionUpdateFromErrors(
error: unknown, error: unknown,
type: MembershipActionType, type: MembershipActionType,
method: string, method: string,
@@ -905,7 +873,7 @@ export class MembershipManager
return createInsertActionUpdate(type, resendDelay); return createInsertActionUpdate(type, resendDelay);
} }
throw Error("Exceeded maximum retries for " + type + " attempts (client." + method + "): " + (error as Error)); throw Error("Exceeded maximum retries for " + type + " attempts (client." + method + ")", { cause: error });
} }
/** /**
@@ -1049,14 +1017,64 @@ export class MembershipManager
} }
} }
function createInsertActionUpdate(type: MembershipActionType, offset?: number): ActionUpdate { /**
return { * Implementation of the Membership manager that uses sticky events
insert: [{ ts: Date.now() + (offset ?? 0), type }], * rather than state events.
}; */
} export class StickyEventMembershipManager extends MembershipManager {
public constructor(
joinConfig: (SessionConfig & MembershipConfig) | undefined,
room: Pick<Room, "getLiveTimeline" | "roomId" | "getVersion">,
private readonly clientWithSticky: Pick<
MatrixClient,
| "getUserId"
| "getDeviceId"
| "sendStateEvent"
| "_unstable_sendDelayedStateEvent"
| "_unstable_updateDelayedEvent"
| "_unstable_sendStickyEvent"
| "_unstable_sendStickyDelayedEvent"
>,
getOldestMembership: () => CallMembership | undefined,
sessionDescription: SessionDescription,
parentLogger?: Logger,
) {
super(joinConfig, room, clientWithSticky, getOldestMembership, sessionDescription, parentLogger);
}
function createReplaceActionUpdate(type: MembershipActionType, offset?: number): ActionUpdate { protected clientSendDelayedEvent: (myMembership: EmptyObject) => Promise<SendDelayedEventResponse> = (
return { myMembership,
replace: [{ ts: Date.now() + (offset ?? 0), type }], ) =>
}; this.clientWithSticky._unstable_sendStickyDelayedEvent(
this.room.roomId,
STICK_DURATION_MS,
{ delay: this.delayedLeaveEventDelayMs },
null,
EventType.GroupCallMemberPrefix,
Object.assign(myMembership, { sticky_key: this.stateKey }),
);
protected clientSendMembership: (myMembership: SessionMembershipData | EmptyObject) => Promise<ISendEventResponse> =
(myMembership) =>
this.clientWithSticky._unstable_sendStickyEvent(
this.room.roomId,
STICK_DURATION_MS,
null,
EventType.GroupCallMemberPrefix,
Object.assign(myMembership, { sticky_key: this.stateKey }),
);
protected actionUpdateFromErrors(
error: unknown,
type: MembershipActionType,
method: string,
): ActionUpdate | undefined {
// Override method name.
if (method === "sendStateEvent") {
method = "_unstable_sendStickyEvent";
} else if (method === "_unstable_sendDelayedStateEvent") {
method = "_unstable_sendStickyDelayedEvent";
}
return super.actionUpdateFromErrors(error, type, method);
}
} }