Skip to content

Commit 13a967a

Browse files
authored
Prepare delayed call leave events more reliably (#4447)
* 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
1 parent 66c8094 commit 13a967a

File tree

2 files changed

+85
-20
lines changed

2 files changed

+85
-20
lines changed

spec/unit/matrixrtc/MatrixRTCSession.spec.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,6 @@ describe("MatrixRTCSession", () => {
4646
client = new MatrixClient({ baseUrl: "base_url" });
4747
client.getUserId = jest.fn().mockReturnValue("@alice:example.org");
4848
client.getDeviceId = jest.fn().mockReturnValue("AAAAAAA");
49-
client.doesServerSupportUnstableFeature = jest.fn((feature) =>
50-
Promise.resolve(feature === "org.matrix.msc4140"),
51-
);
5249
});
5350

5451
afterEach(() => {
@@ -414,6 +411,8 @@ describe("MatrixRTCSession", () => {
414411
client._unstable_sendDelayedStateEvent = sendDelayedStateMock;
415412
client.sendEvent = sendEventMock;
416413

414+
client._unstable_updateDelayedEvent = jest.fn();
415+
417416
mockRoom = makeMockRoom([]);
418417
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
419418
});
@@ -490,6 +489,13 @@ describe("MatrixRTCSession", () => {
490489
);
491490
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
492491
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
492+
493+
// should have tried updating the delayed leave to test that it wasn't replaced by own state
494+
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
495+
// should update delayed disconnect
496+
jest.advanceTimersByTime(5000);
497+
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(2);
498+
493499
jest.useRealTimers();
494500
}
495501

src/matrixrtc/MatrixRTCSession.ts

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
140140
private encryptionKeys = new Map<string, Array<{ key: Uint8Array; timestamp: number }>>();
141141
private lastEncryptionKeyUpdateRequest?: number;
142142

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

1014-
const stateKey = legacy ? localUserId : this.makeMembershipStateKey(localUserId, localDeviceId);
10151016
try {
1016-
await this.client.sendStateEvent(this.room.roomId, EventType.GroupCallMemberPrefix, newContent, stateKey);
1017-
logger.info(`Sent updated call member event.`);
1018-
1019-
// check periodically to see if we need to refresh our member event
1020-
if (this.isJoined()) {
1021-
if (legacy) {
1017+
if (legacy) {
1018+
await this.client.sendStateEvent(
1019+
this.room.roomId,
1020+
EventType.GroupCallMemberPrefix,
1021+
newContent,
1022+
localUserId,
1023+
);
1024+
if (this.isJoined()) {
1025+
// check periodically to see if we need to refresh our member event
10221026
this.memberEventTimeout = setTimeout(
10231027
this.triggerCallMembershipEventUpdate,
10241028
MEMBER_EVENT_CHECK_PERIOD,
10251029
);
1026-
} else {
1030+
}
1031+
} else if (this.isJoined()) {
1032+
const stateKey = this.makeMembershipStateKey(localUserId, localDeviceId);
1033+
const prepareDelayedDisconnection = async (): Promise<void> => {
10271034
try {
10281035
// TODO: If delayed event times out, re-join!
10291036
const res = await this.client._unstable_sendDelayedStateEvent(
@@ -1035,12 +1042,63 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
10351042
{}, // leave event
10361043
stateKey,
10371044
);
1038-
this.scheduleDelayDisconnection(res.delay_id);
1045+
this.disconnectDelayId = res.delay_id;
1046+
} catch (e) {
1047+
// TODO: Retry if rate-limited
1048+
logger.error("Failed to prepare delayed disconnection event:", e);
1049+
}
1050+
};
1051+
await prepareDelayedDisconnection();
1052+
// Send join event _after_ preparing the delayed disconnection event
1053+
await this.client.sendStateEvent(
1054+
this.room.roomId,
1055+
EventType.GroupCallMemberPrefix,
1056+
newContent,
1057+
stateKey,
1058+
);
1059+
// If sending state cancels your own delayed state, prepare another delayed state
1060+
// TODO: Remove this once MSC4140 is stable & doesn't cancel own delayed state
1061+
if (this.disconnectDelayId !== undefined) {
1062+
try {
1063+
await this.client._unstable_updateDelayedEvent(
1064+
this.disconnectDelayId,
1065+
UpdateDelayedEventAction.Restart,
1066+
);
1067+
} catch (e) {
1068+
// TODO: Make embedded client include errcode, and retry only if not M_NOT_FOUND (or rate-limited)
1069+
logger.warn("Failed to update delayed disconnection event, prepare it again:", e);
1070+
this.disconnectDelayId = undefined;
1071+
await prepareDelayedDisconnection();
1072+
}
1073+
}
1074+
if (this.disconnectDelayId !== undefined) {
1075+
this.scheduleDelayDisconnection();
1076+
}
1077+
} else {
1078+
let sentDelayedDisconnect = false;
1079+
if (this.disconnectDelayId !== undefined) {
1080+
try {
1081+
await this.client._unstable_updateDelayedEvent(
1082+
this.disconnectDelayId,
1083+
UpdateDelayedEventAction.Send,
1084+
);
1085+
sentDelayedDisconnect = true;
10391086
} catch (e) {
1040-
logger.error("Failed to send delayed event:", e);
1087+
// TODO: Retry if rate-limited
1088+
logger.error("Failed to send our delayed disconnection event:", e);
10411089
}
1090+
this.disconnectDelayId = undefined;
1091+
}
1092+
if (!sentDelayedDisconnect) {
1093+
await this.client.sendStateEvent(
1094+
this.room.roomId,
1095+
EventType.GroupCallMemberPrefix,
1096+
{},
1097+
this.makeMembershipStateKey(localUserId, localDeviceId),
1098+
);
10421099
}
10431100
}
1101+
logger.info("Sent updated call member event.");
10441102
} catch (e) {
10451103
const resendDelay = CALL_MEMBER_EVENT_RETRY_DELAY_MIN + Math.random() * 2000;
10461104
logger.warn(`Failed to send call member event (retrying in ${resendDelay}): ${e}`);
@@ -1049,18 +1107,19 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
10491107
}
10501108
}
10511109

1052-
private scheduleDelayDisconnection(delayId: string): void {
1053-
this.memberEventTimeout = setTimeout(() => this.delayDisconnection(delayId), 5000);
1110+
private scheduleDelayDisconnection(): void {
1111+
this.memberEventTimeout = setTimeout(this.delayDisconnection, 5000);
10541112
}
10551113

1056-
private async delayDisconnection(delayId: string): Promise<void> {
1114+
private readonly delayDisconnection = async (): Promise<void> => {
10571115
try {
1058-
await this.client._unstable_updateDelayedEvent(delayId, UpdateDelayedEventAction.Restart);
1059-
this.scheduleDelayDisconnection(delayId);
1116+
await this.client._unstable_updateDelayedEvent(this.disconnectDelayId!, UpdateDelayedEventAction.Restart);
1117+
this.scheduleDelayDisconnection();
10601118
} catch (e) {
1061-
logger.error("Failed to delay our disconnection event", e);
1119+
// TODO: Retry if rate-limited
1120+
logger.error("Failed to delay our disconnection event:", e);
10621121
}
1063-
}
1122+
};
10641123

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

0 commit comments

Comments
 (0)