import { given } from "@nivinjoseph/n-defensive"; import { Duration } from "./duration.js"; import { Delay } from "./delay.js"; import { ApplicationException } from "@nivinjoseph/n-exception"; import { DecoratorReplacementMethod, DecoratorTargetMethod, MethodDecoratorContext } from "./decorator-helpers.js"; /** * Creates a throttle decorator that limits how often a method can be called. * Ensures a minimum time between executions, dropping calls that occur too frequently. * * @param delay - The minimum time that must pass between executions * @returns A method decorator that implements throttling behavior * @throws ArgumentException if delay is not a positive Duration * * @example * ```typescript * class Example { * @throttle(Duration.fromSeconds(1)) * async handleScroll(): Promise { * // This will execute at most once per second * // Additional calls within the second will be ignored * console.log('Scroll event processed'); * } * } * ``` */ export function throttle< This, Args extends Array, Return extends Promise | void>( delay: Duration ): ThrottleMethodDecorator; export function throttle< This, Args extends Array, Return extends Promise | void >( target: DecoratorTargetMethod, context: MethodDecoratorContext ): DecoratorReplacementMethod; export function throttle< This, Args extends Array, Return extends Promise | void>( delayOrTarget: Duration | DecoratorTargetMethod, context?: MethodDecoratorContext ): ThrottleMethodDecorator | DecoratorReplacementMethod { if (delayOrTarget instanceof Duration) { const delay = delayOrTarget; given(delay, "delay").ensureIsObject().ensureIsInstanceOf(Duration) .ensure(t => t.toMilliSeconds() > 0, "delay should be greater than 0ms"); const decorator: ThrottleMethodDecorator = function (target, context) { return createReplacementMethod(target, context, delay); }; return decorator; } const target = delayOrTarget; if (context == null) throw new ApplicationException("Context should not be null or undefined"); return createReplacementMethod(target, context, null); } function createReplacementMethod< This, Args extends Array, Return extends Promise | void >( target: DecoratorTargetMethod, context: MethodDecoratorContext, delay: Duration | null ): DecoratorReplacementMethod { const { name, kind } = context; given(kind, "kind").ensureHasValue().ensureIsString().ensure(t => t === "method", "throttle decorator can only be used on a method"); const activeKey = Symbol.for(`@nivinjoseph/n-util/throttle/${String(name)}/isActive`); const scheduledCallKey = Symbol.for(`@nivinjoseph/n-util/throttle/${String(name)}/scheduledCall`); context.addInitializer(function (this) { (this as any)[activeKey] = false; (this as any)[scheduledCallKey] = null; }); return async function (this: This, ...args: Args): Promise { (this as any)[scheduledCallKey] = async (): Promise => { await target.call(this, ...args); }; if ((this as any)[activeKey]) return; while ((this as any)[scheduledCallKey] != null && !(this as any)[activeKey]) { (this as any)[activeKey] = true; const currentCall: () => Promise = (this as any)[scheduledCallKey]; (this as any)[scheduledCallKey] = null; try { await currentCall(); } finally { if (delay != null) await Delay.milliseconds(delay.toMilliSeconds()); (this as any)[activeKey] = false; } } }; } export type ThrottleMethodDecorator< This, Args extends Array, Return extends Promise | void > = ( target: DecoratorTargetMethod, context: MethodDecoratorContext ) => DecoratorReplacementMethod;