refactor(player): E.9 — extract scheduled pause/resume timer lifecycle (#572)

Two timer mutables (`scheduledPauseTimer`, `scheduledResumeTimer`) plus
the three clear helpers move into `src/store/scheduleTimers.ts`. The
module also gains `schedulePauseTimer(delayMs, onFire)` /
`scheduleResumeTimer(delayMs, onFire)` so callers no longer need to do
the `window.setTimeout(...) as unknown as number` cast or null the
handle inside the fire callback — the module auto-clears its own
reference before invoking the user callback.

`schedulePauseIn` / `scheduleResumeIn` store actions are now four lines
shorter each and don't reach into the timer mutables.

playerStore 3389 → 3369 LOC. 9 focused tests cover schedule + fire +
clear + replace-on-reschedule + independence between the two timers.
This commit is contained in:
Frank Stellmacher
2026-05-12 14:01:36 +02:00
committed by GitHub
parent 18b88e3ae0
commit 89bf7e2364
3 changed files with 154 additions and 31 deletions
+11 -31
View File
@@ -58,6 +58,13 @@ import {
markLoudnessStable,
setCachedLoudnessGain,
} from './loudnessGainCache';
import {
clearAllPlaybackScheduleTimers,
clearScheduledPauseTimers,
clearScheduledResumeTimers,
schedulePauseTimer,
scheduleResumeTimer,
} from './scheduleTimers';
// Re-export the playback-progress public surface so existing call sites
// (PlayerBar, FullscreenPlayer, WaveformSeek, LyricsPane, MobilePlayerView,
@@ -451,29 +458,6 @@ const STORE_PROGRESS_COMMIT_MIN_DELTA_SEC = 5.0;
let lastStoreProgressCommitAt = 0;
/** Deferred pause / resume — cleared on stop, new track, manual pause/resume. */
let scheduledPauseTimer: number | null = null;
let scheduledResumeTimer: number | null = null;
function clearScheduledPauseTimers() {
if (scheduledPauseTimer != null) {
window.clearTimeout(scheduledPauseTimer);
scheduledPauseTimer = null;
}
}
function clearScheduledResumeTimers() {
if (scheduledResumeTimer != null) {
window.clearTimeout(scheduledResumeTimer);
scheduledResumeTimer = null;
}
}
function clearAllPlaybackScheduleTimers() {
clearScheduledPauseTimers();
clearScheduledResumeTimers();
}
function setSeekTarget(seconds: number) {
seekTarget = seconds;
seekTargetSetAt = Date.now();
@@ -2686,32 +2670,28 @@ export const usePlayerStore = create<PlayerState>()(
schedulePauseIn: (seconds) => {
const s = get();
if (!s.isPlaying) return;
clearScheduledPauseTimers();
const delayMs = Math.max(500, Math.round(Number(seconds) * 1000));
const startedAt = Date.now();
const at = startedAt + delayMs;
set({ scheduledPauseAtMs: at, scheduledPauseStartMs: startedAt });
scheduledPauseTimer = window.setTimeout(() => {
scheduledPauseTimer = null;
schedulePauseTimer(delayMs, () => {
set({ scheduledPauseAtMs: null, scheduledPauseStartMs: null });
get().pause();
}, delayMs) as unknown as number;
});
},
scheduleResumeIn: (seconds) => {
const s = get();
if (s.isPlaying) return;
if (!s.currentTrack && !s.currentRadio) return;
clearScheduledResumeTimers();
const delayMs = Math.max(500, Math.round(Number(seconds) * 1000));
const startedAt = Date.now();
const at = startedAt + delayMs;
set({ scheduledResumeAtMs: at, scheduledResumeStartMs: startedAt });
scheduledResumeTimer = window.setTimeout(() => {
scheduledResumeTimer = null;
scheduleResumeTimer(delayMs, () => {
set({ scheduledResumeAtMs: null, scheduledResumeStartMs: null });
get().resume();
}, delayMs) as unknown as number;
});
},
togglePlay: () => {