refactor(player): E.14 — extract loudness-backfill-window helpers (#577)

The `LOUDNESS_BACKFILL_WINDOW_AHEAD` constant, the predicate
`isTrackInsideLoudnessBackfillWindow`, and the id-list collector
`collectLoudnessBackfillWindowTrackIds` move into
`src/store/loudnessBackfillWindow.ts`.

`isTrackInsideLoudnessBackfillWindow` was rewritten to be pure — it
now takes the queue / queueIndex / currentTrack as explicit
parameters instead of reading `usePlayerStore.getState()` internally.
The one call site in `refreshLoudnessForTrack` reads state once and
passes it through. Net: zero behaviour change, clearer test surface
(no store mocks), no top-level coupling back to playerStore.

`prefetchLoudnessForEnqueuedTracks` stays in playerStore — it calls
`refreshLoudnessForTrack` which lives there too.

13 focused tests pin the window-slides-with-queueIndex contract, the
clamp at the end of the queue, the empty-input fallbacks, and the
duplicate collapse when currentTrack is also in the ahead range.

playerStore 3261 → 3238 LOC.
This commit is contained in:
Frank Stellmacher
2026-05-12 14:36:55 +02:00
committed by GitHub
parent c88649836e
commit 08d098d5aa
3 changed files with 147 additions and 31 deletions
+48
View File
@@ -0,0 +1,48 @@
import type { Track } from './playerStore';
/**
* After a bulk enqueue (queue replace, append-many, lucky-mix) the runtime
* warms the loudness cache for the current track + the next N entries so
* the engine's `audio_chain_preload` sees a real cached gain instead of
* the startup trim. These helpers compute that window — both as a
* "does this id sit inside it?" predicate and as the explicit id list
* the prefetch loop iterates over.
*
* Pure functions of the state slice — no store imports, no side effects.
* The caller passes the queue + index + current track so the test surface
* stays trivial and there's no top-level coupling back to playerStore.
*/
export const LOUDNESS_BACKFILL_WINDOW_AHEAD = 5;
export function isTrackInsideLoudnessBackfillWindow(
trackId: string,
queue: Track[],
queueIndex: number,
currentTrack: Track | null,
): boolean {
if (!trackId) return false;
if (currentTrack?.id === trackId) return true;
if (queue.length === 0) return false;
const start = Math.max(0, queueIndex + 1);
const end = Math.min(queue.length, start + LOUDNESS_BACKFILL_WINDOW_AHEAD);
for (let i = start; i < end; i++) {
if (queue[i]?.id === trackId) return true;
}
return false;
}
export function collectLoudnessBackfillWindowTrackIds(
queue: Track[],
queueIndex: number,
currentTrack: Track | null,
): string[] {
const ids = new Set<string>();
if (currentTrack?.id) ids.add(currentTrack.id);
const start = Math.max(0, queueIndex + 1);
const end = Math.min(queue.length, start + LOUDNESS_BACKFILL_WINDOW_AHEAD);
for (let i = start; i < end; i++) {
const tid = queue[i]?.id;
if (tid) ids.add(tid);
}
return Array.from(ids);
}