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.
This commit is contained in:
Jannis Braun
2026-02-23 21:13:35 +01:00
parent bc51fc6f7e
commit 61e607204f
+11 -5
View File
@@ -107,6 +107,12 @@ function parseIdentity(identity: string): { userId: string; username: string } {
let _connectGeneration = 0; let _connectGeneration = 0;
/** Strip all listeners then disconnect — prevents ghost Room memory leaks. */
function destroyRoom(room: Room | null): Promise<void> | void {
if (!room) return;
room.removeAllListeners();
return room.disconnect();
}
export function useLiveKit() { export function useLiveKit() {
const [room, setRoom] = useState<Room | null>(null); const [room, setRoom] = useState<Room | null>(null);
@@ -314,8 +320,8 @@ export function useLiveKit() {
if (roomToDisconnect) { if (roomToDisconnect) {
try { try {
console.log('[LiveKit] Disconnecting previous room:', roomToDisconnect.name); console.log('[LiveKit] Destroying previous room:', roomToDisconnect.name);
await roomToDisconnect.disconnect(); await destroyRoom(roomToDisconnect);
} catch (err) { } catch (err) {
console.warn('Error disconnecting from previous room:', err); console.warn('Error disconnecting from previous room:', err);
} }
@@ -428,7 +434,7 @@ export function useLiveKit() {
}); });
await newRoom.connect(url, token); await newRoom.connect(url, token);
if (gen !== _connectGeneration) { newRoom.disconnect(); return; } if (gen !== _connectGeneration) { destroyRoom(newRoom); return; }
_activeRoom = newRoom; _activeRoom = newRoom;
connectedChannelRef.current = storedId; connectedChannelRef.current = storedId;
setConnectedChannelId(storedId); setConnectedChannelId(storedId);
@@ -469,7 +475,7 @@ export function useLiveKit() {
connectedChannelRef.current = null; connectedChannelRef.current = null;
setConnectedChannelId(null); setConnectedChannelId(null);
if (roomRef.current) { if (roomRef.current) {
await roomRef.current.disconnect(); await destroyRoom(roomRef.current);
roomRef.current = null; roomRef.current = null;
_activeRoom = null; _activeRoom = null;
setRoom(null); setRoom(null);
@@ -555,7 +561,7 @@ export function useLiveKit() {
}, [room]); }, [room]);
useEffect(() => { 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; } };
}, []); }, []);