fix: gapless player wiped the queue on natural track end (stale closure)

The engine is created once (mount effect), so its onIndex/onEndOfQueue callbacks
captured the first-render syncIndex/setPlaying. useGlobal's setData reads the
`data` from the render that created the setter when given a functional updater —
that's the INITIAL empty state. So when a track ended and the engine called
syncIndex(i) → setState(s => ({...s, index:i})), `s` was {queue:[], index:0}:
the queue got wiped, `current` went undefined, playback stopped and the dock
vanished. Manual track selection was unaffected because playQueue passes a plain
object (no stale `data` read) — which is why it seemed to work.

Route the two engine-invoked setters through refs kept current each render, so a
natural advance mirrors into the live state instead of the stale initial one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 12:33:31 +00:00
co-authored by Claude Opus 4.8
parent 0ef8e9fbd2
commit f0d475ea2e
@@ -60,6 +60,13 @@ export const MusicPlayerHost = () => {
const isRestoringRef = useRef(false);
const seekToRef = useRef<number | null>(null);
const engineIndexRef = useRef(0);
// The engine is created once, so its callbacks would capture first-render closures. useMusicPlayer's
// functional setters (syncIndex/setPlaying) read the state captured at THAT render (the initial EMPTY
// queue) — calling them from a stale closure wipes the queue. Route them through refs kept current.
const syncIndexRef = useRef(syncIndex);
syncIndexRef.current = syncIndex;
const setPlayingRef = useRef(setPlaying);
setPlayingRef.current = setPlaying;
const withToken = (u: string) => (token ? `${u}${u.includes('?') ? '&' : '?'}token=${encodeURIComponent(token)}` : u);
const streamUrl = (t: PlayerTrack) =>
@@ -92,9 +99,9 @@ export const MusicPlayerHost = () => {
},
onIndex: (i) => {
engineIndexRef.current = i; // engine advanced on its own → mirror to UI without restarting
syncIndex(i);
syncIndexRef.current(i);
},
onEndOfQueue: () => setPlaying(false),
onEndOfQueue: () => setPlayingRef.current(false),
onLoadingChange: setLoading,
});
engineRef.current = engine;