From 8060d10fccb62ac2153313d9c877e31106f2278a Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 3 Sep 2026 14:46:49 +0100 Subject: [PATCH] Don't report an aborted connect as a connection error When the ConnectionManager stops a Connection while livekitRoom.connect() is still pending (which happens on every join, because the local membership emits twice in quick succession and the connection set is recomputed), livekit-client rejects the pending connect with "Client initiated disconnect". Connection.start() then treated that as a failure: it pushed an UnknownCallError into the state of a connection that was already stopped and rethrew, and since start() is fire-and-forget the throw surfaced as Unhandled promise rejection: Error: Failed to connect to Livekit server in every rageshake, right after a "livekitRoom.connect FAILED ... Client initiated disconnect" line. Nothing was actually wrong: the replacement connection connects fine a moment later. Mark the connection as stopped before disconnecting, and have start() return quietly when the rejection is the abort we asked for. --- .../remoteMembers/Connection.test.ts | 28 +++++++++++++++++++ .../CallViewModel/remoteMembers/Connection.ts | 13 ++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/state/CallViewModel/remoteMembers/Connection.test.ts b/src/state/CallViewModel/remoteMembers/Connection.test.ts index 723295853..ae87e98c2 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.test.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.test.ts @@ -391,6 +391,34 @@ describe("Start connection states", () => { expect(connectedState).toEqual(ConnectionState.LivekitConnected); }); + it("stopping while connecting does not report an error", async () => { + setupTest(); + const connection = setupRemoteConnection(); + + const capturedStates: (ConnectionState | Error)[] = []; + const s = connection.state$.subscribe((value) => { + capturedStates.push(value); + }); + onTestFinished(() => s.unsubscribe()); + + // livekit-client rejects a pending connect() when disconnect() is called. + const pendingConnect = Promise.withResolvers(); + fakeLivekitRoom.connect.mockReturnValue(pendingConnect.promise); + fakeLivekitRoom.disconnect.mockResolvedValue(undefined); + + const started = connection.start(); + await vi.waitFor(() => expect(fakeLivekitRoom.connect).toHaveBeenCalled()); + const stopping = connection.stop(); + pendingConnect.reject(new Error("Client initiated disconnect")); + await stopping; + + // start() resolves rather than rejecting (it is not awaited by the + // ConnectionManager, so a rejection would be unhandled). + await expect(started).resolves.toBeUndefined(); + expect(capturedStates.at(-1)).toEqual(ConnectionState.Stopped); + expect(capturedStates.some((st) => st instanceof Error)).toBe(false); + }); + it("shutting down the scope should stop the connection", async () => { setupTest(); vi.useFakeTimers(); diff --git a/src/state/CallViewModel/remoteMembers/Connection.ts b/src/state/CallViewModel/remoteMembers/Connection.ts index f320e6303..84ea98eef 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.ts @@ -258,6 +258,15 @@ export class Connection { // If we were stopped while connecting, don't proceed to update state. if (this.stopped) return; } catch (error) { + if (this.stopped) { + // stop() was called while we were connecting, which makes the pending + // connect reject. That is the abort we asked for, not a failure, so + // don't record an error state on a stopped connection or rethrow it + // (start() is not awaited by the ConnectionManager, so a throw here + // becomes an unhandled promise rejection). + this.logger.debug(`Connect aborted because the connection was stopped`); + return; + } this.logger.debug(`Failed to connect to LiveKit room: ${error}`); this._state$.next( error instanceof ElementCallError @@ -296,9 +305,11 @@ export class Connection { `stop: disconnecing from lk room ${this.transport.livekit_service_url}`, ); if (this.stopped) return; + // Mark as stopped before disconnecting so that a connect() aborted by the + // disconnect sees the flag and does not report the abort as an error. + this.stopped = true; await this.livekitRoom.disconnect(); this._state$.next(ConnectionState.Stopped); - this.stopped = true; this.logger.debug( `stop: DONE disconnecing from lk room ${this.transport.livekit_service_url}`, );