File size: 6,966 Bytes
1dbc34b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 | /**
* Debounce and throttle utilities for rate-limiting function calls
*/
/**
* Options for the debounce function
*/
export interface DebounceOptions {
/**
* If true, call the function immediately on the first invocation (leading edge)
* @default false
*/
leading?: boolean;
/**
* If true, call the function after the delay on the last invocation (trailing edge)
* @default true
*/
trailing?: boolean;
/**
* Maximum time to wait before forcing invocation (useful for continuous events)
* If set, the function will be called at most every `maxWait` milliseconds
*/
maxWait?: number;
}
/**
* The return type of the debounce function with additional control methods
*/
export interface DebouncedFunction<T extends (...args: unknown[]) => unknown> {
/**
* Call the debounced function
*/
(...args: Parameters<T>): void;
/**
* Cancel any pending invocation
*/
cancel(): void;
/**
* Immediately invoke any pending function call
*/
flush(): void;
/**
* Check if there's a pending invocation
*/
pending(): boolean;
}
/**
* Creates a debounced version of a function that delays invoking the function
* until after `wait` milliseconds have elapsed since the last time the debounced
* function was invoked.
*
* Useful for rate-limiting events like window resize, scroll, or input changes.
*
* @param fn - The function to debounce
* @param wait - The number of milliseconds to delay
* @param options - Optional configuration
* @returns A debounced version of the function with cancel, flush, and pending methods
*
* @example
* // Basic usage - save input after user stops typing for 300ms
* const saveInput = debounce((value: string) => {
* api.save(value);
* }, 300);
*
* input.addEventListener('input', (e) => saveInput(e.target.value));
*
* @example
* // With leading edge - execute immediately on first call
* const handleClick = debounce(() => {
* submitForm();
* }, 1000, { leading: true, trailing: false });
*
* @example
* // With maxWait - ensure function runs at least every 5 seconds during continuous input
* const autoSave = debounce((content: string) => {
* saveToServer(content);
* }, 1000, { maxWait: 5000 });
*/
export function debounce<T extends (...args: unknown[]) => unknown>(
fn: T,
wait: number,
options: DebounceOptions = {}
): DebouncedFunction<T> {
const { leading = false, trailing = true, maxWait } = options;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
let maxTimeoutId: ReturnType<typeof setTimeout> | null = null;
let lastArgs: Parameters<T> | null = null;
let lastCallTime: number | null = null;
let lastInvokeTime = 0;
// Validate options
if (maxWait !== undefined && maxWait < wait) {
throw new Error('maxWait must be greater than or equal to wait');
}
function invokeFunc(): void {
const args = lastArgs;
lastArgs = null;
lastInvokeTime = Date.now();
if (args !== null) {
fn(...args);
}
}
function shouldInvoke(time: number): boolean {
const timeSinceLastCall = lastCallTime === null ? 0 : time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
// First call, or wait time has passed, or maxWait exceeded
return (
lastCallTime === null ||
timeSinceLastCall >= wait ||
timeSinceLastCall < 0 ||
(maxWait !== undefined && timeSinceLastInvoke >= maxWait)
);
}
function timerExpired(): void {
const time = Date.now();
if (shouldInvoke(time)) {
trailingEdge();
return;
}
// Restart the timer with remaining time
const timeSinceLastCall = lastCallTime === null ? 0 : time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
const timeWaiting = wait - timeSinceLastCall;
const remainingWait =
maxWait !== undefined ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting;
timeoutId = setTimeout(timerExpired, remainingWait);
}
function trailingEdge(): void {
timeoutId = null;
if (trailing && lastArgs !== null) {
invokeFunc();
}
lastArgs = null;
}
function leadingEdge(time: number): void {
lastInvokeTime = time;
// Start timer for trailing edge
timeoutId = setTimeout(timerExpired, wait);
// Invoke leading edge
if (leading) {
invokeFunc();
}
}
function cancel(): void {
if (timeoutId !== null) {
clearTimeout(timeoutId);
timeoutId = null;
}
if (maxTimeoutId !== null) {
clearTimeout(maxTimeoutId);
maxTimeoutId = null;
}
lastArgs = null;
lastCallTime = null;
lastInvokeTime = 0;
}
function flush(): void {
if (timeoutId !== null) {
invokeFunc();
cancel();
}
}
function pending(): boolean {
return timeoutId !== null;
}
function debounced(...args: Parameters<T>): void {
const time = Date.now();
const isInvoking = shouldInvoke(time);
lastArgs = args;
lastCallTime = time;
if (isInvoking) {
if (timeoutId === null) {
leadingEdge(time);
return;
}
// Handle maxWait case
if (maxWait !== undefined) {
timeoutId = setTimeout(timerExpired, wait);
invokeFunc();
return;
}
}
if (timeoutId === null) {
timeoutId = setTimeout(timerExpired, wait);
}
}
debounced.cancel = cancel;
debounced.flush = flush;
debounced.pending = pending;
return debounced;
}
/**
* Options for the throttle function
*/
export interface ThrottleOptions {
/**
* If true, call the function on the leading edge
* @default true
*/
leading?: boolean;
/**
* If true, call the function on the trailing edge
* @default true
*/
trailing?: boolean;
}
/**
* Creates a throttled version of a function that only invokes the function
* at most once per every `wait` milliseconds.
*
* Useful for rate-limiting events like scroll or mousemove where you want
* regular updates but not on every event.
*
* @param fn - The function to throttle
* @param wait - The number of milliseconds to throttle invocations to
* @param options - Optional configuration
* @returns A throttled version of the function with cancel, flush, and pending methods
*
* @example
* // Throttle scroll handler to run at most every 100ms
* const handleScroll = throttle(() => {
* updateScrollPosition();
* }, 100);
*
* window.addEventListener('scroll', handleScroll);
*
* @example
* // Throttle with leading edge only (no trailing call)
* const submitOnce = throttle(() => {
* submitForm();
* }, 1000, { trailing: false });
*/
export function throttle<T extends (...args: unknown[]) => unknown>(
fn: T,
wait: number,
options: ThrottleOptions = {}
): DebouncedFunction<T> {
const { leading = true, trailing = true } = options;
return debounce(fn, wait, {
leading,
trailing,
maxWait: wait,
});
}
|