From 3fc8807b1d431af1bbcb7bdb36673ccff2283e43 Mon Sep 17 00:00:00 2001 From: Etan Joseph Heyman Date: Thu, 19 Feb 2026 04:03:38 +0200 Subject: [PATCH] nightshift: fix event listener leak in useAudioPreloader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cleanup function used `removeEventListener('ended', () => {})` which is a no-op — the anonymous function doesn't match the original handler reference. Track ended handlers in a Map so they can be properly removed during effect cleanup, preventing memory leaks from retained closures. Co-Authored-By: Claude Opus 4.6 --- src/hooks/useAudioPreloader.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/hooks/useAudioPreloader.ts b/src/hooks/useAudioPreloader.ts index ff40d4d..54975e6 100644 --- a/src/hooks/useAudioPreloader.ts +++ b/src/hooks/useAudioPreloader.ts @@ -49,6 +49,8 @@ export function useAudioPreloader( setState({ loaded: 0, total, ready: false }) audioMapRef.current.clear() + const endedHandlers = new Map void>() + snippets.forEach(({ lineNumber, audioUrl }) => { if (!audioUrl) return @@ -92,17 +94,20 @@ export function useAudioPreloader( audio.addEventListener('canplay', handleCanPlay, { once: true }) // Fallback for cached audio audio.addEventListener('error', handleError, { once: true }) audio.addEventListener('ended', handleEnded) + endedHandlers.set(lineNumber, handleEnded) audio.src = audioUrl audio.load() // Explicitly trigger loading }) return () => { // Cleanup all audio elements on unmount - audioMapRef.current.forEach(audio => { + audioMapRef.current.forEach((audio, lineNumber) => { audio.pause() - audio.removeEventListener('ended', () => {}) + const handler = endedHandlers.get(lineNumber) + if (handler) audio.removeEventListener('ended', handler) audio.src = '' }) + endedHandlers.clear() audioMapRef.current.clear() currentlyPlayingRef.current = null }