You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-23 17:02:25 +03:00
Split membership manager into legacy variant to improve readability
This commit is contained in:
@@ -31,8 +31,9 @@ import {
|
|||||||
type Transport,
|
type Transport,
|
||||||
} from "../../../src/matrixrtc";
|
} from "../../../src/matrixrtc";
|
||||||
import { makeMockClient, makeMockRoom, sessionMembershipTemplate, mockCallMembership, type MockClient } from "./mocks";
|
import { makeMockClient, makeMockRoom, sessionMembershipTemplate, mockCallMembership, type MockClient } from "./mocks";
|
||||||
import { MembershipManager, StickyEventMembershipManager } from "../../../src/matrixrtc/MembershipManager.ts";
|
import { LegacyMembershipManager, StickyEventMembershipManager } from "../../../src/matrixrtc/MembershipManager.ts";
|
||||||
import { SessionMembershipData } from "src/matrixrtc/membership/legacy.ts";
|
import { SessionMembershipData } from "src/matrixrtc/membership/legacy.ts";
|
||||||
|
import { RtcMembershipData } from "src/matrixrtc/membership/rtc.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a promise that will resolve once a mocked method is called.
|
* Create a promise that will resolve once a mocked method is called.
|
||||||
@@ -90,27 +91,29 @@ describe("MembershipManager", () => {
|
|||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
client = makeMockClient("@alice:example.org", "AAAAAAA");
|
client = makeMockClient("@alice:example.org", "AAAAAAA");
|
||||||
room = makeMockRoom([sessionMembershipTemplate]);
|
room = makeMockRoom([sessionMembershipTemplate]);
|
||||||
// Provide a default mock that is like the default "non error" server behaviour.
|
|
||||||
(client._unstable_sendDelayedStateEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
|
|
||||||
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue(undefined);
|
|
||||||
(client._unstable_sendStickyEvent as Mock<any>).mockResolvedValue({ event_id: "id" });
|
|
||||||
(client._unstable_sendStickyDelayedEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
|
|
||||||
(client.sendStateEvent as Mock<any>).mockResolvedValue({ event_id: "id" });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
// There is no need to clean up mocks since we will recreate the client.
|
// There is no need to clean up mocks since we will recreate the client.
|
||||||
});
|
});
|
||||||
|
describe("LegacyMembershipManager", () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Provide a default mock that is like the default "non error" server behaviour.
|
||||||
|
(client._unstable_sendDelayedStateEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
|
||||||
|
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue(undefined);
|
||||||
|
(client.sendStateEvent as Mock<any>).mockResolvedValue({ event_id: "id" });
|
||||||
|
})
|
||||||
|
|
||||||
describe("isActivated()", () => {
|
describe("isActivated()", () => {
|
||||||
it("defaults to false", () => {
|
it("defaults to false", () => {
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
expect(manager.isActivated()).toEqual(false);
|
expect(manager.isActivated()).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns true after join()", () => {
|
it("returns true after join()", () => {
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
manager.join([]);
|
manager.join([]);
|
||||||
expect(manager.isActivated()).toEqual(true);
|
expect(manager.isActivated()).toEqual(true);
|
||||||
});
|
});
|
||||||
@@ -124,7 +127,7 @@ describe("MembershipManager", () => {
|
|||||||
const updateDelayedEventHandle = createAsyncHandle<void>(client._unstable_updateDelayedEvent as Mock);
|
const updateDelayedEventHandle = createAsyncHandle<void>(client._unstable_updateDelayedEvent as Mock);
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
const memberManager = new MembershipManager(undefined, room, client, callSession);
|
const memberManager = new LegacyMembershipManager(undefined, room, client, callSession);
|
||||||
memberManager.join([focus], undefined);
|
memberManager.join([focus], undefined);
|
||||||
// expects
|
// expects
|
||||||
await waitForMockCall(client.sendStateEvent, Promise.resolve({ event_id: "id" }));
|
await waitForMockCall(client.sendStateEvent, Promise.resolve({ event_id: "id" }));
|
||||||
@@ -154,7 +157,7 @@ describe("MembershipManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("reschedules delayed leave event if sending state cancels it", async () => {
|
it("reschedules delayed leave event if sending state cancels it", async () => {
|
||||||
const memberManager = new MembershipManager(undefined, room, client, callSession);
|
const memberManager = new LegacyMembershipManager(undefined, room, client, callSession);
|
||||||
const waitForSendState = waitForMockCall(client.sendStateEvent);
|
const waitForSendState = waitForMockCall(client.sendStateEvent);
|
||||||
const waitForUpdateDelaye = waitForMockCallOnce(
|
const waitForUpdateDelaye = waitForMockCallOnce(
|
||||||
client._unstable_updateDelayedEvent,
|
client._unstable_updateDelayedEvent,
|
||||||
@@ -223,7 +226,7 @@ describe("MembershipManager", () => {
|
|||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const manager = new MembershipManager(
|
const manager = new LegacyMembershipManager(
|
||||||
{
|
{
|
||||||
delayedLeaveEventDelayMs: 9000,
|
delayedLeaveEventDelayMs: 9000,
|
||||||
},
|
},
|
||||||
@@ -286,7 +289,7 @@ describe("MembershipManager", () => {
|
|||||||
describe("delayed leave event", () => {
|
describe("delayed leave event", () => {
|
||||||
it("does not try again to schedule a delayed leave event if not supported", () => {
|
it("does not try again to schedule a delayed leave event if not supported", () => {
|
||||||
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent as Mock);
|
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent as Mock);
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
manager.join([focus]);
|
manager.join([focus]);
|
||||||
delayedHandle.reject?.(
|
delayedHandle.reject?.(
|
||||||
new UnsupportedDelayedEventsEndpointError(
|
new UnsupportedDelayedEventsEndpointError(
|
||||||
@@ -298,14 +301,14 @@ describe("MembershipManager", () => {
|
|||||||
});
|
});
|
||||||
it("does try to schedule a delayed leave event again if rate limited", async () => {
|
it("does try to schedule a delayed leave event again if rate limited", async () => {
|
||||||
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent as Mock);
|
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent as Mock);
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
manager.join([focus]);
|
manager.join([focus]);
|
||||||
delayedHandle.reject?.(new HTTPError("rate limited", 429, undefined));
|
delayedHandle.reject?.(new HTTPError("rate limited", 429, undefined));
|
||||||
await jest.advanceTimersByTimeAsync(5000);
|
await jest.advanceTimersByTimeAsync(5000);
|
||||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
|
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
it("uses delayedLeaveEventDelayMs from config", () => {
|
it("uses delayedLeaveEventDelayMs from config", () => {
|
||||||
const manager = new MembershipManager({ delayedLeaveEventDelayMs: 123456 }, room, client, callSession);
|
const manager = new LegacyMembershipManager({ delayedLeaveEventDelayMs: 123456 }, room, client, callSession);
|
||||||
manager.join([focus]);
|
manager.join([focus]);
|
||||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
|
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
|
||||||
room.roomId,
|
room.roomId,
|
||||||
@@ -319,7 +322,7 @@ describe("MembershipManager", () => {
|
|||||||
|
|
||||||
it("rejoins if delayed event is not found (404)", async () => {
|
it("rejoins if delayed event is not found (404)", async () => {
|
||||||
const RESTART_DELAY = 15000;
|
const RESTART_DELAY = 15000;
|
||||||
const manager = new MembershipManager(
|
const manager = new LegacyMembershipManager(
|
||||||
{ delayedLeaveEventRestartMs: RESTART_DELAY },
|
{ delayedLeaveEventRestartMs: RESTART_DELAY },
|
||||||
room,
|
room,
|
||||||
client,
|
client,
|
||||||
@@ -357,7 +360,7 @@ describe("MembershipManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("uses membershipEventExpiryMs from config", async () => {
|
it("uses membershipEventExpiryMs from config", async () => {
|
||||||
const manager = new MembershipManager(
|
const manager = new LegacyMembershipManager(
|
||||||
{ membershipEventExpiryMs: 1234567 },
|
{ membershipEventExpiryMs: 1234567 },
|
||||||
room,
|
room,
|
||||||
client,
|
client,
|
||||||
@@ -387,7 +390,7 @@ describe("MembershipManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("does nothing if join called when already joined", async () => {
|
it("does nothing if join called when already joined", async () => {
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
manager.join([focus]);
|
manager.join([focus]);
|
||||||
await waitForMockCall(client.sendStateEvent);
|
await waitForMockCall(client.sendStateEvent);
|
||||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
||||||
@@ -399,7 +402,7 @@ describe("MembershipManager", () => {
|
|||||||
describe("leave()", () => {
|
describe("leave()", () => {
|
||||||
// TODO add rate limit cases.
|
// TODO add rate limit cases.
|
||||||
it("resolves delayed leave event when leave is called", async () => {
|
it("resolves delayed leave event when leave is called", async () => {
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
manager.join([focus]);
|
manager.join([focus]);
|
||||||
await jest.advanceTimersByTimeAsync(1);
|
await jest.advanceTimersByTimeAsync(1);
|
||||||
await manager.leave();
|
await manager.leave();
|
||||||
@@ -407,7 +410,7 @@ describe("MembershipManager", () => {
|
|||||||
expect(client.sendStateEvent).toHaveBeenCalled();
|
expect(client.sendStateEvent).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
it("send leave event when leave is called and resolving delayed leave fails", async () => {
|
it("send leave event when leave is called and resolving delayed leave fails", async () => {
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
manager.join([focus]);
|
manager.join([focus]);
|
||||||
await jest.advanceTimersByTimeAsync(1);
|
await jest.advanceTimersByTimeAsync(1);
|
||||||
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValue("unknown");
|
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValue("unknown");
|
||||||
@@ -422,7 +425,7 @@ describe("MembershipManager", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
it("does nothing if not joined", () => {
|
it("does nothing if not joined", () => {
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
expect(async () => await manager.leave()).not.toThrow();
|
expect(async () => await manager.leave()).not.toThrow();
|
||||||
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
|
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
|
||||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||||
@@ -431,7 +434,7 @@ describe("MembershipManager", () => {
|
|||||||
|
|
||||||
describe("onRTCSessionMemberUpdate()", () => {
|
describe("onRTCSessionMemberUpdate()", () => {
|
||||||
it("does nothing if not joined", async () => {
|
it("does nothing if not joined", async () => {
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
await manager.onRTCSessionMemberUpdate([mockCallMembership(sessionMembershipTemplate, room.roomId)]);
|
await manager.onRTCSessionMemberUpdate([mockCallMembership(sessionMembershipTemplate, room.roomId)]);
|
||||||
await jest.advanceTimersToNextTimerAsync();
|
await jest.advanceTimersToNextTimerAsync();
|
||||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||||
@@ -439,7 +442,7 @@ describe("MembershipManager", () => {
|
|||||||
expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled();
|
expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
it("does nothing if own membership still present", async () => {
|
it("does nothing if own membership still present", async () => {
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
manager.join([focus], focusActive);
|
manager.join([focus], focusActive);
|
||||||
await jest.advanceTimersByTimeAsync(1);
|
await jest.advanceTimersByTimeAsync(1);
|
||||||
const myMembership = (client.sendStateEvent as Mock).mock.calls[0][2];
|
const myMembership = (client.sendStateEvent as Mock).mock.calls[0][2];
|
||||||
@@ -463,7 +466,7 @@ describe("MembershipManager", () => {
|
|||||||
expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled();
|
expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
it("recreates membership if it is missing", async () => {
|
it("recreates membership if it is missing", async () => {
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
manager.join([focus], focusActive);
|
manager.join([focus], focusActive);
|
||||||
await jest.advanceTimersByTimeAsync(1);
|
await jest.advanceTimersByTimeAsync(1);
|
||||||
// clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
|
// clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
|
||||||
@@ -481,7 +484,7 @@ describe("MembershipManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("updates the UpdateExpiry entry in the action scheduler", async () => {
|
it("updates the UpdateExpiry entry in the action scheduler", async () => {
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
manager.join([focus], focusActive);
|
manager.join([focus], focusActive);
|
||||||
await jest.advanceTimersByTimeAsync(1);
|
await jest.advanceTimersByTimeAsync(1);
|
||||||
// clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
|
// clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
|
||||||
@@ -510,7 +513,7 @@ describe("MembershipManager", () => {
|
|||||||
// TODO: Not sure about this name
|
// TODO: Not sure about this name
|
||||||
describe("background timers", () => {
|
describe("background timers", () => {
|
||||||
it("sends only one keep-alive for delayed leave event per `delayedLeaveEventRestartMs`", async () => {
|
it("sends only one keep-alive for delayed leave event per `delayedLeaveEventRestartMs`", async () => {
|
||||||
const manager = new MembershipManager(
|
const manager = new LegacyMembershipManager(
|
||||||
{ delayedLeaveEventRestartMs: 10_000, delayedLeaveEventDelayMs: 30_000 },
|
{ delayedLeaveEventRestartMs: 10_000, delayedLeaveEventDelayMs: 30_000 },
|
||||||
room,
|
room,
|
||||||
client,
|
client,
|
||||||
@@ -541,7 +544,7 @@ describe("MembershipManager", () => {
|
|||||||
// the expiration logic still makes sense.
|
// the expiration logic still makes sense.
|
||||||
// TODO: Add git commit when we removed it.
|
// TODO: Add git commit when we removed it.
|
||||||
async function testExpires(expire: number, headroom?: number) {
|
async function testExpires(expire: number, headroom?: number) {
|
||||||
const manager = new MembershipManager(
|
const manager = new LegacyMembershipManager(
|
||||||
{ membershipEventExpiryMs: expire, membershipEventExpiryHeadroomMs: headroom },
|
{ membershipEventExpiryMs: expire, membershipEventExpiryHeadroomMs: headroom },
|
||||||
room,
|
room,
|
||||||
client,
|
client,
|
||||||
@@ -570,14 +573,14 @@ describe("MembershipManager", () => {
|
|||||||
|
|
||||||
describe("status updates", () => {
|
describe("status updates", () => {
|
||||||
it("starts 'Disconnected'", () => {
|
it("starts 'Disconnected'", () => {
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
expect(manager.status).toBe(Status.Disconnected);
|
expect(manager.status).toBe(Status.Disconnected);
|
||||||
});
|
});
|
||||||
it("emits 'Connection' and 'Connected' after join", async () => {
|
it("emits 'Connection' and 'Connected' after join", async () => {
|
||||||
const handleDelayedEvent = createAsyncHandle<void>(client._unstable_sendDelayedStateEvent);
|
const handleDelayedEvent = createAsyncHandle<void>(client._unstable_sendDelayedStateEvent);
|
||||||
const handleStateEvent = createAsyncHandle<void>(client.sendStateEvent);
|
const handleStateEvent = createAsyncHandle<void>(client.sendStateEvent);
|
||||||
|
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
expect(manager.status).toBe(Status.Disconnected);
|
expect(manager.status).toBe(Status.Disconnected);
|
||||||
const connectEmit = jest.fn();
|
const connectEmit = jest.fn();
|
||||||
manager.on(MembershipManagerEvent.StatusChanged, connectEmit);
|
manager.on(MembershipManagerEvent.StatusChanged, connectEmit);
|
||||||
@@ -591,7 +594,7 @@ describe("MembershipManager", () => {
|
|||||||
expect(connectEmit).toHaveBeenCalledWith(Status.Connecting, Status.Connected);
|
expect(connectEmit).toHaveBeenCalledWith(Status.Connecting, Status.Connected);
|
||||||
});
|
});
|
||||||
it("emits 'Disconnecting' and 'Disconnected' after leave", async () => {
|
it("emits 'Disconnecting' and 'Disconnected' after leave", async () => {
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
const connectEmit = jest.fn();
|
const connectEmit = jest.fn();
|
||||||
manager.on(MembershipManagerEvent.StatusChanged, connectEmit);
|
manager.on(MembershipManagerEvent.StatusChanged, connectEmit);
|
||||||
manager.join([focus], focusActive);
|
manager.join([focus], focusActive);
|
||||||
@@ -607,7 +610,7 @@ describe("MembershipManager", () => {
|
|||||||
it("sends retry if call membership event is still valid at time of retry", async () => {
|
it("sends retry if call membership event is still valid at time of retry", async () => {
|
||||||
const handle = createAsyncHandle(client._unstable_sendDelayedStateEvent);
|
const handle = createAsyncHandle(client._unstable_sendDelayedStateEvent);
|
||||||
|
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
manager.join([focus], focusActive);
|
manager.join([focus], focusActive);
|
||||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
@@ -634,7 +637,7 @@ describe("MembershipManager", () => {
|
|||||||
new Headers({ "Retry-After": "1" }),
|
new Headers({ "Retry-After": "1" }),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
// Should call _unstable_sendDelayedStateEvent but not sendStateEvent because of the
|
// Should call _unstable_sendDelayedStateEvent but not sendStateEvent because of the
|
||||||
// RateLimit error.
|
// RateLimit error.
|
||||||
manager.join([focus], focusActive);
|
manager.join([focus], focusActive);
|
||||||
@@ -654,7 +657,7 @@ describe("MembershipManager", () => {
|
|||||||
it("abandons retry loop if leave() was called before sending state event", async () => {
|
it("abandons retry loop if leave() was called before sending state event", async () => {
|
||||||
const handle = createAsyncHandle(client._unstable_sendDelayedStateEvent);
|
const handle = createAsyncHandle(client._unstable_sendDelayedStateEvent);
|
||||||
|
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
manager.join([focus], focusActive);
|
manager.join([focus], focusActive);
|
||||||
handle.reject?.(
|
handle.reject?.(
|
||||||
new MatrixError(
|
new MatrixError(
|
||||||
@@ -689,7 +692,7 @@ describe("MembershipManager", () => {
|
|||||||
new Headers({ "Retry-After": "1" }),
|
new Headers({ "Retry-After": "1" }),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
manager.join([focus], focusActive);
|
manager.join([focus], focusActive);
|
||||||
|
|
||||||
// Hit rate limit
|
// Hit rate limit
|
||||||
@@ -722,7 +725,7 @@ describe("MembershipManager", () => {
|
|||||||
new Headers({ "Retry-After": "2" }),
|
new Headers({ "Retry-After": "2" }),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
manager.join([focus], focusActive, delayEventSendError);
|
manager.join([focus], focusActive, delayEventSendError);
|
||||||
|
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
@@ -742,7 +745,7 @@ describe("MembershipManager", () => {
|
|||||||
new Headers({ "Retry-After": "1" }),
|
new Headers({ "Retry-After": "1" }),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
manager.join([focus], focusActive, delayEventRestartError);
|
manager.join([focus], focusActive, delayEventRestartError);
|
||||||
|
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
@@ -753,7 +756,7 @@ describe("MembershipManager", () => {
|
|||||||
it("falls back to using pure state events when some error occurs while sending delayed events", async () => {
|
it("falls back to using pure state events when some error occurs while sending delayed events", async () => {
|
||||||
const unrecoverableError = jest.fn();
|
const unrecoverableError = jest.fn();
|
||||||
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(new HTTPError("unknown", 601));
|
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(new HTTPError("unknown", 601));
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
manager.join([focus], focusActive, unrecoverableError);
|
manager.join([focus], focusActive, unrecoverableError);
|
||||||
await waitForMockCall(client.sendStateEvent);
|
await waitForMockCall(client.sendStateEvent);
|
||||||
expect(unrecoverableError).not.toHaveBeenCalledWith();
|
expect(unrecoverableError).not.toHaveBeenCalledWith();
|
||||||
@@ -762,7 +765,7 @@ describe("MembershipManager", () => {
|
|||||||
it("retries before failing in case its a network error", async () => {
|
it("retries before failing in case its a network error", async () => {
|
||||||
const unrecoverableError = jest.fn();
|
const unrecoverableError = jest.fn();
|
||||||
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(new HTTPError("unknown", 501));
|
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(new HTTPError("unknown", 501));
|
||||||
const manager = new MembershipManager(
|
const manager = new LegacyMembershipManager(
|
||||||
{ networkErrorRetryMs: 1000, maximumNetworkErrorRetryCount: 7 },
|
{ networkErrorRetryMs: 1000, maximumNetworkErrorRetryCount: 7 },
|
||||||
room,
|
room,
|
||||||
client,
|
client,
|
||||||
@@ -784,7 +787,7 @@ describe("MembershipManager", () => {
|
|||||||
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(
|
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(
|
||||||
new UnsupportedDelayedEventsEndpointError("not supported", "sendDelayedStateEvent"),
|
new UnsupportedDelayedEventsEndpointError("not supported", "sendDelayedStateEvent"),
|
||||||
);
|
);
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
manager.join([focus], focusActive, unrecoverableError);
|
manager.join([focus], focusActive, unrecoverableError);
|
||||||
await jest.advanceTimersByTimeAsync(1);
|
await jest.advanceTimersByTimeAsync(1);
|
||||||
|
|
||||||
@@ -794,7 +797,7 @@ describe("MembershipManager", () => {
|
|||||||
});
|
});
|
||||||
describe("probablyLeft", () => {
|
describe("probablyLeft", () => {
|
||||||
it("emits probablyLeft when the membership manager could not hear back from the server for the duration of the delayed event", async () => {
|
it("emits probablyLeft when the membership manager could not hear back from the server for the duration of the delayed event", async () => {
|
||||||
const manager = new MembershipManager(
|
const manager = new LegacyMembershipManager(
|
||||||
{ delayedLeaveEventDelayMs: 10000 },
|
{ delayedLeaveEventDelayMs: 10000 },
|
||||||
room,
|
room,
|
||||||
client,
|
client,
|
||||||
@@ -852,7 +855,7 @@ describe("MembershipManager", () => {
|
|||||||
|
|
||||||
describe("updateCallIntent()", () => {
|
describe("updateCallIntent()", () => {
|
||||||
it("should fail if the user has not joined the call", async () => {
|
it("should fail if the user has not joined the call", async () => {
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
// After joining we want our own focus to be the one we select.
|
// After joining we want our own focus to be the one we select.
|
||||||
try {
|
try {
|
||||||
await manager.updateCallIntent("video");
|
await manager.updateCallIntent("video");
|
||||||
@@ -861,7 +864,7 @@ describe("MembershipManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("can adjust the intent", async () => {
|
it("can adjust the intent", async () => {
|
||||||
const manager = new MembershipManager({}, room, client, callSession);
|
const manager = new LegacyMembershipManager({}, room, client, callSession);
|
||||||
manager.join([]);
|
manager.join([]);
|
||||||
expect(manager.isActivated()).toEqual(true);
|
expect(manager.isActivated()).toEqual(true);
|
||||||
const membership = mockCallMembership({ ...sessionMembershipTemplate, user_id: client.getUserId()! }, room.roomId);
|
const membership = mockCallMembership({ ...sessionMembershipTemplate, user_id: client.getUserId()! }, room.roomId);
|
||||||
@@ -874,7 +877,7 @@ describe("MembershipManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("does nothing if the intent doesn't change", async () => {
|
it("does nothing if the intent doesn't change", async () => {
|
||||||
const manager = new MembershipManager({ callIntent: "video" }, room, client, callSession);
|
const manager = new LegacyMembershipManager({ callIntent: "video" }, room, client, callSession);
|
||||||
manager.join([]);
|
manager.join([]);
|
||||||
expect(manager.isActivated()).toEqual(true);
|
expect(manager.isActivated()).toEqual(true);
|
||||||
const membership = mockCallMembership(
|
const membership = mockCallMembership(
|
||||||
@@ -886,6 +889,7 @@ describe("MembershipManager", () => {
|
|||||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(0);
|
expect(client.sendStateEvent).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("StickyEventMembershipManager", () => {
|
describe("StickyEventMembershipManager", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -914,15 +918,15 @@ describe("MembershipManager", () => {
|
|||||||
{
|
{
|
||||||
application: { type: "m.call" },
|
application: { type: "m.call" },
|
||||||
member: {
|
member: {
|
||||||
user_id: "@alice:example.org",
|
claimed_user_id: "@alice:example.org",
|
||||||
id: "_@alice:example.org_AAAAAAA_m.call",
|
id: "_@alice:example.org_AAAAAAA_m.call",
|
||||||
device_id: "AAAAAAA",
|
claimed_device_id: "AAAAAAA",
|
||||||
},
|
},
|
||||||
slot_id: "m.call#",
|
slot_id: "m.call#",
|
||||||
rtc_transports: [focus],
|
rtc_transports: [focus],
|
||||||
versions: [],
|
versions: [],
|
||||||
msc4354_sticky_key: "_@alice:example.org_AAAAAAA_m.call",
|
msc4354_sticky_key: "_@alice:example.org_AAAAAAA_m.call",
|
||||||
},
|
} satisfies RtcMembershipData,
|
||||||
);
|
);
|
||||||
updateDelayedEventHandle.resolve?.();
|
updateDelayedEventHandle.resolve?.();
|
||||||
|
|
||||||
@@ -949,7 +953,7 @@ it("Should prefix log with MembershipManager used", () => {
|
|||||||
const client = makeMockClient("@alice:example.org", "AAAAAAA");
|
const client = makeMockClient("@alice:example.org", "AAAAAAA");
|
||||||
const room = makeMockRoom([sessionMembershipTemplate]);
|
const room = makeMockRoom([sessionMembershipTemplate]);
|
||||||
|
|
||||||
const membershipManager = new MembershipManager(undefined, room, client, callSession);
|
const membershipManager = new LegacyMembershipManager(undefined, room, client, callSession);
|
||||||
|
|
||||||
const spy = jest.spyOn(console, "error");
|
const spy = jest.spyOn(console, "error");
|
||||||
// Double join
|
// Double join
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { KnownMembership } from "../@types/membership.ts";
|
|||||||
import { type ISendEventResponse } from "../@types/requests.ts";
|
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 { MembershipManager, StickyEventMembershipManager } from "./MembershipManager.ts";
|
import { LegacyMembershipManager, 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 {
|
||||||
@@ -618,7 +618,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
|||||||
this.slotDescription,
|
this.slotDescription,
|
||||||
this.logger,
|
this.logger,
|
||||||
)
|
)
|
||||||
: new MembershipManager(joinConfig, this.roomSubset, this.client, this.slotDescription, this.logger);
|
: new LegacyMembershipManager(joinConfig, this.roomSubset, this.client, this.slotDescription, this.logger);
|
||||||
|
|
||||||
this.reEmitter.reEmit(this.membershipManager!, [
|
this.reEmitter.reEmit(this.membershipManager!, [
|
||||||
MembershipManagerEvent.ProbablyLeft,
|
MembershipManagerEvent.ProbablyLeft,
|
||||||
|
|||||||
@@ -29,9 +29,13 @@ import { type Room } from "../models/room.ts";
|
|||||||
import {
|
import {
|
||||||
type CallMembership,
|
type CallMembership,
|
||||||
DEFAULT_EXPIRE_DURATION,
|
DEFAULT_EXPIRE_DURATION,
|
||||||
type RtcMembershipData,
|
|
||||||
type SessionMembershipData,
|
|
||||||
} from "./CallMembership.ts";
|
} from "./CallMembership.ts";
|
||||||
|
import type {
|
||||||
|
RtcMembershipData,
|
||||||
|
} from "./membership/rtc.ts"
|
||||||
|
import type {
|
||||||
|
SessionMembershipData,
|
||||||
|
} from "./membership/legacy.ts"
|
||||||
import { type Transport, isMyMembership, type RTCCallIntent, Status, SlotDescription } from "./types.ts";
|
import { type Transport, isMyMembership, type RTCCallIntent, Status, SlotDescription } from "./types.ts";
|
||||||
import { type MembershipConfig, type SessionConfig } from "./MatrixRTCSession.ts";
|
import { type MembershipConfig, type SessionConfig } from "./MatrixRTCSession.ts";
|
||||||
import { ActionScheduler, type ActionUpdate } from "./MembershipManagerActionScheduler.ts";
|
import { ActionScheduler, type ActionUpdate } from "./MembershipManagerActionScheduler.ts";
|
||||||
@@ -181,7 +185,7 @@ type MembershipManagerClient = Pick<
|
|||||||
* - Stop the timer for the delay refresh
|
* - Stop the timer for the delay refresh
|
||||||
* - Stop the timer for updating the state event
|
* - Stop the timer for updating the state event
|
||||||
*/
|
*/
|
||||||
export class MembershipManager
|
export abstract class MembershipManager<MembershipData extends SessionMembershipData|RtcMembershipData>
|
||||||
extends TypedEventEmitter<MembershipManagerEvent, MembershipManagerEventHandlerMap>
|
extends TypedEventEmitter<MembershipManagerEvent, MembershipManagerEventHandlerMap>
|
||||||
implements IMembershipManager
|
implements IMembershipManager
|
||||||
{
|
{
|
||||||
@@ -382,7 +386,7 @@ export class MembershipManager
|
|||||||
protected memberId: string;
|
protected memberId: string;
|
||||||
protected rtcTransport?: Transport;
|
protected rtcTransport?: Transport;
|
||||||
/** @deprecated This will be removed in favor or rtcTransport becoming a list of actively used transports */
|
/** @deprecated This will be removed in favor or rtcTransport becoming a list of actively used transports */
|
||||||
private fociPreferred?: Transport[];
|
protected fociPreferred?: Transport[];
|
||||||
|
|
||||||
// Config:
|
// Config:
|
||||||
private delayedLeaveEventDelayMsOverride?: number;
|
private delayedLeaveEventDelayMsOverride?: number;
|
||||||
@@ -473,15 +477,7 @@ export class MembershipManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// an abstraction to switch between sending state or a sticky event
|
protected abstract clientSendDelayedDisconnectMembership: () => Promise<SendDelayedEventResponse>;
|
||||||
protected clientSendDelayedDisconnectMembership: () => Promise<SendDelayedEventResponse> = () =>
|
|
||||||
this.client._unstable_sendDelayedStateEvent(
|
|
||||||
this.room.roomId,
|
|
||||||
{ delay: this.delayedLeaveEventDelayMs },
|
|
||||||
EventType.GroupCallMemberPrefix,
|
|
||||||
{},
|
|
||||||
this.memberId,
|
|
||||||
);
|
|
||||||
|
|
||||||
// HANDLERS (used in the membershipLoopHandler)
|
// HANDLERS (used in the membershipLoopHandler)
|
||||||
private async sendOrResendDelayedLeaveEvent(): Promise<ActionUpdate> {
|
private async sendOrResendDelayedLeaveEvent(): Promise<ActionUpdate> {
|
||||||
@@ -669,16 +665,9 @@ export class MembershipManager
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected clientSendMembership: (
|
protected abstract clientSendMembership: (
|
||||||
myMembership: RtcMembershipData | SessionMembershipData | EmptyObject,
|
myMembership: MembershipData | EmptyObject,
|
||||||
) => Promise<ISendEventResponse> = (myMembership) => {
|
) => Promise<ISendEventResponse>;
|
||||||
return this.client.sendStateEvent(
|
|
||||||
this.room.roomId,
|
|
||||||
EventType.GroupCallMemberPrefix,
|
|
||||||
myMembership as EmptyObject | SessionMembershipData,
|
|
||||||
this.memberId,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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))
|
||||||
@@ -771,30 +760,7 @@ export class MembershipManager
|
|||||||
/**
|
/**
|
||||||
* Constructs our own membership
|
* Constructs our own membership
|
||||||
*/
|
*/
|
||||||
protected makeMyMembership(expires: number): SessionMembershipData | RtcMembershipData {
|
protected abstract makeMyMembership(expires: number): MembershipData;
|
||||||
const ownMembership = this.ownMembership;
|
|
||||||
|
|
||||||
const focusObjects =
|
|
||||||
this.rtcTransport === undefined
|
|
||||||
? {
|
|
||||||
focus_active: { type: "livekit", focus_selection: "oldest_membership" } as const,
|
|
||||||
foci_preferred: this.fociPreferred ?? [],
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
focus_active: { type: "livekit", focus_selection: "multi_sfu" } as const,
|
|
||||||
foci_preferred: [this.rtcTransport, ...(this.fociPreferred ?? [])],
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
"application": this.slotDescription.application,
|
|
||||||
"call_id": this.slotDescription.id,
|
|
||||||
"scope": "m.room",
|
|
||||||
"device_id": this.deviceId,
|
|
||||||
expires,
|
|
||||||
"m.call.intent": this.callIntent,
|
|
||||||
...focusObjects,
|
|
||||||
...(ownMembership !== undefined ? { created_ts: ownMembership.createdTs() } : undefined),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error checks and handlers
|
// Error checks and handlers
|
||||||
|
|
||||||
@@ -1020,11 +986,65 @@ export class MembershipManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles sending membership for MSC3401 RTC events.
|
||||||
|
*/
|
||||||
|
export class LegacyMembershipManager extends MembershipManager<SessionMembershipData> {
|
||||||
|
protected clientSendDelayedDisconnectMembership: () => Promise<SendDelayedEventResponse> = () =>
|
||||||
|
this.client._unstable_sendDelayedStateEvent(
|
||||||
|
this.room.roomId,
|
||||||
|
{ delay: this.delayedLeaveEventDelayMs },
|
||||||
|
EventType.GroupCallMemberPrefix,
|
||||||
|
{},
|
||||||
|
this.memberId,
|
||||||
|
);
|
||||||
|
|
||||||
|
protected makeMyMembership(expires: number): SessionMembershipData {
|
||||||
|
const ownMembership = this.ownMembership;
|
||||||
|
|
||||||
|
const focusObjects =
|
||||||
|
this.rtcTransport === undefined
|
||||||
|
? {
|
||||||
|
focus_active: { type: "livekit", focus_selection: "oldest_membership" } as const,
|
||||||
|
foci_preferred: this.fociPreferred ?? [],
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
focus_active: { type: "livekit", focus_selection: "multi_sfu" } as const,
|
||||||
|
foci_preferred: [this.rtcTransport, ...(this.fociPreferred ?? [])],
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
"application": this.slotDescription.application,
|
||||||
|
"call_id": this.slotDescription.id ?? "",
|
||||||
|
"scope": "m.room",
|
||||||
|
"device_id": this.deviceId,
|
||||||
|
expires,
|
||||||
|
"m.call.intent": this.callIntent,
|
||||||
|
...focusObjects,
|
||||||
|
...(ownMembership !== undefined ? { created_ts: ownMembership.createdTs() } : undefined),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected clientSendMembership: (
|
||||||
|
myMembership: SessionMembershipData | EmptyObject,
|
||||||
|
) => Promise<ISendEventResponse> = (myMembership) => {
|
||||||
|
return this.client.sendStateEvent(
|
||||||
|
this.room.roomId,
|
||||||
|
EventType.GroupCallMemberPrefix,
|
||||||
|
myMembership,
|
||||||
|
this.memberId,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of the Membership manager that uses sticky events
|
* Implementation of the Membership manager that uses sticky events
|
||||||
* rather than state events.
|
* rather than state events.
|
||||||
|
*
|
||||||
|
* This exclusively sends RTCMembershipData
|
||||||
*/
|
*/
|
||||||
export class StickyEventMembershipManager extends MembershipManager {
|
export class StickyEventMembershipManager extends MembershipManager<RtcMembershipData> {
|
||||||
public constructor(
|
public constructor(
|
||||||
joinConfig: (SessionConfig & MembershipConfig) | undefined,
|
joinConfig: (SessionConfig & MembershipConfig) | undefined,
|
||||||
room: Pick<Room, "getLiveTimeline" | "roomId" | "getVersion">,
|
room: Pick<Room, "getLiveTimeline" | "roomId" | "getVersion">,
|
||||||
@@ -1036,6 +1056,8 @@ export class StickyEventMembershipManager extends MembershipManager {
|
|||||||
super(joinConfig, room, clientWithSticky, sessionDescription, parentLogger);
|
super(joinConfig, room, clientWithSticky, sessionDescription, parentLogger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected readonly eventType = EventType.RTCMembership;
|
||||||
|
|
||||||
protected clientSendDelayedDisconnectMembership: () => Promise<SendDelayedEventResponse> = () =>
|
protected clientSendDelayedDisconnectMembership: () => Promise<SendDelayedEventResponse> = () =>
|
||||||
this.clientWithSticky._unstable_sendStickyDelayedEvent(
|
this.clientWithSticky._unstable_sendStickyDelayedEvent(
|
||||||
this.room.roomId,
|
this.room.roomId,
|
||||||
@@ -1047,7 +1069,7 @@ export class StickyEventMembershipManager extends MembershipManager {
|
|||||||
);
|
);
|
||||||
|
|
||||||
protected clientSendMembership: (
|
protected clientSendMembership: (
|
||||||
myMembership: RtcMembershipData | SessionMembershipData | EmptyObject,
|
myMembership: RtcMembershipData | EmptyObject,
|
||||||
) => Promise<ISendEventResponse> = (myMembership) => {
|
) => Promise<ISendEventResponse> = (myMembership) => {
|
||||||
return this.clientWithSticky._unstable_sendStickyEvent(
|
return this.clientWithSticky._unstable_sendStickyEvent(
|
||||||
this.room.roomId,
|
this.room.roomId,
|
||||||
@@ -1066,7 +1088,7 @@ export class StickyEventMembershipManager extends MembershipManager {
|
|||||||
return super.actionUpdateFromErrors(e, t, StickyEventMembershipManager.nameMap.get(m) ?? "unknown");
|
return super.actionUpdateFromErrors(e, t, StickyEventMembershipManager.nameMap.get(m) ?? "unknown");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected makeMyMembership(expires: number): SessionMembershipData | RtcMembershipData {
|
protected makeMyMembership(_expires: number): RtcMembershipData {
|
||||||
const ownMembership = this.ownMembership;
|
const ownMembership = this.ownMembership;
|
||||||
|
|
||||||
const relationObject = ownMembership?.eventId
|
const relationObject = ownMembership?.eventId
|
||||||
@@ -1079,7 +1101,7 @@ export class StickyEventMembershipManager extends MembershipManager {
|
|||||||
},
|
},
|
||||||
slot_id: slotDescriptionToId(this.slotDescription),
|
slot_id: slotDescriptionToId(this.slotDescription),
|
||||||
rtc_transports: this.rtcTransport ? [this.rtcTransport] : [],
|
rtc_transports: this.rtcTransport ? [this.rtcTransport] : [],
|
||||||
member: { device_id: this.deviceId, user_id: this.client.getUserId()!, id: this.memberId },
|
member: { claimed_device_id: this.deviceId, claimed_user_id: this.client.getUserId()!, id: this.memberId },
|
||||||
versions: [],
|
versions: [],
|
||||||
...relationObject,
|
...relationObject,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user