diff --git a/.changeset/empty-bugs-pull.md b/.changeset/empty-bugs-pull.md new file mode 100644 index 00000000..4bf90596 --- /dev/null +++ b/.changeset/empty-bugs-pull.md @@ -0,0 +1,5 @@ +--- +"@livekit/rtc-node": patch +--- + +fix(rtc): consistent connection state handling diff --git a/packages/livekit-rtc/src/room.ts b/packages/livekit-rtc/src/room.ts index e71f509e..db80fe22 100644 --- a/packages/livekit-rtc/src/room.ts +++ b/packages/livekit-rtc/src/room.ts @@ -107,9 +107,9 @@ export class Room extends (EventEmitter as new () => TypedEmitter private _token?: string; private _serverUrl?: string; + private _connectionState: ConnectionState = ConnectionState.CONN_DISCONNECTED; e2eeManager?: E2EEManager; - connectionState: ConnectionState = ConnectionState.CONN_DISCONNECTED; remoteParticipants: Map = new Map(); localParticipant?: LocalParticipant; @@ -118,6 +118,10 @@ export class Room extends (EventEmitter as new () => TypedEmitter super(); } + get connectionState() { + return this._connectionState; + } + get name(): string | undefined { return this.info?.name; } @@ -262,7 +266,6 @@ export class Room extends (EventEmitter as new () => TypedEmitter this._token = token; this._serverUrl = url; this.info = cb.message.value.room!.info; - this.connectionState = ConnectionState.CONN_CONNECTED; // Reset the abort controller for this connection session so that // a previous disconnect doesn't immediately cancel new operations. this.disconnectController = new AbortController(); @@ -281,6 +284,7 @@ export class Room extends (EventEmitter as new () => TypedEmitter rp.trackPublications.set(publication.sid!, publication); } } + this.updateConnectionState(ConnectionState.CONN_CONNECTED); break; case 'error': default: @@ -321,6 +325,14 @@ export class Room extends (EventEmitter as new () => TypedEmitter this.removeAllListeners(); } + private updateConnectionState(newState: ConnectionState) { + if (this._connectionState === newState) { + return; + } + this._connectionState = newState; + this.emit(RoomEvent.ConnectionStateChanged, this._connectionState); + } + // Runs at most once per connection session. The FFI layer and explicit // disconnect() both race to get here — whichever wins emits the events, // the other is a no-op. A reconnect via connect() clears hasCleanedUp. @@ -359,12 +371,7 @@ export class Room extends (EventEmitter as new () => TypedEmitter // to reject and clean up their event listeners. this.disconnectController.abort(); - // Only emit ConnectionStateChanged if the FFI 'connectionStateChanged' - // path didn't already flip us to DISCONNECTED. - if (this.connectionState !== ConnectionState.CONN_DISCONNECTED) { - this.connectionState = ConnectionState.CONN_DISCONNECTED; - this.emit(RoomEvent.ConnectionStateChanged, this.connectionState); - } + this.updateConnectionState(ConnectionState.CONN_DISCONNECTED); this.emit(RoomEvent.Disconnected, reason); } @@ -678,14 +685,7 @@ export class Room extends (EventEmitter as new () => TypedEmitter this.emit(RoomEvent.EncryptionError, new Error('internal server error')); } } else if (ev.case == 'connectionStateChanged') { - const newState = ev.value.state!; - // Skip redundant transitions — cleanupOnDisconnect may have already - // flipped us to DISCONNECTED, and we don't want to emit the event twice. - if (this.connectionState === newState) { - return; - } - this.connectionState = newState; - this.emit(RoomEvent.ConnectionStateChanged, this.connectionState); + this.updateConnectionState(ev.value.state!); /*} else if (ev.case == 'connected') { this.emit(RoomEvent.Connected);*/ } else if (ev.case == 'disconnected') {