pankod/refine

View on GitHub
packages/core/src/definitions/helpers/async-debounce/index.ts

Summary

Maintainability
A
1 hr
Test Coverage
import debounce from "lodash/debounce";

// biome-ignore lint/suspicious/noExplicitAny: any is used to accept any function
type Callbacks<T extends (...args: any) => any> = {
  resolve?: (value: Awaited<ReturnType<T>>) => void;
  // biome-ignore lint/suspicious/noExplicitAny: any is used to match the type of Promise.reject
  reject?: (reason?: any) => void;
};

// biome-ignore lint/suspicious/noExplicitAny: any is used to accept any function
type DebouncedFunction<T extends (...args: any) => any> = {
  (...args: Parameters<T>): Promise<Awaited<ReturnType<T>>>;
  flush: () => void;
  cancel: () => void;
};

/**
 * Debounces sync and async functions with given wait time. The debounced function returns a promise which can be awaited or catched.
 * Only the last call of the debounced function will resolve or reject.
 * Previous calls will be rejected with the given cancelReason.
 *
 * The original debounce function doesn't work well with async functions,
 * It won't return a promise to resolve/reject and therefore it's not possible to await the result.
 * This will always return a promise to handle and await the result.
 * Previous calls will be rejected immediately after a new call made.
 */
// biome-ignore lint/suspicious/noExplicitAny: any is used to accept any function
export const asyncDebounce = <T extends (...args: any[]) => any>(
  func: T,
  wait = 1000,
  cancelReason?: string,
): DebouncedFunction<T> => {
  let callbacks: Array<Callbacks<T>> = [];

  const cancelPrevious = () => {
    callbacks.forEach((cb) => cb.reject?.(cancelReason));
    callbacks = [];
  };

  const debouncedFunc = debounce((...args: Parameters<T>) => {
    const { resolve, reject } = callbacks.pop() || {};
    Promise.resolve(func(...args))
      .then(resolve)
      .catch(reject);
  }, wait);

  const runner = (...args: Parameters<T>) => {
    return new Promise<Awaited<ReturnType<T>>>((resolve, reject) => {
      cancelPrevious();

      callbacks.push({
        resolve,
        reject,
      });

      debouncedFunc(...args);
    });
  };

  runner.flush = () => debouncedFunc.flush();
  runner.cancel = () => {
    debouncedFunc.cancel();
    cancelPrevious();
  };

  return runner;
};