/** * Function-focused utility helpers. * * @module bquery/core/utils/function */ /** A debounced function with a cancel method to clear the pending timeout. */ export interface DebouncedFn { (...args: TArgs): void; /** Cancels the pending debounced invocation. */ cancel(): void; } /** A throttled function with a cancel method to reset the throttle timer. */ export interface ThrottledFn { (...args: TArgs): void; /** Resets the throttle timer, allowing the next call to execute immediately. */ cancel(): void; } /** * Creates a debounced function that delays execution until after * the specified delay has elapsed since the last call. * * @template TArgs - The argument types of the function * @param fn - The function to debounce * @param delayMs - Delay in milliseconds * @returns A debounced version of the function with a `cancel()` method * * @example * ```ts * const search = debounce((query: string) => { * console.log('Searching:', query); * }, 300); * * search('h'); * search('he'); * search('hello'); // Only this call executes after 300ms * * search('cancel me'); * search.cancel(); // Cancels the pending invocation * ``` */ export function debounce( fn: (...args: TArgs) => void, delayMs: number ): DebouncedFn { let timeoutId: ReturnType | undefined; const debounced: DebouncedFn = Object.assign( (...args: TArgs) => { if (timeoutId !== undefined) { clearTimeout(timeoutId); } timeoutId = setTimeout(() => { timeoutId = undefined; fn(...args); }, delayMs); }, { cancel: () => { if (timeoutId !== undefined) { clearTimeout(timeoutId); timeoutId = undefined; } }, } ); return debounced; } /** * Creates a throttled function that runs at most once per interval. * * @template TArgs - The argument types of the function * @param fn - The function to throttle * @param intervalMs - Minimum interval between calls in milliseconds * @returns A throttled version of the function with a `cancel()` method * * @example * ```ts * const handleScroll = throttle(() => { * console.log('Scroll position:', window.scrollY); * }, 100); * * window.addEventListener('scroll', handleScroll); * * handleScroll.cancel(); // Resets throttle, next call executes immediately * ``` */ export function throttle( fn: (...args: TArgs) => void, intervalMs: number ): ThrottledFn { let lastRun = 0; const throttled: ThrottledFn = Object.assign( (...args: TArgs) => { const now = Date.now(); if (now - lastRun >= intervalMs) { lastRun = now; fn(...args); } }, { cancel: () => { lastRun = 0; }, } ); return throttled; } /** * Ensures a function only runs once. Subsequent calls return the first result. * * @template TArgs - The argument types of the function * @template TResult - The return type of the function * @param fn - The function to wrap * @returns A function that only runs once * * @example * ```ts * const init = once(() => ({ ready: true })); * init(); * init(); // only runs once * ``` */ export function once( fn: (...args: TArgs) => TResult ): (...args: TArgs) => TResult { let hasRun = false; let result!: TResult; return (...args: TArgs) => { if (!hasRun) { result = fn(...args); hasRun = true; } return result; }; } /** * A no-operation function. * * @example * ```ts * noop(); * ``` */ export function noop(): void { // Intentionally empty }