Skip to content

Instantly share code, notes, and snippets.

@OysteinAmundsen
Created April 7, 2022 09:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save OysteinAmundsen/6fad0b3d9092da10a4d3ff4a2b3afc48 to your computer and use it in GitHub Desktop.
Save OysteinAmundsen/6fad0b3d9092da10a4d3ff4a2b3afc48 to your computer and use it in GitHub Desktop.
Debounce decorator
/**
* Debounce function decorator
*
* @param delay
* @param immediate
* @returns the function debounced
*/
export function Debouncer(delay = 200, immediate = false): MethodDecorator {
return function (target: Record<string, unknown>, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
const map = new WeakMap();
const originalMethod = descriptor.value;
descriptor.value = function (...params: any[]) {
let method = map.get(this);
if (!method) {
method = debounce(originalMethod, delay, immediate);
map.set(this, method);
}
const resultPromise = method.result();
const debounced = method.bind(this);
debounced(...params);
return resultPromise;
};
return descriptor;
} as MethodDecorator;
}
/**
* Slightly modified version of lodash.debounce: https://github.com/lodash/lodash/blob/master/debounce.js
*
* This version returns a shared promise to all callers of the function, which will resolve all calls
* with the functions result when it is received. Then it will reset, so that the next calls will not be
* polluted with the previous result.
*
*/
export function debounce<T extends (...args: any[]) => any>(func: T, wait = 200, options?: any) {
let lastArgs: any;
let lastThis: any;
let maxWait: number | undefined;
let result: T;
let resolver: (value: T | PromiseLike<T>) => void;
let rejector: (reaons?: any) => void;
let resultPromise = createResultPromise();
let timerId: number | undefined;
let lastCallTime: number | undefined;
let lastInvokeTime = 0;
let leading = false;
let maxing = false;
let trailing = true;
// Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
const useRAF = !wait && wait !== 0 && typeof window.requestAnimationFrame === 'function';
if (typeof func !== 'function') throw new TypeError('Expected a function');
wait = +wait || 0;
if (isObject(options)) {
leading = !!options.leading;
maxing = 'maxWait' in options;
maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}
/**
* Called when all calls are in, and it is time to actually invoke the debounced function.
*
* @param time
* @returns
*/
function invokeFunc(time: number): T {
const args = lastArgs;
const thisArg = lastThis;
lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
resolver(result); // Resolve the promise to all listeners
setTimeout(() => (resultPromise = createResultPromise())); // Reset the result promise for next run
return result;
}
function startTimer(pendingFunc: FrameRequestCallback, wait: number): number {
if (useRAF && timerId) {
window.cancelAnimationFrame(timerId);
return window.requestAnimationFrame(pendingFunc);
}
return setTimeout(pendingFunc, wait);
}
function cancelTimer(id: number): void {
if (useRAF) {
return window.cancelAnimationFrame(id);
}
clearTimeout(id);
}
function leadingEdge(time: number): T {
// Reset any `maxWait` timer.
lastInvokeTime = time;
// Start the timer for the trailing edge.
timerId = startTimer(timerExpired, wait);
// Invoke the leading edge.
return leading ? invokeFunc(time) : result;
}
function remainingWait(time: number): number {
const timeSinceLastCall = time - (lastCallTime || 0);
const timeSinceLastInvoke = time - lastInvokeTime;
const timeWaiting = wait - timeSinceLastCall;
return maxing ? Math.min(timeWaiting, (maxWait || 0) - timeSinceLastInvoke) : timeWaiting;
}
function shouldInvoke(time: number): boolean {
const timeSinceLastCall = time - (lastCallTime || 0);
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 ||
(maxing && timeSinceLastInvoke >= (maxWait || 0))
);
}
function timerExpired(): T | undefined {
const time = Date.now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
// Restart the timer.
timerId = startTimer(timerExpired, remainingWait(time));
return;
}
function trailingEdge(time: number): T {
timerId = undefined;
// Only invoke if we have `lastArgs` which means `func` has been
// debounced at least once.
if (trailing && lastArgs) {
return invokeFunc(time);
}
lastArgs = lastThis = undefined;
return result;
}
function createResultPromise() {
return new Promise<T>((resolve, reject) => {
resolver = resolve;
rejector = reject;
});
}
function debounced(...args: any[]): T {
const time = Date.now();
const isInvoking = shouldInvoke(time);
lastArgs = args;
lastThis = this;
lastCallTime = time;
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime);
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = startTimer(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
if (timerId === undefined) {
timerId = startTimer(timerExpired, wait);
}
return result;
}
debounced.cancel = function cancel(): void {
if (timerId !== undefined) {
cancelTimer(timerId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timerId = undefined;
};
debounced.flush = function flush(): T {
return timerId === undefined ? result : trailingEdge(Date.now());
};
debounced.pending = function pending(): boolean {
return timerId !== undefined;
};
debounced.result = function () {
return resultPromise;
};
return debounced;
}
function isObject(value: any): boolean {
const type = typeof value;
return value != null && (type === 'object' || type === 'function');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment