All files / async / unstable_throttle.ts

100.00% Branches 3/3
100.00% Lines 33/33
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x2
x2
x2
 
x7
x7
 
x7
x31
x38
x38
x38
x38
x38
x38
x31
x31
x49
x49
x31
x7
 
x7
x10
x7
 
x7
x9
x9
x9
x7
 
x7
x21
x21
x7
 
x7
x7









































































































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

/**
 * A throttled function that will be executed at most once during the
 * specified `timeframe` in milliseconds.
 */
export interface ThrottledFunction<T extends Array<unknown>> {
  (...args: T): void;
  /**
   * Clears the throttling state.
   * {@linkcode ThrottledFunction.lastExecution} will be reset to `NaN` and
   * {@linkcode ThrottledFunction.throttling} will be reset to `false`.
   */
  clear(): void;
  /**
   * Execute the last throttled call (if any) and clears the throttling state.
   */
  flush(): void;
  /**
   * Returns a boolean indicating whether the function is currently being throttled.
   */
  readonly throttling: boolean;
  /**
   * Returns the timestamp of the last execution of the throttled function.
   * It is set to `NaN` if it has not been called yet.
   */
  readonly lastExecution: number;
}

/**
 * Creates a throttled function that prevents the given `func`
 * from being called more than once within a given `timeframe` in milliseconds.
 *
 * @experimental **UNSTABLE**: New API, yet to be vetted.
 *
 * @example Usage
 * ```ts
 * import { throttle } from "./unstable_throttle.ts"
 * import { retry } from "@std/async/retry"
 * import { assert } from "@std/assert"
 *
 * let called = 0;
 * const requestReceived = Promise.withResolvers<void>();
 * await using server = Deno.serve({ port: 0 }, () => {
 *   requestReceived.resolve();
 *   return new Response(`${called++}`)
 * });
 *
 * // A throttled function will be executed at most once during a specified ms timeframe
 * const timeframe = 100
 * const func = throttle<[string]>((url) => fetch(url).then(r => r.body?.cancel()), timeframe);
 * for (let i = 0; i < 10; i++) {
 *   func(`http://localhost:${server.addr.port}/api`);
 * }
 *
 * await retry(() => assert(!func.throttling))
 * await requestReceived.promise;
 * assert(called === 1)
 * assert(!Number.isNaN(func.lastExecution))
 * ```
 *
 * @typeParam T The arguments of the provided function.
 * @param fn The function to throttle.
 * @param timeframe The timeframe in milliseconds in which the function should be called at most once.
 * @returns The throttled function.
 */
// deno-lint-ignore no-explicit-any
export function throttle<T extends Array<any>>(
  fn: (this: ThrottledFunction<T>, ...args: T) => void,
  timeframe: number,
): ThrottledFunction<T> {
  let lastExecution = NaN;
  let flush: (() => void) | null = null;

  const throttled = ((...args: T) => {
    flush = () => {
      try {
        fn.call(throttled, ...args);
      } finally {
        lastExecution = Date.now();
        flush = null;
      }
    };
    if (throttled.throttling) {
      return;
    }
    flush?.();
  }) as ThrottledFunction<T>;

  throttled.clear = () => {
    lastExecution = NaN;
  };

  throttled.flush = () => {
    lastExecution = NaN;
    flush?.();
    throttled.clear();
  };

  Object.defineProperties(throttled, {
    throttling: { get: () => Date.now() - lastExecution <= timeframe },
    lastExecution: { get: () => lastExecution },
  });

  return throttled;
}