refactor(audio): Phase 3 — replace RAF polling loop with store subscription
useWaveform.ts: - Remove requestAnimationFrame polling loop that was re-running after every re-initialization and leaking across renders when cleanup didn't fire - Remove local useState for isPlaying/currentTime/duration; these now come directly from usePlayerStore selectors — WaveSurfer event handlers in AudioService already write to the store, so no intermediate sync needed - The useEffect is now a clean async init only; no cleanup needed (AudioService persists intentionally across page navigations) tests/: - Delete 3 obsolete test files that tested removed APIs (logging system, setupAudioContext, ensureAudioContext, initializeAudioContext) - Add tests/audioService.test.ts: 25 tests covering initialize(), play(), pause(), seekTo(), cleanup(), and all WaveSurfer event→store mappings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,21 +22,21 @@ export function useWaveform(
|
||||
containerRef: React.RefObject<HTMLDivElement>,
|
||||
options: UseWaveformOptions
|
||||
) {
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const markersRef = useRef<CommentMarker[]>([]);
|
||||
|
||||
// Playback state comes directly from the store — no intermediate local state
|
||||
// or RAF polling loop needed. The store is updated by WaveSurfer event handlers
|
||||
// in AudioService, so these values are always in sync.
|
||||
const isPlaying = usePlayerStore(state => state.isPlaying);
|
||||
const currentTime = usePlayerStore(state => state.currentTime);
|
||||
const duration = usePlayerStore(state => state.duration);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
if (!options.url || options.url === 'null' || options.url === 'undefined') return;
|
||||
|
||||
// animationFrameId is declared here so the useEffect cleanup can cancel it
|
||||
// even if initializeAudio hasn't finished yet
|
||||
let animationFrameId: number | null = null;
|
||||
|
||||
const initializeAudio = async () => {
|
||||
try {
|
||||
await audioService.initialize(containerRef.current!, options.url!);
|
||||
@@ -72,25 +72,6 @@ export function useWaveform(
|
||||
}
|
||||
}
|
||||
|
||||
// Sync local state from the store at ~15fps via RAF.
|
||||
// The loop is started after initialization so we only poll when loaded.
|
||||
let lastUpdateTime = 0;
|
||||
const updateInterval = 1000 / 15;
|
||||
|
||||
const handleStateUpdate = () => {
|
||||
const now = Date.now();
|
||||
if (now - lastUpdateTime >= updateInterval) {
|
||||
const state = usePlayerStore.getState();
|
||||
setIsPlaying(state.isPlaying);
|
||||
setCurrentTime(state.currentTime);
|
||||
setDuration(state.duration);
|
||||
lastUpdateTime = now;
|
||||
}
|
||||
animationFrameId = requestAnimationFrame(handleStateUpdate);
|
||||
};
|
||||
|
||||
animationFrameId = requestAnimationFrame(handleStateUpdate);
|
||||
|
||||
setIsReady(true);
|
||||
options.onReady?.(audioService.getDuration());
|
||||
} catch (err) {
|
||||
@@ -101,12 +82,6 @@ export function useWaveform(
|
||||
};
|
||||
|
||||
initializeAudio();
|
||||
|
||||
return () => {
|
||||
if (animationFrameId !== null) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
};
|
||||
}, [options.url, options.songId, options.bandId]);
|
||||
|
||||
const play = () => {
|
||||
|
||||
Reference in New Issue
Block a user