@tanstack/pacer 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/cjs/async-debouncer.cjs +78 -45
  2. package/dist/cjs/async-debouncer.cjs.map +1 -1
  3. package/dist/cjs/async-debouncer.d.cts +60 -24
  4. package/dist/cjs/async-queuer.cjs +53 -3
  5. package/dist/cjs/async-queuer.cjs.map +1 -1
  6. package/dist/cjs/async-queuer.d.cts +28 -3
  7. package/dist/cjs/async-rate-limiter.cjs +75 -38
  8. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  9. package/dist/cjs/async-rate-limiter.d.cts +76 -24
  10. package/dist/cjs/async-throttler.cjs +85 -57
  11. package/dist/cjs/async-throttler.cjs.map +1 -1
  12. package/dist/cjs/async-throttler.d.cts +66 -25
  13. package/dist/cjs/debouncer.cjs +12 -14
  14. package/dist/cjs/debouncer.cjs.map +1 -1
  15. package/dist/cjs/debouncer.d.cts +10 -9
  16. package/dist/cjs/queuer.cjs +51 -1
  17. package/dist/cjs/queuer.cjs.map +1 -1
  18. package/dist/cjs/queuer.d.cts +30 -1
  19. package/dist/cjs/rate-limiter.cjs +19 -9
  20. package/dist/cjs/rate-limiter.cjs.map +1 -1
  21. package/dist/cjs/rate-limiter.d.cts +31 -11
  22. package/dist/cjs/throttler.cjs +29 -36
  23. package/dist/cjs/throttler.cjs.map +1 -1
  24. package/dist/cjs/throttler.d.cts +16 -17
  25. package/dist/cjs/types.d.cts +2 -6
  26. package/dist/cjs/utils.cjs.map +1 -1
  27. package/dist/cjs/utils.d.cts +1 -1
  28. package/dist/esm/async-debouncer.d.ts +60 -24
  29. package/dist/esm/async-debouncer.js +78 -45
  30. package/dist/esm/async-debouncer.js.map +1 -1
  31. package/dist/esm/async-queuer.d.ts +28 -3
  32. package/dist/esm/async-queuer.js +53 -3
  33. package/dist/esm/async-queuer.js.map +1 -1
  34. package/dist/esm/async-rate-limiter.d.ts +76 -24
  35. package/dist/esm/async-rate-limiter.js +75 -38
  36. package/dist/esm/async-rate-limiter.js.map +1 -1
  37. package/dist/esm/async-throttler.d.ts +66 -25
  38. package/dist/esm/async-throttler.js +85 -57
  39. package/dist/esm/async-throttler.js.map +1 -1
  40. package/dist/esm/debouncer.d.ts +10 -9
  41. package/dist/esm/debouncer.js +12 -14
  42. package/dist/esm/debouncer.js.map +1 -1
  43. package/dist/esm/queuer.d.ts +30 -1
  44. package/dist/esm/queuer.js +51 -1
  45. package/dist/esm/queuer.js.map +1 -1
  46. package/dist/esm/rate-limiter.d.ts +31 -11
  47. package/dist/esm/rate-limiter.js +19 -9
  48. package/dist/esm/rate-limiter.js.map +1 -1
  49. package/dist/esm/throttler.d.ts +16 -17
  50. package/dist/esm/throttler.js +29 -36
  51. package/dist/esm/throttler.js.map +1 -1
  52. package/dist/esm/types.d.ts +2 -6
  53. package/dist/esm/utils.d.ts +1 -1
  54. package/dist/esm/utils.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/async-debouncer.ts +130 -81
  57. package/src/async-queuer.ts +93 -8
  58. package/src/async-rate-limiter.ts +141 -67
  59. package/src/async-throttler.ts +152 -94
  60. package/src/debouncer.ts +26 -33
  61. package/src/queuer.ts +92 -4
  62. package/src/rate-limiter.ts +56 -32
  63. package/src/throttler.ts +45 -53
  64. package/src/types.ts +2 -10
  65. package/src/utils.ts +1 -1
@@ -1,17 +1,16 @@
1
1
  const defaultOptions = {
2
2
  enabled: true,
3
3
  leading: true,
4
- trailing: true,
5
- wait: 0,
6
4
  onExecute: () => {
7
- }
5
+ },
6
+ trailing: true,
7
+ wait: 0
8
8
  };
9
9
  class Throttler {
10
10
  constructor(fn, initialOptions) {
11
11
  this.fn = fn;
12
12
  this._executionCount = 0;
13
13
  this._lastExecutionTime = 0;
14
- this._isPending = false;
15
14
  this._options = {
16
15
  ...defaultOptions,
17
16
  ...initialOptions
@@ -22,11 +21,10 @@ class Throttler {
22
21
  * Returns the new options state
23
22
  */
24
23
  setOptions(newOptions) {
25
- this._options = {
26
- ...this._options,
27
- ...newOptions
28
- };
29
- return this._options;
24
+ this._options = { ...this._options, ...newOptions };
25
+ if (!this._options.enabled) {
26
+ this.cancel();
27
+ }
30
28
  }
31
29
  /**
32
30
  * Returns the current throttler options
@@ -59,26 +57,18 @@ class Throttler {
59
57
  maybeExecute(...args) {
60
58
  const now = Date.now();
61
59
  const timeSinceLastExecution = now - this._lastExecutionTime;
62
- if (timeSinceLastExecution >= this._options.wait) {
63
- if (this._options.leading) {
64
- this.executeFunction(...args);
65
- }
66
- this._lastExecutionTime = now;
67
- this._isPending = false;
60
+ if (this._options.leading && timeSinceLastExecution >= this._options.wait) {
61
+ this.executeFunction(...args);
68
62
  } else {
69
63
  this._lastArgs = args;
70
64
  if (!this._timeoutId && this._options.trailing) {
71
- this._isPending = true;
65
+ const _timeSinceLastExecution = this._lastExecutionTime ? now - this._lastExecutionTime : 0;
66
+ const timeoutDuration = this._options.wait - _timeSinceLastExecution;
72
67
  this._timeoutId = setTimeout(() => {
73
- if (this._lastArgs) {
68
+ if (this._lastArgs !== void 0) {
74
69
  this.executeFunction(...this._lastArgs);
75
- this._lastArgs = void 0;
76
70
  }
77
- this._lastExecutionTime = Date.now();
78
- this._timeoutId = void 0;
79
- this._isPending = false;
80
- this._options.onExecute(this);
81
- }, this._options.wait - timeSinceLastExecution);
71
+ }, timeoutDuration);
82
72
  }
83
73
  }
84
74
  }
@@ -86,6 +76,10 @@ class Throttler {
86
76
  if (!this._options.enabled) return;
87
77
  this.fn(...args);
88
78
  this._executionCount++;
79
+ this._lastExecutionTime = Date.now();
80
+ this._timeoutId = void 0;
81
+ this._lastArgs = void 0;
82
+ this._options.onExecute(this);
89
83
  }
90
84
  /**
91
85
  * Cancels any pending trailing execution and clears internal state.
@@ -101,21 +95,8 @@ class Throttler {
101
95
  clearTimeout(this._timeoutId);
102
96
  this._timeoutId = void 0;
103
97
  this._lastArgs = void 0;
104
- this._isPending = false;
105
98
  }
106
99
  }
107
- /**
108
- * Returns the number of times the function has been executed
109
- */
110
- getExecutionCount() {
111
- return this._executionCount;
112
- }
113
- /**
114
- * Returns `true` if there is a pending execution
115
- */
116
- getIsPending() {
117
- return this._options.enabled && this._isPending;
118
- }
119
100
  /**
120
101
  * Returns the last execution time
121
102
  */
@@ -128,6 +109,18 @@ class Throttler {
128
109
  getNextExecutionTime() {
129
110
  return this._lastExecutionTime + this._options.wait;
130
111
  }
112
+ /**
113
+ * Returns the number of times the function has been executed
114
+ */
115
+ getExecutionCount() {
116
+ return this._executionCount;
117
+ }
118
+ /**
119
+ * Returns `true` if there is a pending execution
120
+ */
121
+ getIsPending() {
122
+ return this._options.enabled && !!this._timeoutId;
123
+ }
131
124
  }
132
125
  function throttle(fn, initialOptions) {
133
126
  const throttler = new Throttler(fn, initialOptions);
@@ -1 +1 @@
1
- {"version":3,"file":"throttler.js","sources":["../../src/throttler.ts"],"sourcesContent":["import type { AnyFunction } from './types'\n\n/**\n * Options for configuring a throttled function\n */\nexport interface ThrottlerOptions<\n TFn extends AnyFunction,\n TArgs extends Parameters<TFn>,\n> {\n /**\n * Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.\n * Defaults to true.\n */\n enabled?: boolean\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to true.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (throttler: Throttler<TFn, TArgs>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once\n */\n wait: number\n}\n\nconst defaultOptions: Required<ThrottlerOptions<any, any>> = {\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n onExecute: () => {},\n}\n\n/**\n * A class that creates a throttled function.\n *\n * Throttling ensures a function is called at most once within a specified time window.\n * Unlike debouncing which waits for a pause in calls, throttling guarantees consistent\n * execution timing regardless of call frequency.\n *\n * Supports both leading and trailing edge execution:\n * - Leading: Execute immediately on first call (default: true)\n * - Trailing: Execute after wait period if called during throttle (default: true)\n *\n * For collapsing rapid-fire events where you only care about the last call, consider using Debouncer.\n *\n * @example\n * ```ts\n * const throttler = new Throttler(\n * (id: string) => api.getData(id),\n * { wait: 1000 } // Execute at most once per second\n * );\n *\n * // First call executes immediately\n * throttler.maybeExecute('123');\n *\n * // Subsequent calls within 1000ms are throttled\n * throttler.maybeExecute('123'); // Throttled\n * ```\n */\nexport class Throttler<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {\n private _executionCount = 0\n private _lastArgs: TArgs | undefined\n private _lastExecutionTime = 0\n private _options: Required<ThrottlerOptions<TFn, TArgs>>\n private _timeoutId: NodeJS.Timeout | undefined\n private _isPending = false\n\n constructor(\n private fn: TFn,\n initialOptions: ThrottlerOptions<TFn, TArgs>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n }\n }\n\n /**\n * Updates the throttler options\n * Returns the new options state\n */\n setOptions(\n newOptions: Partial<ThrottlerOptions<TFn, TArgs>>,\n ): Required<ThrottlerOptions<TFn, TArgs>> {\n this._options = {\n ...this._options,\n ...newOptions,\n }\n return this._options\n }\n\n /**\n * Returns the current throttler options\n */\n getOptions(): Required<ThrottlerOptions<TFn, TArgs>> {\n return this._options\n }\n\n /**\n * Attempts to execute the throttled function. The execution behavior depends on the throttler options:\n *\n * - If enough time has passed since the last execution (>= wait period):\n * - With leading=true: Executes immediately\n * - With leading=false: Waits for the next trailing execution\n *\n * - If within the wait period:\n * - With trailing=true: Schedules execution for end of wait period\n * - With trailing=false: Drops the execution\n *\n * @example\n * ```ts\n * const throttled = new Throttler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute(...args: TArgs): void {\n const now = Date.now()\n const timeSinceLastExecution = now - this._lastExecutionTime\n\n // Handle leading execution\n if (timeSinceLastExecution >= this._options.wait) {\n if (this._options.leading) {\n this.executeFunction(...args)\n }\n this._lastExecutionTime = now\n this._isPending = false\n } else {\n // Store the most recent arguments for potential trailing execution\n this._lastArgs = args\n\n // Set up trailing execution if not already scheduled\n if (!this._timeoutId && this._options.trailing) {\n this._isPending = true\n this._timeoutId = setTimeout(() => {\n if (this._lastArgs) {\n this.executeFunction(...this._lastArgs)\n this._lastArgs = undefined\n }\n this._lastExecutionTime = Date.now()\n this._timeoutId = undefined\n this._isPending = false\n this._options.onExecute(this)\n }, this._options.wait - timeSinceLastExecution)\n }\n }\n }\n\n private executeFunction(...args: TArgs): void {\n if (!this._options.enabled) return\n this.fn(...args) // EXECUTE!\n this._executionCount++\n }\n\n /**\n * Cancels any pending trailing execution and clears internal state.\n *\n * If a trailing execution is scheduled (due to throttling with trailing=true),\n * this will prevent that execution from occurring. The internal timeout and\n * stored arguments will be cleared.\n *\n * Has no effect if there is no pending execution.\n */\n cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = undefined\n this._lastArgs = undefined\n this._isPending = false\n }\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns `true` if there is a pending execution\n */\n getIsPending(): boolean {\n return this._options.enabled && this._isPending\n }\n\n /**\n * Returns the last execution time\n */\n getLastExecutionTime(): number {\n return this._lastExecutionTime\n }\n\n /**\n * Returns the next execution time\n */\n getNextExecutionTime(): number {\n return this._lastExecutionTime + this._options.wait\n }\n}\n\n/**\n * Creates a throttled function that limits how often the provided function can execute.\n *\n * Throttling ensures a function executes at most once within a specified time window,\n * regardless of how many times it is called. This is useful for rate-limiting\n * expensive operations or UI updates.\n *\n * The throttled function can be configured to execute on the leading and/or trailing\n * edge of the throttle window via options.\n *\n * For handling bursts of events, consider using debounce() instead. For hard execution\n * limits, consider using rateLimit().\n *\n * @example\n * ```ts\n * // Basic throttling - max once per second\n * const throttled = throttle(updateUI, { wait: 1000 });\n *\n * // Configure leading/trailing execution\n * const throttled = throttle(saveData, {\n * wait: 2000,\n * leading: true, // Execute immediately on first call\n * trailing: true // Execute again after delay if called during wait\n * });\n * ```\n */\nexport function throttle<\n TFn extends AnyFunction,\n TArgs extends Parameters<TFn>,\n>(fn: TFn, initialOptions: Omit<ThrottlerOptions<TFn, TArgs>, 'enabled'>) {\n const throttler = new Throttler(fn, initialOptions)\n return throttler.maybeExecute.bind(throttler)\n}\n"],"names":[],"mappings":"AAkCA,MAAM,iBAAuD;AAAA,EAC3D,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,MAAM;AAAA,EACN,WAAW,MAAM;AAAA,EAAA;AACnB;AA6BO,MAAM,UAAkE;AAAA,EAQ7E,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AARV,SAAQ,kBAAkB;AAE1B,SAAQ,qBAAqB;AAG7B,SAAQ,aAAa;AAMnB,SAAK,WAAW;AAAA,MACd,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOF,WACE,YACwC;AACxC,SAAK,WAAW;AAAA,MACd,GAAG,KAAK;AAAA,MACR,GAAG;AAAA,IACL;AACA,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,aAAqD;AACnD,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyBd,gBAAgB,MAAmB;AAC3B,UAAA,MAAM,KAAK,IAAI;AACf,UAAA,yBAAyB,MAAM,KAAK;AAGtC,QAAA,0BAA0B,KAAK,SAAS,MAAM;AAC5C,UAAA,KAAK,SAAS,SAAS;AACpB,aAAA,gBAAgB,GAAG,IAAI;AAAA,MAAA;AAE9B,WAAK,qBAAqB;AAC1B,WAAK,aAAa;AAAA,IAAA,OACb;AAEL,WAAK,YAAY;AAGjB,UAAI,CAAC,KAAK,cAAc,KAAK,SAAS,UAAU;AAC9C,aAAK,aAAa;AACb,aAAA,aAAa,WAAW,MAAM;AACjC,cAAI,KAAK,WAAW;AACb,iBAAA,gBAAgB,GAAG,KAAK,SAAS;AACtC,iBAAK,YAAY;AAAA,UAAA;AAEd,eAAA,qBAAqB,KAAK,IAAI;AACnC,eAAK,aAAa;AAClB,eAAK,aAAa;AACb,eAAA,SAAS,UAAU,IAAI;AAAA,QAC3B,GAAA,KAAK,SAAS,OAAO,sBAAsB;AAAA,MAAA;AAAA,IAChD;AAAA,EACF;AAAA,EAGM,mBAAmB,MAAmB;AACxC,QAAA,CAAC,KAAK,SAAS,QAAS;AACvB,SAAA,GAAG,GAAG,IAAI;AACV,SAAA;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYP,SAAe;AACb,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,aAAa;AAClB,WAAK,YAAY;AACjB,WAAK,aAAa;AAAA,IAAA;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAMF,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,eAAwB;AACf,WAAA,KAAK,SAAS,WAAW,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMvC,uBAA+B;AAC7B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,uBAA+B;AACtB,WAAA,KAAK,qBAAqB,KAAK,SAAS;AAAA,EAAA;AAEnD;AA4BgB,SAAA,SAGd,IAAS,gBAA+D;AACxE,QAAM,YAAY,IAAI,UAAU,IAAI,cAAc;AAC3C,SAAA,UAAU,aAAa,KAAK,SAAS;AAC9C;"}
1
+ {"version":3,"file":"throttler.js","sources":["../../src/throttler.ts"],"sourcesContent":["import type { AnyFunction } from './types'\n\n/**\n * Options for configuring a throttled function\n */\nexport interface ThrottlerOptions<TFn extends AnyFunction> {\n /**\n * Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.\n * Defaults to true.\n */\n enabled?: boolean\n /**\n * Whether to execute on the leading edge of the timeout.\n * Defaults to true.\n */\n leading?: boolean\n /**\n * Callback function that is called after the function is executed\n */\n onExecute?: (throttler: Throttler<TFn>) => void\n /**\n * Whether to execute on the trailing edge of the timeout.\n * Defaults to true.\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once\n */\n wait: number\n}\n\nconst defaultOptions: Required<ThrottlerOptions<any>> = {\n enabled: true,\n leading: true,\n onExecute: () => {},\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates a throttled function.\n *\n * Throttling ensures a function is called at most once within a specified time window.\n * Unlike debouncing which waits for a pause in calls, throttling guarantees consistent\n * execution timing regardless of call frequency.\n *\n * Supports both leading and trailing edge execution:\n * - Leading: Execute immediately on first call (default: true)\n * - Trailing: Execute after wait period if called during throttle (default: true)\n *\n * For collapsing rapid-fire events where you only care about the last call, consider using Debouncer.\n *\n * @example\n * ```ts\n * const throttler = new Throttler(\n * (id: string) => api.getData(id),\n * { wait: 1000 } // Execute at most once per second\n * );\n *\n * // First call executes immediately\n * throttler.maybeExecute('123');\n *\n * // Subsequent calls within 1000ms are throttled\n * throttler.maybeExecute('123'); // Throttled\n * ```\n */\nexport class Throttler<TFn extends AnyFunction> {\n private _executionCount = 0\n private _lastArgs: Parameters<TFn> | undefined\n private _lastExecutionTime = 0\n private _options: Required<ThrottlerOptions<TFn>>\n private _timeoutId: NodeJS.Timeout | undefined\n\n constructor(\n private fn: TFn,\n initialOptions: ThrottlerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n }\n }\n\n /**\n * Updates the throttler options\n * Returns the new options state\n */\n setOptions(newOptions: Partial<ThrottlerOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this.cancel()\n }\n }\n\n /**\n * Returns the current throttler options\n */\n getOptions(): Required<ThrottlerOptions<TFn>> {\n return this._options\n }\n\n /**\n * Attempts to execute the throttled function. The execution behavior depends on the throttler options:\n *\n * - If enough time has passed since the last execution (>= wait period):\n * - With leading=true: Executes immediately\n * - With leading=false: Waits for the next trailing execution\n *\n * - If within the wait period:\n * - With trailing=true: Schedules execution for end of wait period\n * - With trailing=false: Drops the execution\n *\n * @example\n * ```ts\n * const throttled = new Throttler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute(...args: Parameters<TFn>): void {\n const now = Date.now()\n const timeSinceLastExecution = now - this._lastExecutionTime\n\n // Handle leading execution\n if (this._options.leading && timeSinceLastExecution >= this._options.wait) {\n this.executeFunction(...args)\n } else {\n // Store the most recent arguments for potential trailing execution\n this._lastArgs = args\n\n // Set up trailing execution if not already scheduled\n if (!this._timeoutId && this._options.trailing) {\n const _timeSinceLastExecution = this._lastExecutionTime\n ? now - this._lastExecutionTime\n : 0\n const timeoutDuration = this._options.wait - _timeSinceLastExecution\n this._timeoutId = setTimeout(() => {\n if (this._lastArgs !== undefined) {\n this.executeFunction(...this._lastArgs)\n }\n }, timeoutDuration)\n }\n }\n }\n\n private executeFunction(...args: Parameters<TFn>): void {\n if (!this._options.enabled) return\n this.fn(...args) // EXECUTE!\n this._executionCount++\n this._lastExecutionTime = Date.now()\n this._timeoutId = undefined\n this._lastArgs = undefined\n this._options.onExecute(this)\n }\n\n /**\n * Cancels any pending trailing execution and clears internal state.\n *\n * If a trailing execution is scheduled (due to throttling with trailing=true),\n * this will prevent that execution from occurring. The internal timeout and\n * stored arguments will be cleared.\n *\n * Has no effect if there is no pending execution.\n */\n cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = undefined\n this._lastArgs = undefined\n }\n }\n\n /**\n * Returns the last execution time\n */\n getLastExecutionTime(): number {\n return this._lastExecutionTime\n }\n\n /**\n * Returns the next execution time\n */\n getNextExecutionTime(): number {\n return this._lastExecutionTime + this._options.wait\n }\n\n /**\n * Returns the number of times the function has been executed\n */\n getExecutionCount(): number {\n return this._executionCount\n }\n\n /**\n * Returns `true` if there is a pending execution\n */\n getIsPending(): boolean {\n return this._options.enabled && !!this._timeoutId\n }\n}\n\n/**\n * Creates a throttled function that limits how often the provided function can execute.\n *\n * Throttling ensures a function executes at most once within a specified time window,\n * regardless of how many times it is called. This is useful for rate-limiting\n * expensive operations or UI updates.\n *\n * The throttled function can be configured to execute on the leading and/or trailing\n * edge of the throttle window via options.\n *\n * For handling bursts of events, consider using debounce() instead. For hard execution\n * limits, consider using rateLimit().\n *\n * @example\n * ```ts\n * // Basic throttling - max once per second\n * const throttled = throttle(updateUI, { wait: 1000 });\n *\n * // Configure leading/trailing execution\n * const throttled = throttle(saveData, {\n * wait: 2000,\n * leading: true, // Execute immediately on first call\n * trailing: true // Execute again after delay if called during wait\n * });\n * ```\n */\nexport function throttle<TFn extends AnyFunction>(\n fn: TFn,\n initialOptions: Omit<ThrottlerOptions<TFn>, 'enabled'>,\n) {\n const throttler = new Throttler(fn, initialOptions)\n return throttler.maybeExecute.bind(throttler)\n}\n"],"names":[],"mappings":"AA+BA,MAAM,iBAAkD;AAAA,EACtD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW,MAAM;AAAA,EAAC;AAAA,EAClB,UAAU;AAAA,EACV,MAAM;AACR;AA6BO,MAAM,UAAmC;AAAA,EAO9C,YACU,IACR,gBACA;AAFQ,SAAA,KAAA;AAPV,SAAQ,kBAAkB;AAE1B,SAAQ,qBAAqB;AAQ3B,SAAK,WAAW;AAAA,MACd,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOF,WAAW,YAAkD;AAC3D,SAAK,WAAW,EAAE,GAAG,KAAK,UAAU,GAAG,WAAW;AAG9C,QAAA,CAAC,KAAK,SAAS,SAAS;AAC1B,WAAK,OAAO;AAAA,IAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAMF,aAA8C;AAC5C,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyBd,gBAAgB,MAA6B;AACrC,UAAA,MAAM,KAAK,IAAI;AACf,UAAA,yBAAyB,MAAM,KAAK;AAG1C,QAAI,KAAK,SAAS,WAAW,0BAA0B,KAAK,SAAS,MAAM;AACpE,WAAA,gBAAgB,GAAG,IAAI;AAAA,IAAA,OACvB;AAEL,WAAK,YAAY;AAGjB,UAAI,CAAC,KAAK,cAAc,KAAK,SAAS,UAAU;AAC9C,cAAM,0BAA0B,KAAK,qBACjC,MAAM,KAAK,qBACX;AACE,cAAA,kBAAkB,KAAK,SAAS,OAAO;AACxC,aAAA,aAAa,WAAW,MAAM;AAC7B,cAAA,KAAK,cAAc,QAAW;AAC3B,iBAAA,gBAAgB,GAAG,KAAK,SAAS;AAAA,UAAA;AAAA,WAEvC,eAAe;AAAA,MAAA;AAAA,IACpB;AAAA,EACF;AAAA,EAGM,mBAAmB,MAA6B;AAClD,QAAA,CAAC,KAAK,SAAS,QAAS;AACvB,SAAA,GAAG,GAAG,IAAI;AACV,SAAA;AACA,SAAA,qBAAqB,KAAK,IAAI;AACnC,SAAK,aAAa;AAClB,SAAK,YAAY;AACZ,SAAA,SAAS,UAAU,IAAI;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAY9B,SAAe;AACb,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK,UAAU;AAC5B,WAAK,aAAa;AAClB,WAAK,YAAY;AAAA,IAAA;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAMF,uBAA+B;AAC7B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,uBAA+B;AACtB,WAAA,KAAK,qBAAqB,KAAK,SAAS;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMjD,oBAA4B;AAC1B,WAAO,KAAK;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMd,eAAwB;AACtB,WAAO,KAAK,SAAS,WAAW,CAAC,CAAC,KAAK;AAAA,EAAA;AAE3C;AA4BgB,SAAA,SACd,IACA,gBACA;AACA,QAAM,YAAY,IAAI,UAAU,IAAI,cAAc;AAC3C,SAAA,UAAU,aAAa,KAAK,SAAS;AAC9C;"}
@@ -1,12 +1,8 @@
1
1
  /**
2
2
  * Represents a function that can be called with any arguments and returns any value.
3
- * @template TArgs - The type of the arguments the function can be called with.
4
- * @returns The return value of the function.
5
3
  */
6
- export type AnyFunction<TArgs extends Array<any> = Array<any>> = (...args: TArgs) => any;
4
+ export type AnyFunction = (...args: Array<any>) => any;
7
5
  /**
8
6
  * Represents an asynchronous function that can be called with any arguments and returns a promise.
9
- * @template TArgs - The type of the arguments the function can be called with.
10
- * @returns A promise that resolves to the return value of the function.
11
7
  */
12
- export type AnyAsyncFunction<TArgs extends Array<any> = Array<any>> = (...args: TArgs) => Promise<any>;
8
+ export type AnyAsyncFunction = (...args: Array<any>) => Promise<any>;
@@ -1 +1 @@
1
- export declare function bindInstanceMethods<T extends Record<string, any>>(instance: T): any;
1
+ export declare function bindInstanceMethods<T extends Record<string, any>>(instance: T): T;
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["export function bindInstanceMethods<T extends Record<string, any>>(\n instance: T,\n) {\n return Object.getOwnPropertyNames(Object.getPrototypeOf(instance))\n .filter((key) => typeof instance[key as keyof T] === 'function')\n .reduce((acc: any, key) => {\n const method = instance[key as keyof T]\n if (typeof method === 'function') {\n acc[key] = method.bind(instance)\n }\n return acc\n }, {} as T)\n}\n"],"names":[],"mappings":"AAAO,SAAS,oBACd,UACA;AACA,SAAO,OAAO,oBAAoB,OAAO,eAAe,QAAQ,CAAC,EAC9D,OAAO,CAAC,QAAQ,OAAO,SAAS,GAAc,MAAM,UAAU,EAC9D,OAAO,CAAC,KAAU,QAAQ;AACnB,UAAA,SAAS,SAAS,GAAc;AAClC,QAAA,OAAO,WAAW,YAAY;AAChC,UAAI,GAAG,IAAI,OAAO,KAAK,QAAQ;AAAA,IAAA;AAE1B,WAAA;AAAA,EACT,GAAG,EAAO;AACd;"}
1
+ {"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["export function bindInstanceMethods<T extends Record<string, any>>(\n instance: T,\n): T {\n return Object.getOwnPropertyNames(Object.getPrototypeOf(instance))\n .filter((key) => typeof instance[key as keyof T] === 'function')\n .reduce((acc: any, key) => {\n const method = instance[key as keyof T]\n if (typeof method === 'function') {\n acc[key] = method.bind(instance)\n }\n return acc\n }, {} as T)\n}\n"],"names":[],"mappings":"AAAO,SAAS,oBACd,UACG;AACH,SAAO,OAAO,oBAAoB,OAAO,eAAe,QAAQ,CAAC,EAC9D,OAAO,CAAC,QAAQ,OAAO,SAAS,GAAc,MAAM,UAAU,EAC9D,OAAO,CAAC,KAAU,QAAQ;AACnB,UAAA,SAAS,SAAS,GAAc;AAClC,QAAA,OAAO,WAAW,YAAY;AAChC,UAAI,GAAG,IAAI,OAAO,KAAK,QAAQ;AAAA,IAAA;AAE1B,WAAA;AAAA,EACT,GAAG,EAAO;AACd;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/pacer",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Utilities for debouncing, throttling, rate-limiting, queuing, and more.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -3,10 +3,7 @@ import type { AnyAsyncFunction } from './types'
3
3
  /**
4
4
  * Options for configuring an async debounced function
5
5
  */
6
- export interface AsyncDebouncerOptions<
7
- TFn extends AnyAsyncFunction,
8
- TArgs extends Parameters<TFn>,
9
- > {
6
+ export interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {
10
7
  /**
11
8
  * Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.
12
9
  * Defaults to true.
@@ -20,11 +17,15 @@ export interface AsyncDebouncerOptions<
20
17
  /**
21
18
  * Optional error handler for when the debounced function throws
22
19
  */
23
- onError?: (error: unknown) => void
20
+ onError?: (error: unknown, debouncer: AsyncDebouncer<TFn>) => void
24
21
  /**
25
- * Optional function to call when the debounced function is executed
22
+ * Optional callback to call when the debounced function is executed
26
23
  */
27
- onExecute?: (debouncer: AsyncDebouncer<TFn, TArgs>) => void
24
+ onSettled?: (debouncer: AsyncDebouncer<TFn>) => void
25
+ /**
26
+ * Optional callback to call when the debounced function is executed
27
+ */
28
+ onSuccess?: (result: ReturnType<TFn>, debouncer: AsyncDebouncer<TFn>) => void
28
29
  /**
29
30
  * Whether to execute on the trailing edge of the timeout.
30
31
  * Defaults to true.
@@ -37,12 +38,13 @@ export interface AsyncDebouncerOptions<
37
38
  wait: number
38
39
  }
39
40
 
40
- const defaultOptions: Required<AsyncDebouncerOptions<any, any>> = {
41
+ const defaultOptions: Required<AsyncDebouncerOptions<any>> = {
41
42
  enabled: true,
42
43
  leading: false,
43
- trailing: true,
44
44
  onError: () => {},
45
- onExecute: () => {},
45
+ onSettled: () => {},
46
+ onSuccess: () => {},
47
+ trailing: true,
46
48
  wait: 0,
47
49
  }
48
50
 
@@ -56,33 +58,38 @@ const defaultOptions: Required<AsyncDebouncerOptions<any, any>> = {
56
58
  * Unlike throttling which allows execution at regular intervals, debouncing prevents any execution until
57
59
  * the function stops being called for the specified delay period.
58
60
  *
61
+ * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,
62
+ * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call
63
+ * instead of setting the result on a state variable from within the debounced function.
64
+ *
59
65
  * @example
60
66
  * ```ts
61
67
  * const asyncDebouncer = new AsyncDebouncer(async (value: string) => {
62
- * await searchAPI(value);
68
+ * const results = await searchAPI(value);
69
+ * return results; // Return value is preserved
63
70
  * }, { wait: 500 });
64
71
  *
65
72
  * // Called on each keystroke but only executes after 500ms of no typing
66
- * inputElement.addEventListener('input', () => {
67
- * asyncDebouncer.maybeExecute(inputElement.value);
68
- * });
73
+ * // Returns the API response directly
74
+ * const results = await asyncDebouncer.maybeExecute(inputElement.value);
69
75
  * ```
70
76
  */
71
- export class AsyncDebouncer<
72
- TFn extends AnyAsyncFunction,
73
- TArgs extends Parameters<TFn>,
74
- > {
77
+ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
75
78
  private _abortController: AbortController | null = null
76
- private _executionCount = 0
77
- private _isExecuting = false
78
- private _lastArgs: TArgs | undefined
79
- private _options: Required<AsyncDebouncerOptions<TFn, TArgs>>
80
- private _timeoutId: ReturnType<typeof setTimeout> | null = null
81
79
  private _canLeadingExecute = true
80
+ private _errorCount = 0
81
+ private _isExecuting = false
82
+ private _isPending = false
83
+ private _lastArgs: Parameters<TFn> | undefined
84
+ private _lastResult: ReturnType<TFn> | undefined
85
+ private _options: Required<AsyncDebouncerOptions<TFn>>
86
+ private _settleCount = 0
87
+ private _successCount = 0
88
+ private _timeoutId: NodeJS.Timeout | null = null
82
89
 
83
90
  constructor(
84
91
  private fn: TFn,
85
- initialOptions: AsyncDebouncerOptions<TFn, TArgs>,
92
+ initialOptions: AsyncDebouncerOptions<TFn>,
86
93
  ) {
87
94
  this._options = {
88
95
  ...defaultOptions,
@@ -94,20 +101,19 @@ export class AsyncDebouncer<
94
101
  * Updates the debouncer options
95
102
  * Returns the new options state
96
103
  */
97
- setOptions(
98
- newOptions: Partial<AsyncDebouncerOptions<TFn, TArgs>>,
99
- ): Required<AsyncDebouncerOptions<TFn, TArgs>> {
100
- this._options = {
101
- ...this._options,
102
- ...newOptions,
104
+ setOptions(newOptions: Partial<AsyncDebouncerOptions<TFn>>): void {
105
+ this._options = { ...this._options, ...newOptions }
106
+
107
+ // End the pending state if the debouncer is disabled
108
+ if (!this._options.enabled) {
109
+ this._isPending = false
103
110
  }
104
- return this._options
105
111
  }
106
112
 
107
113
  /**
108
114
  * Returns the current debouncer options
109
115
  */
110
- getOptions(): Required<AsyncDebouncerOptions<TFn, TArgs>> {
116
+ getOptions(): Required<AsyncDebouncerOptions<TFn>> {
111
117
  return this._options
112
118
  }
113
119
 
@@ -115,61 +121,65 @@ export class AsyncDebouncer<
115
121
  * Attempts to execute the debounced function
116
122
  * If a call is already in progress, it will be queued
117
123
  */
118
- async maybeExecute(...args: TArgs): Promise<void> {
119
- this.cancel()
124
+ async maybeExecute(
125
+ ...args: Parameters<TFn>
126
+ ): Promise<ReturnType<TFn> | undefined> {
127
+ this._cancel()
120
128
  this._lastArgs = args
121
129
 
122
130
  // Handle leading execution
123
131
  if (this._options.leading && this._canLeadingExecute) {
124
132
  this._canLeadingExecute = false
125
133
  await this.executeFunction(...args)
134
+ return this._lastResult
135
+ }
136
+
137
+ // Handle trailing execution
138
+ if (this._options.trailing) {
139
+ this._isPending = true
126
140
  }
127
141
 
128
142
  return new Promise((resolve) => {
129
143
  this._timeoutId = setTimeout(async () => {
130
- if (this._isExecuting) {
131
- resolve()
132
- return
144
+ // Execute trailing if enabled
145
+ if (this._options.trailing && this._lastArgs) {
146
+ await this.executeFunction(...this._lastArgs)
133
147
  }
134
148
 
149
+ // Reset state and resolve
135
150
  this._canLeadingExecute = true
136
- // Execute trailing only if enabled
137
- if (this._options.trailing) {
138
- this._abortController = new AbortController()
139
- try {
140
- this._isExecuting = true
141
- if (this._lastArgs) {
142
- await this.executeFunction(...this._lastArgs)
143
- }
144
- } catch (error) {
145
- try {
146
- this._options.onError(error)
147
- } catch {
148
- console.error('Error in error handler', error)
149
- }
150
- } finally {
151
- this._isExecuting = false
152
- this._abortController = null
153
- resolve()
154
- }
155
- } else {
156
- resolve()
157
- }
151
+ resolve(this._lastResult)
158
152
  }, this._options.wait)
159
153
  })
160
154
  }
161
155
 
162
- private async executeFunction(...args: TArgs): Promise<void> {
163
- if (!this._options.enabled) return
164
- this._executionCount++
165
- await this.fn(...args)
166
- this._options.onExecute(this)
156
+ private async executeFunction(
157
+ ...args: Parameters<TFn>
158
+ ): Promise<ReturnType<TFn> | undefined> {
159
+ if (!this._options.enabled) return undefined
160
+ this._abortController = new AbortController()
161
+ try {
162
+ this._isExecuting = true
163
+ this._lastResult = await this.fn(...args) // EXECUTE!
164
+ this._successCount++
165
+ this._options.onSuccess(this._lastResult!, this)
166
+ } catch (error) {
167
+ this._errorCount++
168
+ this._options.onError(error, this)
169
+ } finally {
170
+ this._isExecuting = false
171
+ this._isPending = false
172
+ this._settleCount++
173
+ this._abortController = null
174
+ this._options.onSettled(this)
175
+ }
176
+ return this._lastResult
167
177
  }
168
178
 
169
179
  /**
170
- * Cancels any pending execution
180
+ * Cancel without resetting _canLeadingExecute
171
181
  */
172
- cancel(): void {
182
+ private _cancel(): void {
173
183
  if (this._timeoutId) {
174
184
  clearTimeout(this._timeoutId)
175
185
  this._timeoutId = null
@@ -179,23 +189,58 @@ export class AsyncDebouncer<
179
189
  this._abortController = null
180
190
  }
181
191
  this._lastArgs = undefined
192
+ this._isPending = false
193
+ this._isExecuting = false
194
+ }
195
+
196
+ /**
197
+ * Cancels any pending execution or aborts any execution in progress
198
+ */
199
+ cancel(): void {
182
200
  this._canLeadingExecute = true
201
+ this._cancel()
202
+ }
203
+
204
+ /**
205
+ * Returns the last result of the debounced function
206
+ */
207
+ getLastResult(): ReturnType<TFn> | undefined {
208
+ return this._lastResult
209
+ }
210
+
211
+ /**
212
+ * Returns the number of times the function has been executed successfully
213
+ */
214
+ getSuccessCount(): number {
215
+ return this._successCount
183
216
  }
184
217
 
185
218
  /**
186
- * Returns the number of times the function has been executed
219
+ * Returns the number of times the function has settled (completed or errored)
187
220
  */
188
- getExecutionCount(): number {
189
- return this._executionCount
221
+ getSettleCount(): number {
222
+ return this._settleCount
190
223
  }
191
224
 
192
225
  /**
193
- * Returns `true` if there is a pending execution
226
+ * Returns the number of times the function has errored
227
+ */
228
+ getErrorCount(): number {
229
+ return this._errorCount
230
+ }
231
+
232
+ /**
233
+ * Returns `true` if there is a pending execution queued up for trailing execution
194
234
  */
195
235
  getIsPending(): boolean {
196
- return (
197
- this._options.enabled && (this._timeoutId !== null || this._isExecuting)
198
- )
236
+ return this._options.enabled && this._isPending
237
+ }
238
+
239
+ /**
240
+ * Returns `true` if there is currently an execution in progress
241
+ */
242
+ getIsExecuting(): boolean {
243
+ return this._isExecuting
199
244
  }
200
245
  }
201
246
 
@@ -204,22 +249,26 @@ export class AsyncDebouncer<
204
249
  * The debounced function will only execute once the wait period has elapsed without any new calls.
205
250
  * If called again during the wait period, the timer resets and a new wait period begins.
206
251
  *
252
+ * Unlike the non-async Debouncer, this async version supports returning values from the debounced function,
253
+ * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call
254
+ * instead of setting the result on a state variable from within the debounced function.
255
+ *
207
256
  * @example
208
257
  * ```ts
209
258
  * const debounced = asyncDebounce(async (value: string) => {
210
- * await saveToAPI(value);
259
+ * const result = await saveToAPI(value);
260
+ * return result; // Return value is preserved
211
261
  * }, { wait: 1000 });
212
262
  *
213
263
  * // Will only execute once, 1 second after the last call
214
- * await debounced("first"); // Cancelled
215
- * await debounced("second"); // Cancelled
216
- * await debounced("third"); // Executes after 1s
264
+ * // Returns the API response directly
265
+ * const result = await debounced("third");
217
266
  * ```
218
267
  */
219
- export function asyncDebounce<
220
- TFn extends AnyAsyncFunction,
221
- TArgs extends Parameters<TFn>,
222
- >(fn: TFn, initialOptions: Omit<AsyncDebouncerOptions<TFn, TArgs>, 'enabled'>) {
268
+ export function asyncDebounce<TFn extends AnyAsyncFunction>(
269
+ fn: TFn,
270
+ initialOptions: Omit<AsyncDebouncerOptions<TFn>, 'enabled'>,
271
+ ) {
223
272
  const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)
224
273
  return asyncDebouncer.maybeExecute.bind(asyncDebouncer)
225
274
  }