Skip to content

Commit

Permalink
Prepare delayed call leave events more reliably (#4447)
Browse files Browse the repository at this point in the history
* Prepare delayed call leave events more reliably

- Try sending call join after preparing delayed leave
- On leave, send delayed leave instead of a new event

* Don't rely on errcodes for retry logic

because they are unavailable in widget mode

* Make arrow method readonly

SonarCloud rule typescript:S2933

* Test coverage for restarting delayed call leave

* Remove unneeded unstable_features mock

It's unneeded because all affected methods are mocked
  • Loading branch information
AndrewFerr authored Oct 17, 2024
1 parent 66c8094 commit 13a967a
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 20 deletions.
12 changes: 9 additions & 3 deletions spec/unit/matrixrtc/MatrixRTCSession.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,6 @@ describe("MatrixRTCSession", () => {
client = new MatrixClient({ baseUrl: "base_url" });
client.getUserId = jest.fn().mockReturnValue("@alice:example.org");
client.getDeviceId = jest.fn().mockReturnValue("AAAAAAA");
client.doesServerSupportUnstableFeature = jest.fn((feature) =>
Promise.resolve(feature === "org.matrix.msc4140"),
);
});

afterEach(() => {
Expand Down Expand Up @@ -414,6 +411,8 @@ describe("MatrixRTCSession", () => {
client._unstable_sendDelayedStateEvent = sendDelayedStateMock;
client.sendEvent = sendEventMock;

client._unstable_updateDelayedEvent = jest.fn();

mockRoom = makeMockRoom([]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
});
Expand Down Expand Up @@ -490,6 +489,13 @@ describe("MatrixRTCSession", () => {
);
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);

// should have tried updating the delayed leave to test that it wasn't replaced by own state
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
// should update delayed disconnect
jest.advanceTimersByTime(5000);
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(2);

jest.useRealTimers();
}

Expand Down
93 changes: 76 additions & 17 deletions src/matrixrtc/MatrixRTCSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
private encryptionKeys = new Map<string, Array<{ key: Uint8Array; timestamp: number }>>();
private lastEncryptionKeyUpdateRequest?: number;

private disconnectDelayId: string | undefined;

// We use this to store the last membership fingerprints we saw, so we can proactively re-send encryption keys
// if it looks like a membership has been updated.
private lastMembershipFingerprints: Set<string> | undefined;
Expand Down Expand Up @@ -1011,19 +1013,24 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
newContent = this.makeNewMembership(localDeviceId);
}

const stateKey = legacy ? localUserId : this.makeMembershipStateKey(localUserId, localDeviceId);
try {
await this.client.sendStateEvent(this.room.roomId, EventType.GroupCallMemberPrefix, newContent, stateKey);
logger.info(`Sent updated call member event.`);

// check periodically to see if we need to refresh our member event
if (this.isJoined()) {
if (legacy) {
if (legacy) {
await this.client.sendStateEvent(
this.room.roomId,
EventType.GroupCallMemberPrefix,
newContent,
localUserId,
);
if (this.isJoined()) {
// check periodically to see if we need to refresh our member event
this.memberEventTimeout = setTimeout(
this.triggerCallMembershipEventUpdate,
MEMBER_EVENT_CHECK_PERIOD,
);
} else {
}
} else if (this.isJoined()) {
const stateKey = this.makeMembershipStateKey(localUserId, localDeviceId);
const prepareDelayedDisconnection = async (): Promise<void> => {
try {
// TODO: If delayed event times out, re-join!
const res = await this.client._unstable_sendDelayedStateEvent(
Expand All @@ -1035,12 +1042,63 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
{}, // leave event
stateKey,
);
this.scheduleDelayDisconnection(res.delay_id);
this.disconnectDelayId = res.delay_id;
} catch (e) {
// TODO: Retry if rate-limited
logger.error("Failed to prepare delayed disconnection event:", e);
}
};
await prepareDelayedDisconnection();
// Send join event _after_ preparing the delayed disconnection event
await this.client.sendStateEvent(
this.room.roomId,
EventType.GroupCallMemberPrefix,
newContent,
stateKey,
);
// If sending state cancels your own delayed state, prepare another delayed state
// TODO: Remove this once MSC4140 is stable & doesn't cancel own delayed state
if (this.disconnectDelayId !== undefined) {
try {
await this.client._unstable_updateDelayedEvent(
this.disconnectDelayId,
UpdateDelayedEventAction.Restart,
);
} catch (e) {
// TODO: Make embedded client include errcode, and retry only if not M_NOT_FOUND (or rate-limited)
logger.warn("Failed to update delayed disconnection event, prepare it again:", e);
this.disconnectDelayId = undefined;
await prepareDelayedDisconnection();
}
}
if (this.disconnectDelayId !== undefined) {
this.scheduleDelayDisconnection();
}
} else {
let sentDelayedDisconnect = false;
if (this.disconnectDelayId !== undefined) {
try {
await this.client._unstable_updateDelayedEvent(
this.disconnectDelayId,
UpdateDelayedEventAction.Send,
);
sentDelayedDisconnect = true;
} catch (e) {
logger.error("Failed to send delayed event:", e);
// TODO: Retry if rate-limited
logger.error("Failed to send our delayed disconnection event:", e);
}
this.disconnectDelayId = undefined;
}
if (!sentDelayedDisconnect) {
await this.client.sendStateEvent(
this.room.roomId,
EventType.GroupCallMemberPrefix,
{},
this.makeMembershipStateKey(localUserId, localDeviceId),
);
}
}
logger.info("Sent updated call member event.");
} catch (e) {
const resendDelay = CALL_MEMBER_EVENT_RETRY_DELAY_MIN + Math.random() * 2000;
logger.warn(`Failed to send call member event (retrying in ${resendDelay}): ${e}`);
Expand All @@ -1049,18 +1107,19 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
}
}

private scheduleDelayDisconnection(delayId: string): void {
this.memberEventTimeout = setTimeout(() => this.delayDisconnection(delayId), 5000);
private scheduleDelayDisconnection(): void {
this.memberEventTimeout = setTimeout(this.delayDisconnection, 5000);
}

private async delayDisconnection(delayId: string): Promise<void> {
private readonly delayDisconnection = async (): Promise<void> => {
try {
await this.client._unstable_updateDelayedEvent(delayId, UpdateDelayedEventAction.Restart);
this.scheduleDelayDisconnection(delayId);
await this.client._unstable_updateDelayedEvent(this.disconnectDelayId!, UpdateDelayedEventAction.Restart);
this.scheduleDelayDisconnection();
} catch (e) {
logger.error("Failed to delay our disconnection event", e);
// TODO: Retry if rate-limited
logger.error("Failed to delay our disconnection event:", e);
}
}
};

private stateEventsContainOngoingLegacySession(callMemberEvents: Map<string, MatrixEvent> | undefined): boolean {
if (!callMemberEvents?.size) {
Expand Down

0 comments on commit 13a967a

Please sign in to comment.