All files / async / debounce.ts

100.00% Branches 14/14
100.00% Functions 7/7
100.00% Lines 37/37
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x7
x7
x7
x7
 
x16
x4
x4
x12
x12
 
x12
x20
x20
x8
x8
x20
x20
x12
 
x12
x32
x20
x20
x20
x20
x12
 
x12
x10
x12
 
x12
x12
x12
 
x16
x16
x3
x3
x3
 
x11
x16


























































































































// Copyright 2018-2026 the Deno authors. MIT license.
// This module is browser compatible.

/**
 * A debounced function whose execution is delayed by a given `wait`
 * time in milliseconds. If the function is called again before
 * the timeout expires, the previous call will be aborted.
 */
export interface DebouncedFunction<T extends Array<unknown>> {
  (...args: T): void;
  /** Clears the debounce timeout and omits calling the debounced function. */
  clear(): void;
  /** Clears the debounce timeout and calls the debounced function immediately. */
  flush(): void;
  /** Returns a boolean whether a debounce call is pending or not. */
  readonly pending: boolean;
}

/** Options for {@linkcode debounce}. */
export interface DebounceOptions {
  /** An AbortSignal that clears the debounce timeout when aborted. */
  signal?: AbortSignal | undefined;
}

/**
 * Creates a debounced function that delays the given `func`
 * by a given `wait` time in milliseconds. If the method is called
 * again before the timeout expires, the previous call will be
 * aborted.
 *
 * If an {@linkcode AbortSignal} is provided via `options.signal`, aborting the
 * signal clears any pending debounce timeout, equivalent to calling
 * {@linkcode DebouncedFunction.clear}.
 *
 * @example Usage
 * ```ts ignore
 * import { debounce } from "@std/async/debounce";
 *
 * const log = debounce(
 *   (event: Deno.FsEvent) =>
 *     console.log("[%s] %s", event.kind, event.paths[0]),
 *   200,
 * );
 *
 * for await (const event of Deno.watchFs("./")) {
 *   log(event);
 * }
 * // wait 200ms ...
 * // output: [modify] /path/to/file
 * ```
 *
 * @example With AbortSignal
 * ```ts ignore
 * import { debounce } from "@std/async/debounce";
 *
 * const controller = new AbortController();
 * const log = debounce(
 *   (event: Deno.FsEvent) =>
 *     console.log("[%s] %s", event.kind, event.paths[0]),
 *   200,
 *   { signal: controller.signal },
 * );
 *
 * for await (const event of Deno.watchFs("./")) {
 *   log(event);
 * }
 *
 * // Abort clears any pending debounce
 * controller.abort();
 * ```
 *
 * @typeParam T The arguments of the provided function.
 * @param fn The function to debounce.
 * @param wait The time in milliseconds to delay the function.
 * Must be a positive integer.
 * @param options Optional parameters.
 * @throws {RangeError} If `wait` is not a non-negative integer.
 * @returns The debounced function.
 */
// deno-lint-ignore no-explicit-any
export function debounce<T extends Array<any>>(
  fn: (this: DebouncedFunction<T>, ...args: T) => void,
  wait: number,
  options?: DebounceOptions,
): DebouncedFunction<T> {
  if (!Number.isInteger(wait) || wait < 0) {
    throw new RangeError("'wait' must be a positive integer");
  }
  let timeout: number | null = null;
  let pendingFlush: (() => void) | null = null;

  const debounced: DebouncedFunction<T> = ((...args: T) => {
    debounced.clear();
    pendingFlush = () => {
      debounced.clear();
      fn.call(debounced, ...args);
    };
    timeout = Number(setTimeout(pendingFlush, wait));
  }) as DebouncedFunction<T>;

  debounced.clear = () => {
    if (timeout !== null) {
      clearTimeout(timeout);
      timeout = null;
      pendingFlush = null;
    }
  };

  debounced.flush = () => {
    pendingFlush?.();
  };

  Object.defineProperty(debounced, "pending", {
    get: () => timeout !== null,
  });

  const signal = options?.signal;
  if (signal) {
    signal.throwIfAborted();
    signal.addEventListener("abort", () => debounced.clear(), { once: true });
  }

  return debounced;
}