UNPKG

timeout-flow

Version:

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

142 lines (111 loc) 3.32 kB
// ./src/throttle.js import { resolveDelayFnOptions } from './resolveDelayAndFn.js'; import { pendingAbort } from './abort.js'; import { now } from './now.js' /** * @typedef {((...args: any[]) => void) & { * cancel: () => void, * flush: () => void * }} ThrottledFunction */ /** * Creates a throttled version of a function. * Executes immediately, throttles further calls, optional trailing execution. * * Preferred: * - throttle(fn, delay, [options]) * * Also supported: * - throttle(delay, fn, [options]) // legacy/alt ordering * * Notes: * - Uses performance.now() (via `now()`) for elapsed timing. * - If trailing is enabled, uses the latest arguments/context seen during the wait. * - `.cancel()` clears any pending trailing invocation. * - `.flush()` immediately invokes a pending trailing call (if scheduled). * - Optional AbortSignal: abort cancels any pending trailing invocation. * * @param {Function|string|number} a * @param {Function|string|number|Object} b * @param {Object} [c] * @param {boolean} [c.trailing=true] * @param {AbortSignal} [c.signal] * @returns {ThrottledFunction} */ export function throttle(a, b, c) { // Require an explicit delay by passing `undefined` as defaultDelay. const { fn, delay, options } = resolveDelayFnOptions(a, b, c, undefined); const signal = options?.signal; const trailing = options?.trailing ?? true; const waitWindow = Math.max(0, delay); let lastCallTime = -Infinity; let timeoutId = 0; /** @type {any[] | null} */ let lastArgs = null; /** @type {any | null} */ let lastThis = null; const abort = pendingAbort(signal, () => cancel()); const invoke = () => { if (!lastArgs) return; const args = lastArgs; const ctx = lastThis; // clear first (re-entrancy safe + GC friendly) lastArgs = null; lastThis = null; abort.remove(); fn.apply(ctx, args); }; const cancel = () => { if (timeoutId) clearTimeout(timeoutId); timeoutId = 0; lastArgs = null; lastThis = null; abort.remove(); }; /** @param {number} wait */ const scheduleTrailing = (wait) => { abort.add(); timeoutId = setTimeout(() => { timeoutId = 0; lastCallTime = now(); invoke(); }, wait); }; /** @type {any} */ const throttled = function (...args) { if (signal?.aborted) return; const currentTime = now(); const timeSinceLast = currentTime - lastCallTime; lastArgs = args; lastThis = this; if (timeSinceLast >= waitWindow) { // Leading edge: run now lastCallTime = currentTime; // Clear any pending trailing call (and listener) if (timeoutId) { clearTimeout(timeoutId); timeoutId = 0; abort.remove(); } invoke(); return; } if (!trailing) return; // Trailing edge: schedule once, keep latest args if (!timeoutId) { const wait = Math.max(0, waitWindow - timeSinceLast); scheduleTrailing(wait); } }; throttled.cancel = cancel; throttled.flush = () => { if (!timeoutId) return; clearTimeout(timeoutId); timeoutId = 0; lastCallTime = now(); invoke(); }; // If already aborted at creation time, ensure clean slate. if (signal?.aborted) cancel(); return throttled; }