All files / async / unstable_throttle.ts

100.00% Branches 22/22
100.00% Functions 9/9
100.00% Lines 61/61
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x3
x3
x3
x3
 
x11
x11
 
x11
x11
x11
 
x11
 
x11
x38
x14
x14
x14
x14
x14
x14
x2
x2
x14
x14
x14
x14
x14
x14
x1
x1
x14
x13
x13
x14
x14
x38
x38
x26
x2
x2
x2
x26
x26
x38
x11
 
x11
x1
x1
x11
 
x11
x3
x11
 
x11
x11
x11
x11
x11
x11
 
x11
x11
 
x14
x14
x14










































































































































































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

/** Options for {@linkcode throttle} */
export type ThrottleOptions = {
  /**
   * If `true`, the most recent call will always be executed once the given timeframe elapses with no further calls.
   * Otherwise, the most recent call may not be executed if it was throttled due to a previous call.
   * @default {false}
   */
  ensureLastCall?: boolean;
};

/**
 * 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 `-Infinity` 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 `-Infinity` if it has not been called yet, or reset is called after the last call.
   */
  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 "@std/async/unstable-throttle";
 * 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(func.lastExecution > 0);
 * ```
 *
 * @example With dynamic timeframe
 *
 * ```ts no-assert
 * import { throttle } from "@std/async/unstable-throttle";
 *
 * function processUserInput(input: string) {
 *   // Do some expensive computation with user input that changes on each
 *   // keypress, which takes a variable amount of time depending on the length
 *   // or complexity of input.
 * }
 *
 * const processUserInputThrottled = throttle(
 *   processUserInput,
 *   // Throttle dynamically, waiting twice as long as the previous execution
 *   // took to complete before starting the next call.
 *   (n) => n * 2,
 *   { ensureLastCall: true },
 * );
 * ```
 *
 * @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.
 * If a callback function is supplied, it will be called with the duration of
 * the previous execution and should return the
 * next timeframe to use in milliseconds.
 * @param options Additional options.
 * @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 | ((previousDuration: number) => number),
  options?: ThrottleOptions,
): ThrottledFunction<T> {
  const ensureLast = Boolean(options?.ensureLastCall);
  let timeout = -1;

  let lastExecution = -Infinity;
  let flush: (() => void) | null = null;
  let throttlingAsync = false;

  let tf = typeof timeframe === "function" ? 0 : timeframe;

  const throttled = ((...args: T) => {
    flush = () => {
      const start = Date.now();
      let result: unknown;
      const done = () => {
        throttlingAsync = false;
        lastExecution = Date.now();
        if (typeof timeframe === "function") {
          tf = timeframe(lastExecution - start);
        }
      };
      try {
        clearTimeout(timeout);
        result = fn.call(throttled, ...args);
      } finally {
        if (isPromiseLike(result)) {
          throttlingAsync = true;
          Promise.resolve(result).finally(done);
        } else {
          done();
        }
        flush = null;
      }
    };
    if (throttled.throttling) {
      if (ensureLast) {
        clearTimeout(timeout);
        timeout = setTimeout(() => flush?.(), tf);
      }
      return;
    }
    flush?.();
  }) as ThrottledFunction<T>;

  throttled.clear = () => {
    throttlingAsync = false;
    lastExecution = -Infinity;
  };

  throttled.flush = () => {
    flush?.();
  };

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

  return throttled;
}

function isPromiseLike(obj: unknown): obj is PromiseLike<unknown> {
  return typeof (obj as PromiseLike<unknown>)?.then === "function";
}