1
0

Debouncer.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. /* eslint-disable @typescript-eslint/no-explicit-any */
  2. /**
  3. * @module lib/Debouncer
  4. * This module contains the Debouncer class and debounce function that allow you to reduce the amount of calls in rapidly firing event listeners and such - [see the documentation for more info](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#debouncer)
  5. */
  6. import { NanoEmitter } from "./NanoEmitter.js";
  7. //#region types
  8. /**
  9. * The type of edge to use for the debouncer - [see the docs for a diagram and explanation.](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#debouncer)
  10. * - `immediate` - (default & recommended) - calls the listeners at the very first call ("rising" edge) and queues the latest call until the timeout expires
  11. * - Pros:
  12. * - First call is let through immediately
  13. * - Cons:
  14. * - After all calls stop, the JS engine's event loop will continue to run until the last timeout expires (doesn't really matter on the web, but could cause a process exit delay in Node.js)
  15. * - `idle` - queues all calls until there are no more calls in the given timeout duration ("falling" edge), and only then executes the very last call
  16. * - Pros:
  17. * - Makes sure there are zero calls in the given `timeoutDuration` before executing the last call
  18. * - Cons:
  19. * - Calls are always delayed by at least `1 * timeoutDuration`
  20. * - Calls could get stuck in the queue indefinitely if there is no downtime between calls that is greater than the `timeoutDuration`
  21. */
  22. export type DebouncerType = "immediate" | "idle";
  23. type AnyFunc = (...args: any) => any;
  24. /** The debounced function type that is returned by the {@linkcode debounce} function */
  25. export type DebouncedFunction<TFunc extends AnyFunc> = ((...args: Parameters<TFunc>) => ReturnType<TFunc>) & { debouncer: Debouncer<TFunc> };
  26. /** Event map for the {@linkcode Debouncer} */
  27. export type DebouncerEventMap<TFunc extends AnyFunc> = {
  28. /** Emitted when the debouncer calls all registered listeners, as a pub-sub alternative */
  29. call: TFunc;
  30. /** Emitted when the timeout or edge type is changed after the instance was created */
  31. change: (timeout: number, type: DebouncerType) => void;
  32. };
  33. //#region debounce class
  34. /**
  35. * A debouncer that calls all listeners after a specified timeout, discarding all calls in-between.
  36. * It is very useful for event listeners that fire quickly, like `input` or `mousemove`, to prevent the listeners from being called too often and hogging resources.
  37. * The exact behavior can be customized with the `type` parameter.
  38. *
  39. * The instance inherits from {@linkcode NanoEmitter} and emits the following events:
  40. * - `call` - emitted when the debouncer calls all listeners - use this as a pub-sub alternative to the default callback-style listeners
  41. * - `change` - emitted when the timeout or edge type is changed after the instance was created
  42. */
  43. export class Debouncer<TFunc extends AnyFunc> extends NanoEmitter<DebouncerEventMap<TFunc>> {
  44. /** All registered listener functions and the time they were attached */
  45. protected listeners: TFunc[] = [];
  46. /** The currently active timeout */
  47. protected activeTimeout: ReturnType<typeof setTimeout> | undefined;
  48. /** The latest queued call */
  49. protected queuedCall: (() => void) | undefined;
  50. /**
  51. * Creates a new debouncer with the specified timeout and edge type.
  52. * @param timeout Timeout in milliseconds between letting through calls - defaults to 200
  53. * @param type The edge type to use for the debouncer - see {@linkcode DebouncerType} for details or [the documentation for an explanation and diagram](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#debouncer) - defaults to "immediate"
  54. */
  55. constructor(protected timeout = 200, protected type: DebouncerType = "immediate") {
  56. super();
  57. }
  58. //#region listeners
  59. /** Adds a listener function that will be called on timeout */
  60. public addListener(fn: TFunc): void {
  61. this.listeners.push(fn);
  62. }
  63. /** Removes the listener with the specified function reference */
  64. public removeListener(fn: TFunc): void {
  65. const idx = this.listeners.findIndex((l) => l === fn);
  66. idx !== -1 && this.listeners.splice(idx, 1);
  67. }
  68. /** Removes all listeners */
  69. public removeAllListeners(): void {
  70. this.listeners = [];
  71. }
  72. /** Returns all registered listeners */
  73. public getListeners(): TFunc[] {
  74. return this.listeners;
  75. }
  76. //#region timeout
  77. /** Sets the timeout for the debouncer */
  78. public setTimeout(timeout: number): void {
  79. this.emit("change", this.timeout = timeout, this.type);
  80. }
  81. /** Returns the current timeout */
  82. public getTimeout(): number {
  83. return this.timeout;
  84. }
  85. /** Whether the timeout is currently active, meaning any latest call to the {@linkcode call()} method will be queued */
  86. public isTimeoutActive(): boolean {
  87. return typeof this.activeTimeout !== "undefined";
  88. }
  89. //#region type
  90. /** Sets the edge type for the debouncer */
  91. public setType(type: DebouncerType): void {
  92. this.emit("change", this.timeout, this.type = type);
  93. }
  94. /** Returns the current edge type */
  95. public getType(): DebouncerType {
  96. return this.type;
  97. }
  98. //#region call
  99. /** Use this to call the debouncer with the specified arguments that will be passed to all listener functions registered with {@linkcode addListener()} */
  100. public call(...args: Parameters<TFunc>): void {
  101. /** When called, calls all registered listeners */
  102. const cl = (...a: Parameters<TFunc>): void => {
  103. this.queuedCall = undefined;
  104. this.emit("call", ...a);
  105. this.listeners.forEach((l) => l.call(this, ...a));
  106. };
  107. /** Sets a timeout that will call the latest queued call and then set another timeout if there was a queued call */
  108. const setRepeatTimeout = (): void => {
  109. this.activeTimeout = setTimeout(() => {
  110. if(this.queuedCall) {
  111. this.queuedCall();
  112. setRepeatTimeout();
  113. }
  114. else
  115. this.activeTimeout = undefined;
  116. }, this.timeout);
  117. };
  118. switch(this.type) {
  119. case "immediate":
  120. if(typeof this.activeTimeout === "undefined") {
  121. cl(...args);
  122. setRepeatTimeout();
  123. }
  124. else
  125. this.queuedCall = () => cl(...args);
  126. break;
  127. case "idle":
  128. if(this.activeTimeout)
  129. clearTimeout(this.activeTimeout);
  130. this.activeTimeout = setTimeout(() => {
  131. cl(...args);
  132. this.activeTimeout = undefined;
  133. }, this.timeout);
  134. break;
  135. default:
  136. throw new TypeError(`Invalid debouncer type: ${this.type}`);
  137. }
  138. }
  139. }
  140. //#region debounce fn
  141. /**
  142. * Creates a {@linkcode Debouncer} instance with the specified timeout and edge type and attaches the passed function as a listener.
  143. * The returned function can be called with any arguments and will execute the `call()` method of the debouncer.
  144. * The debouncer instance is accessible via the `debouncer` property of the returned function.
  145. *
  146. * Refer to the {@linkcode Debouncer} class definition or the [Debouncer documentation](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#debouncer) for more information.
  147. */
  148. export function debounce<TFunc extends (...args: any[]) => any>(
  149. fn: TFunc,
  150. timeout = 200,
  151. type: DebouncerType = "immediate"
  152. ): DebouncedFunction<TFunc> {
  153. const debouncer = new Debouncer<TFunc>(timeout, type);
  154. debouncer.addListener(fn);
  155. const func = (((...args: Parameters<TFunc>) => debouncer.call(...args))) as DebouncedFunction<TFunc>;
  156. func.debouncer = debouncer;
  157. return func;
  158. }