UNPKG

timeout-flow

Version:

Fluent, composable, pauseable JavaScript timers and time control flows — plus RAF utilities for frame-based logic.

204 lines (167 loc) 4.64 kB
// ./raf/afterRaf.js import { resolveDelayFnOptions } from '../resolveDelayAndFn.js'; import { attachAbort } from '../abort.js'; import { now } from '../now.js'; /** * Schedules a one-time delayed function using requestAnimationFrame. * * Preferred: * - afterRaf(fn, duration, [options]) * * Also supported (legacy): * - afterRaf(duration, fn, [onFinish], [options]) * * Timing model: * - Uses frame timestamps for progression. * - Paused time does NOT count toward elapsed time (pause freezes remaining). * * @param {Function|string|number} a * @param {Function|string|number|Object} b * @param {Function|Object} [c] - legacy onFinish OR options * @param {Object} [d] - legacy options (when using 4 args) * @param {Function} [c.onFinish] - optional callback after finishing (preferred: in options) * @param {AbortSignal} [c.signal] - optional AbortSignal to auto-cancel * @returns {{ * pause(): void, * resume(): void, * cancel(): void, * reset(): void, * readonly isRunning: boolean, * readonly isPaused: boolean, * readonly isFinished: boolean * }} */ export function afterRaf(a, b, c, d) { const legacyOnFinish = typeof c === 'function' ? c : undefined; const maybeOptions = legacyOnFinish ? d : c; // Require explicit duration: defaultDelay = undefined const { fn, delay, options } = resolveDelayFnOptions(a, b, maybeOptions, undefined); const { signal, onFinish } = legacyOnFinish ? { ...options, onFinish: legacyOnFinish } : options; const totalMs = Math.max(0, delay); let rafId = 0; // remaining time until fire (pause-safe) let remaining = totalMs; // start timestamp for the current running segment let segmentStart = null; // number | null let running = false; let paused = false; let finished = false; const cancelFrame = () => { if (rafId) cancelAnimationFrame(rafId); rafId = 0; }; let cleanupAbort = () => { }; const cleanupAbortListener = () => { cleanupAbort(); cleanupAbort = () => { }; }; const finish = () => { cancelFrame(); running = false; paused = false; finished = true; segmentStart = null; remaining = 0; cleanupAbortListener(); }; const execFinish = () => { finish(); fn?.(); onFinish?.(); }; const cancel = () => { if (finished) return; finish(); }; const attachAbortListener = () => { cleanupAbortListener(); // Preserve semantics: if already aborted, become terminal. if (signal?.aborted) { finished = true; remaining = 0; return; } cleanupAbort = attachAbort(signal, cancel); }; // Attach abort for the lifetime of this timer instance attachAbortListener(); const tick = (ts) => { if (!running || finished) return; if (segmentStart == null) segmentStart = ts; const elapsed = ts - segmentStart; if (elapsed >= remaining) { execFinish(); return; } rafId = requestAnimationFrame(tick); }; const pause = () => { if (!running || finished) return; cancelFrame(); // Freeze remaining based on how long we ran in this segment const currentTime = now(); if (segmentStart != null) { const elapsed = currentTime - segmentStart; remaining = Math.max(0, remaining - elapsed); } segmentStart = null; running = false; paused = true; }; const resume = () => { if (finished || running || !paused) return; if (signal?.aborted) return; // If nothing remains, finish synchronously if (!(remaining > 0)) { execFinish(); return; } paused = false; running = true; segmentStart = null; // will be set on first tick rafId = requestAnimationFrame(tick); }; const reset = () => { cancelFrame(); segmentStart = null; remaining = totalMs; running = false; paused = false; finished = false; // Re-attach abort on reset (more consistent with non-RAF timers) attachAbortListener(); if (finished) { // signal was aborted during attachAbortListener() return; } }; // Start immediately unless already aborted/finished if (!finished) { // Support 0ms: fire on next frame (consistent with RAF timing) if (!(remaining > 0)) { execFinish(); } else { running = true; rafId = requestAnimationFrame(tick); } } else { cleanupAbortListener(); } return { pause, resume, cancel, reset, get isRunning() { return running; }, get isPaused() { return paused; }, get isFinished() { return finished; }, }; }