UNPKG

timeout-flow

Version:

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

133 lines (110 loc) 3.19 kB
// ./src/TimerBase.js import { parseDuration } from './parseDuration.js'; import { now } from './now.js' /** * Base timer class to manage pause/resume/cancel behavior. * Not meant to be used directly—subclass for concrete behavior. */ export class TimerBase { #ms; #startTime = null; // performance.now() #remaining; #timer = null; #running = false; #paused = false; #finished = false; /** * @param {string|number} duration - The duration to wait in ms or shorthand format */ constructor(duration) { this.#ms = parseDuration(duration); this.#remaining = this.#ms; } /** * Whether the timer is currently running (actively counting down). * @returns {boolean} */ get isRunning() { return this.#running; } /** * Whether the timer is paused (resumable). * @returns {boolean} */ get isPaused() { return this.#paused; } /** * Whether the timer has finished or been canceled (terminal state). * @returns {boolean} */ get isFinished() { return this.#finished; } /** * Pause the active timer, storing remaining time. */ pause() { if (!this.#running) return; clearTimeout(this.#timer); this.#timer = null; const elapsed = now() - this.#startTime; this.#remaining = Math.max(0, this.#remaining - elapsed); this.#running = false; this.#paused = true; // not finished; still resumable } /** * Resume the timer using the remaining time (or an override delay). * @param {Function} callback - Called when time completes * @param {number|null} [overrideDelay=null] - Optional custom delay in ms */ resume(callback, overrideDelay = null) { if (this.#finished) return; if (this.#running) return; const rawDelay = overrideDelay ?? this.#remaining; // Reject invalid delays (avoid scheduling with NaN/Infinity/etc.) if (!Number.isFinite(rawDelay) || rawDelay < 0) return; // Support 0ms timers (schedule next macrotask) const delay = Math.max(0, rawDelay); // Important: remaining must reflect what we actually scheduled, // otherwise pause() math will be wrong when overrideDelay is used. this.#remaining = delay; this.#startTime = now(); this.#running = true; this.#paused = false; this.#timer = setTimeout(() => { this.#timer = null; this.#running = false; this.#paused = false; this.#finished = true; this.#remaining = 0; callback?.(); }, delay); } /** * Cancel the timer immediately (terminal). */ cancel() { if (this.#timer) clearTimeout(this.#timer); this.#timer = null; this.#running = false; this.#paused = false; this.#finished = true; // leave #remaining as-is or set to 0; prefer 0 for "terminal means done" this.#remaining = 0; } /** * Reset the timer to its original delay, cancelling if needed. * After reset, the timer is not running. */ reset() { if (this.#timer) clearTimeout(this.#timer); this.#timer = null; this.#startTime = null; this.#remaining = this.#ms; this.#running = false; this.#paused = false; this.#finished = false; } }