UNPKG

p-throttle

Version:

Throttle promise-returning & async functions

117 lines (90 loc) 2.66 kB
const registry = new FinalizationRegistry(({signal, aborted}) => { signal?.removeEventListener('abort', aborted); }); export default function pThrottle({limit, interval, strict, signal, onDelay}) { if (!Number.isFinite(limit)) { throw new TypeError('Expected `limit` to be a finite number'); } if (!Number.isFinite(interval)) { throw new TypeError('Expected `interval` to be a finite number'); } const queue = new Map(); let currentTick = 0; let activeCount = 0; function windowedDelay() { const now = Date.now(); if ((now - currentTick) > interval) { activeCount = 1; currentTick = now; return 0; } if (activeCount < limit) { activeCount++; } else { currentTick += interval; activeCount = 1; } return currentTick - now; } const strictTicks = []; function strictDelay() { const now = Date.now(); // Clear the queue if there's a significant delay since the last execution if (strictTicks.length > 0 && now - strictTicks.at(-1) > interval) { strictTicks.length = 0; } // If the queue is not full, add the current time and execute immediately if (strictTicks.length < limit) { strictTicks.push(now); return 0; } // Calculate the next execution time based on the first item in the queue const nextExecutionTime = strictTicks[0] + interval; // Shift the queue and add the new execution time strictTicks.shift(); strictTicks.push(nextExecutionTime); // Calculate the delay for the current execution return Math.max(0, nextExecutionTime - now); } const getDelay = strict ? strictDelay : windowedDelay; return function_ => { const throttled = function (...arguments_) { if (!throttled.isEnabled) { return (async () => function_.apply(this, arguments_))(); } let timeoutId; return new Promise((resolve, reject) => { const execute = () => { resolve(function_.apply(this, arguments_)); queue.delete(timeoutId); }; const delay = getDelay(); if (delay > 0) { timeoutId = setTimeout(execute, delay); queue.set(timeoutId, reject); onDelay?.(...arguments_); } else { execute(); } }); }; const aborted = () => { for (const timeout of queue.keys()) { clearTimeout(timeout); queue.get(timeout)(signal.reason); } queue.clear(); strictTicks.splice(0, strictTicks.length); }; registry.register(throttled, {signal, aborted}); signal?.throwIfAborted(); signal?.addEventListener('abort', aborted, {once: true}); throttled.isEnabled = true; Object.defineProperty(throttled, 'queueSize', { get() { return queue.size; }, }); return throttled; }; }