diff --git a/spec/unit/matrixrtc/MembershipManager.spec.ts b/spec/unit/matrixrtc/MembershipManager.spec.ts index 18b2fb6309..71f12c5a41 100644 --- a/spec/unit/matrixrtc/MembershipManager.spec.ts +++ b/spec/unit/matrixrtc/MembershipManager.spec.ts @@ -602,6 +602,31 @@ describe("MembershipManager", () => { expect(connectEmit).toHaveBeenCalledWith(Status.Disconnecting, Status.Disconnected); }); }); + describe("local timeout error handling", () => { + it("retries sending restart delayed leave request after configure local timeout", async () => { + const manager = new MembershipManager( + { delayedEventRestartLocalTimeoutMs: 1000 }, + room, + client, + () => undefined, + ); + manager.join([focus], focusActive); + expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1); + await jest.advanceTimersByTimeAsync(1); + + expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1); + + // Simulate a local timeout error + // (client._unstable_updateDelayedEvent as Mock).mockRejectedValue(new HTTPError("timeout", 408)); + // Advance the timers so that the retry happens. + await jest.advanceTimersByTimeAsync(1100); + expect(client._unstable_updateDelayedEvent).toHaveBeenCalledWith("id", "restart", { + localTimeoutMs: 1000, + priority: "auto", + }); + }); + }); + describe("server error handling", () => { // Types of server error: 429 rate limit with no retry-after header, 429 with retry-after, 50x server error (maybe retry every second), connection/socket timeout describe("retries sending delayed leave event", () => { diff --git a/src/matrixrtc/MatrixRTCSession.ts b/src/matrixrtc/MatrixRTCSession.ts index 1951b676a9..58d413f12f 100644 --- a/src/matrixrtc/MatrixRTCSession.ts +++ b/src/matrixrtc/MatrixRTCSession.ts @@ -136,6 +136,15 @@ export interface MembershipConfig { /** @deprecated renamed to `networkErrorRetryMs`*/ callMemberEventRetryDelayMinimum?: number; + /** + * The time (in milliseconds) after which a we consider a delayed event restart http request to have failed. + * Setting this to a lower value will result in more frequent retries but also a higher chance of failiour. + * + * the default local timeout in the js-sdk is 5 seconds. + * @default 5000 + */ + delayedEventRestartLocalTimeoutMs?: number; + /** * If true, use the new to-device transport for sending encryption keys. */ diff --git a/src/matrixrtc/MembershipManager.ts b/src/matrixrtc/MembershipManager.ts index 6839469c42..cca3ce7424 100644 --- a/src/matrixrtc/MembershipManager.ts +++ b/src/matrixrtc/MembershipManager.ts @@ -19,6 +19,7 @@ import { UpdateDelayedEventAction } from "../@types/requests.ts"; import { type MatrixClient } from "../client.ts"; import { UnsupportedDelayedEventsEndpointError } from "../errors.ts"; import { ConnectionError, HTTPError, MatrixError } from "../http-api/errors.ts"; +import { type IRequestOpts } from "../http-api/index.ts"; import { type Logger, logger as rootLogger } from "../logger.ts"; import { type Room } from "../models/room.ts"; import { type CallMembership, DEFAULT_EXPIRE_DURATION, type SessionMembershipData } from "./CallMembership.ts"; @@ -374,6 +375,9 @@ export class MembershipManager return this.joinConfig?.maximumNetworkErrorRetryCount ?? 10; } + private get delayedEventRestartLocalTimeoutMs(): number | undefined { + return this.joinConfig?.delayedEventRestartLocalTimeoutMs; + } // LOOP HANDLER: private async membershipLoopHandler(type: MembershipActionType): Promise { switch (type) { @@ -525,8 +529,11 @@ export class MembershipManager } private async restartDelayedEvent(delayId: string): Promise { + const requestOptions: IRequestOpts = { + localTimeoutMs: this.delayedEventRestartLocalTimeoutMs, + }; return await this.client - ._unstable_updateDelayedEvent(delayId, UpdateDelayedEventAction.Restart) + ._unstable_updateDelayedEvent(delayId, UpdateDelayedEventAction.Restart, requestOptions) .then(() => { this.resetRateLimitCounter(MembershipActionType.RestartDelayedEvent); return createInsertActionUpdate(