From 61e607204fe18018d756baa06243983ad0fc4997 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:13:35 +0100 Subject: [PATCH] fix: prevent ghost Room memory leak by stripping listeners before disconnect LiveKit's Room.disconnect() tears down WebRTC but leaves .on() handlers attached. Every connect() registers ~15 event handlers, which accumulate on orphaned Room instances during rapid channel switches or HMR, causing MaxListenersExceededWarning. Added destroyRoom() helper that calls removeAllListeners() before disconnect() at all four teardown sites. --- packages/web/src/hooks/useLiveKit.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/web/src/hooks/useLiveKit.ts b/packages/web/src/hooks/useLiveKit.ts index d3aaff2b..fa78bd29 100644 --- a/packages/web/src/hooks/useLiveKit.ts +++ b/packages/web/src/hooks/useLiveKit.ts @@ -107,6 +107,12 @@ function parseIdentity(identity: string): { userId: string; username: string } { let _connectGeneration = 0; +/** Strip all listeners then disconnect — prevents ghost Room memory leaks. */ +function destroyRoom(room: Room | null): Promise | void { + if (!room) return; + room.removeAllListeners(); + return room.disconnect(); +} export function useLiveKit() { const [room, setRoom] = useState(null); @@ -314,8 +320,8 @@ export function useLiveKit() { if (roomToDisconnect) { try { - console.log('[LiveKit] Disconnecting previous room:', roomToDisconnect.name); - await roomToDisconnect.disconnect(); + console.log('[LiveKit] Destroying previous room:', roomToDisconnect.name); + await destroyRoom(roomToDisconnect); } catch (err) { console.warn('Error disconnecting from previous room:', err); } @@ -428,7 +434,7 @@ export function useLiveKit() { }); await newRoom.connect(url, token); - if (gen !== _connectGeneration) { newRoom.disconnect(); return; } + if (gen !== _connectGeneration) { destroyRoom(newRoom); return; } _activeRoom = newRoom; connectedChannelRef.current = storedId; setConnectedChannelId(storedId); @@ -469,7 +475,7 @@ export function useLiveKit() { connectedChannelRef.current = null; setConnectedChannelId(null); if (roomRef.current) { - await roomRef.current.disconnect(); + await destroyRoom(roomRef.current); roomRef.current = null; _activeRoom = null; setRoom(null); @@ -555,7 +561,7 @@ export function useLiveKit() { }, [room]); useEffect(() => { - return () => { _connectGeneration++; SpeakingDetector.getInstance().clear(); if (roomRef.current) { roomRef.current.disconnect(); roomRef.current = null; _activeRoom = null; } }; + return () => { _connectGeneration++; SpeakingDetector.getInstance().clear(); if (roomRef.current) { destroyRoom(roomRef.current); roomRef.current = null; _activeRoom = null; } }; }, []);