import { ClassMethodDecorator } from "./decorators/types"; export interface AsyncLimiterState { readonly key: string; working: boolean; nextCallParams: any; nextCallTimer: any; last_call: number initial_call: number last_exec: number } export interface LimiterFunction { (state: AsyncLimiterState): number; } interface GenericLimiterOptions

{ key_on?: number[] | ((params: P) => string); /** If using NodeJS, whether `unref()` should be called on any timers. Basically, if set to `true`, NodeJS will be allowed to exit without calling the function */ unref?: boolean; limiter_logic: (options: any) => LimiterFunction; } type LimitableFunction = (...params: any[]) => void export const async_limiter = (options: GenericLimiterOptions, func: T) => { const limiter = options.limiter_logic(options); const states: Record = {}; function getState(params: Parameters) { let key = "DEFAULT" if (typeof options.key_on == 'function') { key = options.key_on(params); } else if (options.key_on) { key = options.key_on.map(i => params[i]).join('|'); } return states[key] ||= { key, working: false, nextCallParams: null, nextCallTimer: null, initial_call: 0, last_call: 0, last_exec: 0, } } function setTimer(state: AsyncLimiterState, time: number) { if (state.nextCallTimer) clearTimeout(state.nextCallTimer); state.nextCallTimer = setTimeout(() => { state.nextCallTimer = null; workOrScheduleTask(state); }, time - Date.now()); if (options.unref) state.nextCallTimer.unref(); } async function workOrScheduleTask(state: AsyncLimiterState) { if (state.working) return; // The timer ticked but no params are set - let's cleanup if (!state.nextCallParams) { if (state.nextCallTimer) clearTimeout(state.nextCallTimer); delete states[state.key]; return }; let nextCallTime = limiter(state); if (nextCallTime < Date.now()) { state.working = true; state.initial_call = null; state.last_exec = Date.now(); const p = state.nextCallParams; state.nextCallParams = null; try { await func.call(p.self, ...p.parameters); } finally { state.working = false; const cleanupTime = limiter(state); if (cleanupTime <= Date.now()) { workOrScheduleTask(state); } else { setTimer(state, cleanupTime); } } } else { setTimer(state, nextCallTime); } } function limited(...args: Parameters) { const state = getState(args); state.last_call = Date.now(); state.initial_call ||= state.last_call; state.nextCallParams = { self: this, parameters: args }; workOrScheduleTask(state); } limited.cancel_pending = function (...args: Parameters) { const state = getState(args); if (state) { state.nextCallParams = null; if (state.nextCallTimer) clearTimeout(state.nextCallTimer); workOrScheduleTask(state); // Handles additional cleanup } } return limited } export function limiter_decorator_combo LimiterFunction>(limiter: T) { type LimitOptions = Parameters[0]; function limit

(options: LimitOptions & Omit, "limiter_logic">): ClassMethodDecorator any> function limit

void>(options: LimitOptions & Omit, "limiter_logic">, func: T): ReturnType> function limit(options, func?) { const resolvedOptions: GenericLimiterOptions = { ...options, limiter_logic: limit.limiting_logic, } if (func) { return async_limiter(resolvedOptions, func); } else { return (func, context: ClassMethodDecoratorContext any>) => { const limited = async_limiter(resolvedOptions, func); return function (...args) { return limited.call(this, ...args); } } } } limit.limiting_logic = limiter; return limit; } const throttle_limiter = ({ interval }: { interval: number }): LimiterFunction => ({ last_exec }) => (last_exec || 0) + interval; /** First call goes through, next call waits until `interval` after the first call */ export const throttle = limiter_decorator_combo(throttle_limiter); const debounce_limiter = ({ timeToStability, maxTime = Infinity }: { timeToStability: number, maxTime?: number }): LimiterFunction => ({ initial_call, last_call }) => Math.min(last_call + timeToStability, initial_call + maxTime); /** All calls are delayed until no call has been received for `timeToStability` (or until `maxTime` after the first call) */ export const debounce = limiter_decorator_combo(debounce_limiter); const combined_limiter = ({ interval, timeToStability, maxTime = Infinity }: { interval: number, timeToStability: number, maxTime?: number }): LimiterFunction => ({ initial_call, last_call, last_exec }) => { maxTime ||= Infinity; const debounceEndStamp = last_call + (timeToStability || 0); const debounceMaxStamp = (last_exec ?? initial_call) + maxTime; const nextThrottleStamp = (last_exec || 0) + interval; return Math.max(nextThrottleStamp, Math.min(debounceEndStamp, debounceMaxStamp)); } /** Debounce _then_ throttle - `debounce(throttle(X))` */ export const limit = limiter_decorator_combo(combined_limiter);