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);