/**
* lodash (Custom Build)
* Build: `lodash modularize exports="npm" -o ./`
* Copyright jQuery Foundation and other contributors
* Released under MIT license
* Based on Underscore.js 1.8.3
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
export interface DebounceSettings {
/**
* The number of milliseconds to delay.
* @default 0
*/
wait?: number;
/**
* The maximum time `func` is allowed to be delayed before it's invoked
* @default null
*/
maxWait?: number | null;
}
export interface DebouncedFunc {
/**
* Call the original function, but applying the debounce rules.
*
* If the debounced function can be run immediately, this calls it and returns its return
* value.
*
* Otherwise, it returns the return value of the last invokation, or undefined if the debounced
* function was not invoked yet.
*/
(...args: FnArgumentsType): FnReturnType | undefined;
/**
* Throw away any pending invokation of the debounced function.
*/
cancel(): void;
/**
* If there is a pending invokation of the debounced function, invoke it immediately and return
* its return value.
*
* Otherwise, return the value from the last invokation, or undefined if the debounced function
* was never invoked.
*/
flush(): FnReturnType | undefined;
}
/**
* Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since
* the last time the debounced function was invoked. The debounced function comes with a cancel method to
* cancel delayed invocations and a flush method to immediately invoke them. Provide an options object to
* indicate that func should be invoked on the leading and/or trailing edge of the wait timeout. Subsequent
* calls to the debounced function return the result of the last func invocation.
*
* Note: If leading and trailing options are true, func is invoked on the trailing edge of the timeout only
* if the the debounced function is invoked more than once during the wait timeout.
*
* See David Corbacho’s article for details over the differences between _.debounce and _.throttle.
*
* @param func The function to debounce.
* @param wait The number of milliseconds to delay.
* @param options The options object.
* @return Returns the new debounced function.
*/
export function debounce(
func: (...args: FnArgumentsType) => FnReturnType,
options: DebounceSettings = {}
): DebouncedFunc {
let lastArgs: FnArgumentsType | undefined;
let result: FnReturnType | undefined;
let timerId: NodeJS.Timeout | undefined;
let lastCallTime: number | undefined;
let lastInvokeTime = 0;
const wait = options.wait ?? 0;
const maxWait = typeof options.maxWait === 'number' ? Math.max(options.maxWait, wait) : null;
function invokeFunc(time: number) {
const args = lastArgs;
lastArgs = undefined;
lastInvokeTime = time;
result = func(...args!);
return result;
}
function leadingEdge(time: number) {
// Reset any `maxWait` timer.
lastInvokeTime = time;
// Start the timer for the trailing edge.
timerId = setTimeout(timerExpired, wait);
// Invoke the leading edge.
return result;
}
function remainingWait(time: number) {
const timeSinceLastCall = time - lastCallTime!;
const timeSinceLastInvoke = time - lastInvokeTime;
const result = wait - timeSinceLastCall;
return maxWait === null ? result : Math.min(result, maxWait - timeSinceLastInvoke);
}
function shouldInvoke(time: number) {
const timeSinceLastCall = time - lastCallTime!;
const timeSinceLastInvoke = time - lastInvokeTime;
// Either this is the first call, activity has stopped and we're at the
// trailing edge, the system time has gone backwards and we're treating
// it as the trailing edge, or we've hit the `maxWait` limit.
return (
lastCallTime === undefined || //
timeSinceLastCall >= wait ||
timeSinceLastCall < 0 ||
(maxWait !== null && timeSinceLastInvoke >= maxWait)
);
}
function timerExpired() {
const time = Date.now();
if (shouldInvoke(time)) {
trailingEdge(time);
return;
}
// Restart the timer.
timerId = setTimeout(timerExpired, remainingWait(time));
}
function trailingEdge(time: number) {
timerId = undefined;
return invokeFunc(time);
}
function cancel() {
if (timerId !== undefined) {
clearTimeout(timerId);
}
lastInvokeTime = 0;
lastArgs = undefined;
lastCallTime = undefined;
timerId = undefined;
}
function flush() {
return timerId === undefined ? result : trailingEdge(Date.now());
}
function debounced(...args: FnArgumentsType) {
const time = Date.now();
const isInvoking = shouldInvoke(time);
lastArgs = args;
lastCallTime = time;
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime);
}
if (maxWait !== null) {
// Handle invocations in a tight loop.
timerId = setTimeout(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
if (timerId === undefined) {
timerId = setTimeout(timerExpired, wait);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}