From b9c8e42c329e41d24bd05bca3d36d9a363ef1848 Mon Sep 17 00:00:00 2001 From: LautaroPetaccio Date: Wed, 1 Apr 2026 09:38:47 -0300 Subject: [PATCH 1/5] fix: deduplicate getSid() listeners to prevent leak on concurrent calls --- packages/livekit-rtc/src/room.ts | 34 +++++++++++++++------- packages/livekit-rtc/src/tests/e2e.test.ts | 20 +++++++++++++ 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/packages/livekit-rtc/src/room.ts b/packages/livekit-rtc/src/room.ts index 6ef6bf63..03ce8f2a 100644 --- a/packages/livekit-rtc/src/room.ts +++ b/packages/livekit-rtc/src/room.ts @@ -131,6 +131,11 @@ export class Room extends (EventEmitter as new () => TypedEmitter return this._serverUrl; } + // Shared promise for concurrent getSid() callers. Without this, each call + // registers its own RoomSidChanged + Disconnected listeners, and if many + // calls race only one of each pair is cleaned up — leaking the rest. + private sidPromise?: Promise; + /** * Gets the room's server ID. This ID is assigned by the LiveKit server * and is unique for each room session. @@ -144,19 +149,26 @@ export class Room extends (EventEmitter as new () => TypedEmitter if (this.info?.sid && this.info.sid !== '') { return this.info.sid; } - return new Promise((resolve, reject) => { - const handleRoomUpdate = (sid: string) => { - if (sid !== '') { + if (!this.sidPromise) { + this.sidPromise = new Promise((resolve, reject) => { + const handleDisconnect = () => { this.off(RoomEvent.RoomSidChanged, handleRoomUpdate); - resolve(sid); - } - }; - this.on(RoomEvent.RoomSidChanged, handleRoomUpdate); - this.once(RoomEvent.Disconnected, () => { - this.off(RoomEvent.RoomSidChanged, handleRoomUpdate); - reject('Room disconnected before room server id was available'); + this.sidPromise = undefined; + reject('Room disconnected before room server id was available'); + }; + const handleRoomUpdate = (sid: string) => { + if (sid !== '') { + this.off(RoomEvent.RoomSidChanged, handleRoomUpdate); + this.off(RoomEvent.Disconnected as any, handleDisconnect); + this.sidPromise = undefined; + resolve(sid); + } + }; + this.on(RoomEvent.RoomSidChanged, handleRoomUpdate); + this.once(RoomEvent.Disconnected, handleDisconnect); }); - }); + } + return this.sidPromise; } get numParticipants(): number { diff --git a/packages/livekit-rtc/src/tests/e2e.test.ts b/packages/livekit-rtc/src/tests/e2e.test.ts index ad1d5af2..3bbced55 100644 --- a/packages/livekit-rtc/src/tests/e2e.test.ts +++ b/packages/livekit-rtc/src/tests/e2e.test.ts @@ -514,4 +514,24 @@ describeE2E('livekit-rtc e2e', () => { }, testTimeoutMs * 2, ); + + it( + 'concurrent getSid() calls share a single listener and resolve consistently', + async () => { + const { rooms } = await connectTestRooms(1); + const room = rooms[0]!; + + // Fire multiple concurrent getSid() calls — they should all resolve + // to the same SID without leaking event listeners. + const results = await Promise.all([room.getSid(), room.getSid(), room.getSid()]); + + // All calls should return the same non-empty SID + expect(results[0]).toBeTruthy(); + expect(results[1]).toBe(results[0]); + expect(results[2]).toBe(results[0]); + + await room.disconnect(); + }, + testTimeoutMs, + ); }); From 46682eadafb4a64dc2e062ce8f84ca4233b1a182 Mon Sep 17 00:00:00 2001 From: LautaroPetaccio Date: Wed, 1 Apr 2026 11:35:44 -0300 Subject: [PATCH 2/5] chore: add changeset --- .changeset/deduplicate-getsid.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/deduplicate-getsid.md diff --git a/.changeset/deduplicate-getsid.md b/.changeset/deduplicate-getsid.md new file mode 100644 index 00000000..ba8c9bbd --- /dev/null +++ b/.changeset/deduplicate-getsid.md @@ -0,0 +1,5 @@ +--- +'@livekit/rtc-node': patch +--- + +Deduplicate getSid() listeners to prevent event listener leak on concurrent calls From 807b2bce2ab3fcfb0f5b1beef865663b3c7454a7 Mon Sep 17 00:00:00 2001 From: LautaroPetaccio Date: Wed, 1 Apr 2026 11:36:50 -0300 Subject: [PATCH 3/5] chore: add changeset --- .changeset/tall-birds-rest.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tall-birds-rest.md diff --git a/.changeset/tall-birds-rest.md b/.changeset/tall-birds-rest.md new file mode 100644 index 00000000..ba8c9bbd --- /dev/null +++ b/.changeset/tall-birds-rest.md @@ -0,0 +1,5 @@ +--- +'@livekit/rtc-node': patch +--- + +Deduplicate getSid() listeners to prevent event listener leak on concurrent calls From 4c1ccce92b51792db5ba26ec72cede6ab0594ce4 Mon Sep 17 00:00:00 2001 From: LautaroPetaccio Date: Wed, 1 Apr 2026 11:38:34 -0300 Subject: [PATCH 4/5] chore: remove duplicate changeset --- .changeset/deduplicate-getsid.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/deduplicate-getsid.md diff --git a/.changeset/deduplicate-getsid.md b/.changeset/deduplicate-getsid.md deleted file mode 100644 index ba8c9bbd..00000000 --- a/.changeset/deduplicate-getsid.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@livekit/rtc-node': patch ---- - -Deduplicate getSid() listeners to prevent event listener leak on concurrent calls From 9e389945b128157756d3c15611df1c8829ea5fb7 Mon Sep 17 00:00:00 2001 From: LautaroPetaccio Date: Wed, 1 Apr 2026 11:42:42 -0300 Subject: [PATCH 5/5] fix: clear sidPromise on disconnect to prevent hang on reconnect --- packages/livekit-rtc/src/room.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/livekit-rtc/src/room.ts b/packages/livekit-rtc/src/room.ts index 03ce8f2a..739e93bd 100644 --- a/packages/livekit-rtc/src/room.ts +++ b/packages/livekit-rtc/src/room.ts @@ -295,6 +295,10 @@ export class Room extends (EventEmitter as new () => TypedEmitter return ev.message.case == 'disconnect' && ev.message.value.asyncId == res.asyncId; }); + // Clear sidPromise before removing listeners so that a reconnect + // doesn't return a stale, permanently-pending promise. + this.sidPromise = undefined; + FfiClient.instance.removeListener(FfiClientEvent.FfiEvent, this.onFfiEvent); this.removeAllListeners(); }