@tanstack/pacer 0.2.0 → 0.3.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 +44 -16
  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 +46 -35
  8. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  9. package/dist/cjs/async-rate-limiter.d.cts +34 -19
  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 +49 -17
  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 +1 -5
  20. package/dist/cjs/rate-limiter.cjs.map +1 -1
  21. package/dist/cjs/rate-limiter.d.cts +9 -9
  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 +44 -16
  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 +34 -19
  35. package/dist/esm/async-rate-limiter.js +46 -35
  36. package/dist/esm/async-rate-limiter.js.map +1 -1
  37. package/dist/esm/async-throttler.d.ts +49 -17
  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 +9 -9
  47. package/dist/esm/rate-limiter.js +1 -5
  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 +114 -73
  57. package/src/async-queuer.ts +93 -8
  58. package/src/async-rate-limiter.ts +74 -60
  59. package/src/async-throttler.ts +135 -86
  60. package/src/debouncer.ts +26 -33
  61. package/src/queuer.ts +92 -4
  62. package/src/rate-limiter.ts +14 -26
  63. package/src/throttler.ts +45 -53
  64. package/src/types.ts +2 -10
  65. package/src/utils.ts +1 -1
@@ -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.3.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
 
@@ -68,21 +70,22 @@ const defaultOptions: Required<AsyncDebouncerOptions<any, any>> = {
68
70
  * });
69
71
  * ```
70
72
  */
71
- export class AsyncDebouncer<
72
- TFn extends AnyAsyncFunction,
73
- TArgs extends Parameters<TFn>,
74
- > {
73
+ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
75
74
  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
75
  private _canLeadingExecute = true
76
+ private _errorCount = 0
77
+ private _isExecuting = false
78
+ private _isPending = false
79
+ private _lastArgs: Parameters<TFn> | undefined
80
+ private _lastResult: ReturnType<TFn> | undefined
81
+ private _options: Required<AsyncDebouncerOptions<TFn>>
82
+ private _settleCount = 0
83
+ private _successCount = 0
84
+ private _timeoutId: NodeJS.Timeout | null = null
82
85
 
83
86
  constructor(
84
87
  private fn: TFn,
85
- initialOptions: AsyncDebouncerOptions<TFn, TArgs>,
88
+ initialOptions: AsyncDebouncerOptions<TFn>,
86
89
  ) {
87
90
  this._options = {
88
91
  ...defaultOptions,
@@ -94,20 +97,19 @@ export class AsyncDebouncer<
94
97
  * Updates the debouncer options
95
98
  * Returns the new options state
96
99
  */
97
- setOptions(
98
- newOptions: Partial<AsyncDebouncerOptions<TFn, TArgs>>,
99
- ): Required<AsyncDebouncerOptions<TFn, TArgs>> {
100
- this._options = {
101
- ...this._options,
102
- ...newOptions,
100
+ setOptions(newOptions: Partial<AsyncDebouncerOptions<TFn>>): void {
101
+ this._options = { ...this._options, ...newOptions }
102
+
103
+ // End the pending state if the debouncer is disabled
104
+ if (!this._options.enabled) {
105
+ this._isPending = false
103
106
  }
104
- return this._options
105
107
  }
106
108
 
107
109
  /**
108
110
  * Returns the current debouncer options
109
111
  */
110
- getOptions(): Required<AsyncDebouncerOptions<TFn, TArgs>> {
112
+ getOptions(): Required<AsyncDebouncerOptions<TFn>> {
111
113
  return this._options
112
114
  }
113
115
 
@@ -115,61 +117,65 @@ export class AsyncDebouncer<
115
117
  * Attempts to execute the debounced function
116
118
  * If a call is already in progress, it will be queued
117
119
  */
118
- async maybeExecute(...args: TArgs): Promise<void> {
119
- this.cancel()
120
+ async maybeExecute(
121
+ ...args: Parameters<TFn>
122
+ ): Promise<ReturnType<TFn> | undefined> {
123
+ this._cancel()
120
124
  this._lastArgs = args
121
125
 
122
126
  // Handle leading execution
123
127
  if (this._options.leading && this._canLeadingExecute) {
124
128
  this._canLeadingExecute = false
125
129
  await this.executeFunction(...args)
130
+ return this._lastResult
131
+ }
132
+
133
+ // Handle trailing execution
134
+ if (this._options.trailing) {
135
+ this._isPending = true
126
136
  }
127
137
 
128
138
  return new Promise((resolve) => {
129
139
  this._timeoutId = setTimeout(async () => {
130
- if (this._isExecuting) {
131
- resolve()
132
- return
140
+ // Execute trailing if enabled
141
+ if (this._options.trailing && this._lastArgs) {
142
+ await this.executeFunction(...this._lastArgs)
133
143
  }
134
144
 
145
+ // Reset state and resolve
135
146
  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
- }
147
+ resolve(this._lastResult)
158
148
  }, this._options.wait)
159
149
  })
160
150
  }
161
151
 
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)
152
+ private async executeFunction(
153
+ ...args: Parameters<TFn>
154
+ ): Promise<ReturnType<TFn> | undefined> {
155
+ if (!this._options.enabled) return undefined
156
+ this._abortController = new AbortController()
157
+ try {
158
+ this._isExecuting = true
159
+ this._lastResult = await this.fn(...args) // EXECUTE!
160
+ this._successCount++
161
+ this._options.onSuccess(this._lastResult!, this)
162
+ } catch (error) {
163
+ this._errorCount++
164
+ this._options.onError(error, this)
165
+ } finally {
166
+ this._isExecuting = false
167
+ this._isPending = false
168
+ this._settleCount++
169
+ this._abortController = null
170
+ this._options.onSettled(this)
171
+ }
172
+ return this._lastResult
167
173
  }
168
174
 
169
175
  /**
170
- * Cancels any pending execution
176
+ * Cancel without resetting _canLeadingExecute
171
177
  */
172
- cancel(): void {
178
+ private _cancel(): void {
173
179
  if (this._timeoutId) {
174
180
  clearTimeout(this._timeoutId)
175
181
  this._timeoutId = null
@@ -179,23 +185,58 @@ export class AsyncDebouncer<
179
185
  this._abortController = null
180
186
  }
181
187
  this._lastArgs = undefined
188
+ this._isPending = false
189
+ this._isExecuting = false
190
+ }
191
+
192
+ /**
193
+ * Cancels any pending execution or aborts any execution in progress
194
+ */
195
+ cancel(): void {
182
196
  this._canLeadingExecute = true
197
+ this._cancel()
198
+ }
199
+
200
+ /**
201
+ * Returns the last result of the debounced function
202
+ */
203
+ getLastResult(): ReturnType<TFn> | undefined {
204
+ return this._lastResult
205
+ }
206
+
207
+ /**
208
+ * Returns the number of times the function has been executed successfully
209
+ */
210
+ getSuccessCount(): number {
211
+ return this._successCount
212
+ }
213
+
214
+ /**
215
+ * Returns the number of times the function has settled (completed or errored)
216
+ */
217
+ getSettleCount(): number {
218
+ return this._settleCount
183
219
  }
184
220
 
185
221
  /**
186
- * Returns the number of times the function has been executed
222
+ * Returns the number of times the function has errored
187
223
  */
188
- getExecutionCount(): number {
189
- return this._executionCount
224
+ getErrorCount(): number {
225
+ return this._errorCount
190
226
  }
191
227
 
192
228
  /**
193
- * Returns `true` if there is a pending execution
229
+ * Returns `true` if there is a pending execution queued up for trailing execution
194
230
  */
195
231
  getIsPending(): boolean {
196
- return (
197
- this._options.enabled && (this._timeoutId !== null || this._isExecuting)
198
- )
232
+ return this._options.enabled && this._isPending
233
+ }
234
+
235
+ /**
236
+ * Returns `true` if there is currently an execution in progress
237
+ */
238
+ getIsExecuting(): boolean {
239
+ return this._isExecuting
199
240
  }
200
241
  }
201
242
 
@@ -216,10 +257,10 @@ export class AsyncDebouncer<
216
257
  * await debounced("third"); // Executes after 1s
217
258
  * ```
218
259
  */
219
- export function asyncDebounce<
220
- TFn extends AnyAsyncFunction,
221
- TArgs extends Parameters<TFn>,
222
- >(fn: TFn, initialOptions: Omit<AsyncDebouncerOptions<TFn, TArgs>, 'enabled'>) {
260
+ export function asyncDebounce<TFn extends AnyAsyncFunction>(
261
+ fn: TFn,
262
+ initialOptions: Omit<AsyncDebouncerOptions<TFn>, 'enabled'>,
263
+ ) {
223
264
  const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)
224
265
  return asyncDebouncer.maybeExecute.bind(asyncDebouncer)
225
266
  }
@@ -10,6 +10,16 @@ export interface AsyncQueuerOptions<TValue> {
10
10
  * Maximum number of concurrent tasks to process
11
11
  */
12
12
  concurrency?: number
13
+ /**
14
+ * Maximum time in milliseconds that an item can stay in the queue
15
+ * If not provided, items will never expire
16
+ */
17
+ expirationDuration?: number
18
+ /**
19
+ * Function to determine if an item has expired
20
+ * If provided, this overrides the expirationDuration behavior
21
+ */
22
+ getIsExpired?: (item: () => Promise<TValue>, addedAt: number) => boolean
13
23
  /**
14
24
  * Default position to get items from during processing
15
25
  * @default 'front'
@@ -49,7 +59,11 @@ export interface AsyncQueuerOptions<TValue> {
49
59
  */
50
60
  onReject?: (item: () => Promise<TValue>, queuer: AsyncQueuer<TValue>) => void
51
61
  /**
52
- * Whether the queuer should start processing tasks immediately
62
+ * Callback fired whenever an item expires in the queuer
63
+ */
64
+ onExpire?: (item: () => Promise<TValue>, queuer: AsyncQueuer<TValue>) => void
65
+ /**
66
+ * Whether the queuer should start processing tasks immediately or not.
53
67
  */
54
68
  started?: boolean
55
69
  /**
@@ -61,6 +75,8 @@ export interface AsyncQueuerOptions<TValue> {
61
75
  const defaultOptions: Required<AsyncQueuerOptions<any>> = {
62
76
  addItemsTo: 'back',
63
77
  concurrency: 1,
78
+ expirationDuration: Infinity,
79
+ getIsExpired: () => false,
64
80
  getItemsFrom: 'front',
65
81
  getPriority: (item) => (item as any)?.priority ?? 0,
66
82
  initialItems: [],
@@ -69,7 +85,8 @@ const defaultOptions: Required<AsyncQueuerOptions<any>> = {
69
85
  onIsRunningChange: () => {},
70
86
  onItemsChange: () => {},
71
87
  onReject: () => {},
72
- started: false,
88
+ onExpire: () => {},
89
+ started: true,
73
90
  wait: 0,
74
91
  }
75
92
 
@@ -83,6 +100,7 @@ const defaultOptions: Required<AsyncQueuerOptions<any>> = {
83
100
  * - FIFO (First In First Out) or LIFO (Last In First Out) queue behavior
84
101
  * - Pause/resume task processing
85
102
  * - Task cancellation
103
+ * - Item expiration to clear stale items from the queue
86
104
  *
87
105
  * Tasks are processed concurrently up to the configured concurrency limit. When a task completes,
88
106
  * the next pending task is processed if below the concurrency limit.
@@ -107,7 +125,9 @@ export class AsyncQueuer<TValue> {
107
125
  private _activeItems: Set<() => Promise<TValue>> = new Set()
108
126
  private _executionCount = 0
109
127
  private _rejectionCount = 0
128
+ private _expirationCount = 0
110
129
  private _items: Array<() => Promise<TValue>> = []
130
+ private _itemTimestamps: Array<number> = []
111
131
  private _onErrorCallbacks: Array<(error: Error) => void> = []
112
132
  private _onSettledCallbacks: Array<(result: TValue | Error) => void> = []
113
133
  private _onSuccessCallbacks: Array<(result: TValue) => void> = []
@@ -129,11 +149,8 @@ export class AsyncQueuer<TValue> {
129
149
  * Updates the queuer options
130
150
  * Returns the new options state
131
151
  */
132
- setOptions(
133
- newOptions: Partial<AsyncQueuerOptions<TValue>>,
134
- ): AsyncQueuerOptions<TValue> {
152
+ setOptions(newOptions: Partial<AsyncQueuerOptions<TValue>>): void {
135
153
  this._options = { ...this._options, ...newOptions }
136
- return this._options
137
154
  }
138
155
 
139
156
  /**
@@ -152,6 +169,9 @@ export class AsyncQueuer<TValue> {
152
169
  return
153
170
  }
154
171
 
172
+ // Check for expired items
173
+ this.checkExpiredItems()
174
+
155
175
  while (
156
176
  this._activeItems.size < this._options.concurrency &&
157
177
  !this.getIsEmpty()
@@ -196,6 +216,56 @@ export class AsyncQueuer<TValue> {
196
216
  this._pendingTick = false
197
217
  }
198
218
 
219
+ /**
220
+ * Checks for and removes expired items from the queuer
221
+ */
222
+ private checkExpiredItems() {
223
+ if (
224
+ this._options.expirationDuration === Infinity &&
225
+ this._options.getIsExpired === defaultOptions.getIsExpired
226
+ )
227
+ return
228
+
229
+ const now = Date.now()
230
+ const expiredIndices: Array<number> = []
231
+
232
+ // Find indices of expired items
233
+ for (let i = 0; i < this._items.length; i++) {
234
+ const timestamp = this._itemTimestamps[i]
235
+ if (timestamp === undefined) continue
236
+
237
+ const item = this._items[i]
238
+ if (item === undefined) continue
239
+
240
+ const isExpired =
241
+ this._options.getIsExpired !== defaultOptions.getIsExpired
242
+ ? this._options.getIsExpired(item, timestamp)
243
+ : now - timestamp > this._options.expirationDuration
244
+
245
+ if (isExpired) {
246
+ expiredIndices.push(i)
247
+ }
248
+ }
249
+
250
+ // Remove expired items from back to front to maintain indices
251
+ for (let i = expiredIndices.length - 1; i >= 0; i--) {
252
+ const index = expiredIndices[i]
253
+ if (index === undefined) continue
254
+
255
+ const expiredItem = this._items[index]
256
+ if (expiredItem === undefined) continue
257
+
258
+ this._items.splice(index, 1)
259
+ this._itemTimestamps.splice(index, 1)
260
+ this._expirationCount++
261
+ this._options.onExpire(expiredItem, this)
262
+ }
263
+
264
+ if (expiredIndices.length > 0) {
265
+ this._options.onItemsChange(this)
266
+ }
267
+ }
268
+
199
269
  /**
200
270
  * Starts the queuer and processes items
201
271
  */
@@ -295,15 +365,19 @@ export class AsyncQueuer<TValue> {
295
365
 
296
366
  if (insertIndex === -1) {
297
367
  this._items.push(task)
368
+ this._itemTimestamps.push(Date.now())
298
369
  } else {
299
370
  this._items.splice(insertIndex, 0, task)
371
+ this._itemTimestamps.splice(insertIndex, 0, Date.now())
300
372
  }
301
373
  } else {
302
374
  // Default FIFO/LIFO behavior
303
375
  if (position === 'front') {
304
376
  this._items.unshift(task)
377
+ this._itemTimestamps.unshift(Date.now())
305
378
  } else {
306
379
  this._items.push(task)
380
+ this._itemTimestamps.push(Date.now())
307
381
  }
308
382
  }
309
383
 
@@ -328,8 +402,10 @@ export class AsyncQueuer<TValue> {
328
402
 
329
403
  if (position === 'front') {
330
404
  item = this._items.shift()
405
+ this._itemTimestamps.shift()
331
406
  } else {
332
407
  item = this._items.pop()
408
+ this._itemTimestamps.pop()
333
409
  }
334
410
 
335
411
  if (item !== undefined) {
@@ -455,6 +531,13 @@ export class AsyncQueuer<TValue> {
455
531
  )
456
532
  }
457
533
  }
534
+
535
+ /**
536
+ * Returns the number of items that have expired from the queuer
537
+ */
538
+ getExpirationCount(): number {
539
+ return this._expirationCount
540
+ }
458
541
  }
459
542
 
460
543
  /**
@@ -474,7 +557,9 @@ export class AsyncQueuer<TValue> {
474
557
  * @param options - Configuration options for the AsyncQueuer
475
558
  * @returns A bound addItem function that can be used to add tasks to the queuer
476
559
  */
477
- export function asyncQueue<TValue>(options: AsyncQueuerOptions<TValue> = {}) {
478
- const queuer = new AsyncQueuer<TValue>({ ...options, started: true })
560
+ export function asyncQueue<TValue>(
561
+ options: Omit<AsyncQueuerOptions<TValue>, 'started'> = {},
562
+ ) {
563
+ const queuer = new AsyncQueuer<TValue>(options)
479
564
  return queuer.addItem.bind(queuer)
480
565
  }