You've already forked matrix-js-sdk
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:
@@ -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", () => {
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user