@tanstack/pacer 0.1.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 (74) hide show
  1. package/dist/cjs/async-debouncer.cjs +112 -63
  2. package/dist/cjs/async-debouncer.cjs.map +1 -1
  3. package/dist/cjs/async-debouncer.d.cts +66 -25
  4. package/dist/cjs/async-queuer.cjs +198 -124
  5. package/dist/cjs/async-queuer.cjs.map +1 -1
  6. package/dist/cjs/async-queuer.d.cts +91 -49
  7. package/dist/cjs/async-rate-limiter.cjs +83 -55
  8. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  9. package/dist/cjs/async-rate-limiter.d.cts +55 -28
  10. package/dist/cjs/async-throttler.cjs +121 -70
  11. package/dist/cjs/async-throttler.cjs.map +1 -1
  12. package/dist/cjs/async-throttler.d.cts +75 -25
  13. package/dist/cjs/debouncer.cjs +45 -23
  14. package/dist/cjs/debouncer.cjs.map +1 -1
  15. package/dist/cjs/debouncer.d.cts +27 -12
  16. package/dist/cjs/index.cjs +2 -0
  17. package/dist/cjs/index.cjs.map +1 -1
  18. package/dist/cjs/index.d.cts +2 -0
  19. package/dist/cjs/queuer.cjs +161 -101
  20. package/dist/cjs/queuer.cjs.map +1 -1
  21. package/dist/cjs/queuer.d.cts +80 -38
  22. package/dist/cjs/rate-limiter.cjs +52 -44
  23. package/dist/cjs/rate-limiter.cjs.map +1 -1
  24. package/dist/cjs/rate-limiter.d.cts +38 -46
  25. package/dist/cjs/throttler.cjs +57 -44
  26. package/dist/cjs/throttler.cjs.map +1 -1
  27. package/dist/cjs/throttler.d.cts +35 -23
  28. package/dist/cjs/types.d.cts +8 -0
  29. package/dist/cjs/utils.cjs +13 -0
  30. package/dist/cjs/utils.cjs.map +1 -0
  31. package/dist/cjs/utils.d.cts +1 -0
  32. package/dist/esm/async-debouncer.d.ts +66 -25
  33. package/dist/esm/async-debouncer.js +112 -63
  34. package/dist/esm/async-debouncer.js.map +1 -1
  35. package/dist/esm/async-queuer.d.ts +91 -49
  36. package/dist/esm/async-queuer.js +198 -124
  37. package/dist/esm/async-queuer.js.map +1 -1
  38. package/dist/esm/async-rate-limiter.d.ts +55 -28
  39. package/dist/esm/async-rate-limiter.js +83 -55
  40. package/dist/esm/async-rate-limiter.js.map +1 -1
  41. package/dist/esm/async-throttler.d.ts +75 -25
  42. package/dist/esm/async-throttler.js +121 -70
  43. package/dist/esm/async-throttler.js.map +1 -1
  44. package/dist/esm/debouncer.d.ts +27 -12
  45. package/dist/esm/debouncer.js +45 -23
  46. package/dist/esm/debouncer.js.map +1 -1
  47. package/dist/esm/index.d.ts +2 -0
  48. package/dist/esm/index.js +2 -0
  49. package/dist/esm/index.js.map +1 -1
  50. package/dist/esm/queuer.d.ts +80 -38
  51. package/dist/esm/queuer.js +161 -101
  52. package/dist/esm/queuer.js.map +1 -1
  53. package/dist/esm/rate-limiter.d.ts +38 -46
  54. package/dist/esm/rate-limiter.js +52 -44
  55. package/dist/esm/rate-limiter.js.map +1 -1
  56. package/dist/esm/throttler.d.ts +35 -23
  57. package/dist/esm/throttler.js +57 -44
  58. package/dist/esm/throttler.js.map +1 -1
  59. package/dist/esm/types.d.ts +8 -0
  60. package/dist/esm/utils.d.ts +1 -0
  61. package/dist/esm/utils.js +13 -0
  62. package/dist/esm/utils.js.map +1 -0
  63. package/package.json +8 -1
  64. package/src/async-debouncer.ts +157 -88
  65. package/src/async-queuer.ts +266 -148
  66. package/src/async-rate-limiter.ts +123 -83
  67. package/src/async-throttler.ts +173 -89
  68. package/src/debouncer.ts +71 -42
  69. package/src/index.ts +2 -0
  70. package/src/queuer.ts +219 -114
  71. package/src/rate-limiter.ts +74 -88
  72. package/src/throttler.ts +83 -65
  73. package/src/types.ts +9 -0
  74. package/src/utils.ts +13 -0
@@ -0,0 +1,13 @@
1
+ function bindInstanceMethods(instance) {
2
+ return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).filter((key) => typeof instance[key] === "function").reduce((acc, key) => {
3
+ const method = instance[key];
4
+ if (typeof method === "function") {
5
+ acc[key] = method.bind(instance);
6
+ }
7
+ return acc;
8
+ }, {});
9
+ }
10
+ export {
11
+ bindInstanceMethods
12
+ };
13
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
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.1.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",
@@ -127,6 +127,13 @@
127
127
  "default": "./dist/cjs/throttler.cjs"
128
128
  }
129
129
  },
130
+ "./types": {
131
+ "types": "./dist/esm/types.d.ts"
132
+ },
133
+ "./utils": {
134
+ "types": "./dist/esm/utils.d.ts",
135
+ "default": "./dist/esm/utils.js"
136
+ },
130
137
  "./package.json": "./package.json"
131
138
  },
132
139
  "sideEffects": false,
@@ -1,7 +1,9 @@
1
+ import type { AnyAsyncFunction } from './types'
2
+
1
3
  /**
2
4
  * Options for configuring an async debounced function
3
5
  */
4
- export interface AsyncDebouncerOptions {
6
+ export interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {
5
7
  /**
6
8
  * Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.
7
9
  * Defaults to true.
@@ -12,6 +14,18 @@ export interface AsyncDebouncerOptions {
12
14
  * Defaults to false.
13
15
  */
14
16
  leading?: boolean
17
+ /**
18
+ * Optional error handler for when the debounced function throws
19
+ */
20
+ onError?: (error: unknown, debouncer: AsyncDebouncer<TFn>) => void
21
+ /**
22
+ * Optional callback to call when the debounced function is executed
23
+ */
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
15
29
  /**
16
30
  * Whether to execute on the trailing edge of the timeout.
17
31
  * Defaults to true.
@@ -22,17 +36,15 @@ export interface AsyncDebouncerOptions {
22
36
  * Defaults to 0ms
23
37
  */
24
38
  wait: number
25
- /**
26
- * Optional error handler for when the debounced function throws
27
- */
28
- onError?: (error: unknown) => void
29
39
  }
30
40
 
31
- const defaultOptions: Required<AsyncDebouncerOptions> = {
41
+ const defaultOptions: Required<AsyncDebouncerOptions<any>> = {
32
42
  enabled: true,
33
43
  leading: false,
34
- trailing: true,
35
44
  onError: () => {},
45
+ onSettled: () => {},
46
+ onSuccess: () => {},
47
+ trailing: true,
36
48
  wait: 0,
37
49
  }
38
50
 
@@ -48,33 +60,34 @@ const defaultOptions: Required<AsyncDebouncerOptions> = {
48
60
  *
49
61
  * @example
50
62
  * ```ts
51
- * const debouncer = new AsyncDebouncer(async (value: string) => {
63
+ * const asyncDebouncer = new AsyncDebouncer(async (value: string) => {
52
64
  * await searchAPI(value);
53
65
  * }, { wait: 500 });
54
66
  *
55
67
  * // Called on each keystroke but only executes after 500ms of no typing
56
68
  * inputElement.addEventListener('input', () => {
57
- * debouncer.maybeExecute(inputElement.value);
69
+ * asyncDebouncer.maybeExecute(inputElement.value);
58
70
  * });
59
71
  * ```
60
72
  */
61
- export class AsyncDebouncer<
62
- TFn extends (...args: Array<any>) => Promise<any>,
63
- TArgs extends Parameters<TFn>,
64
- > {
65
- private abortController: AbortController | null = null
66
- private executionCount = 0
67
- private isExecuting = false
68
- private lastArgs: TArgs | undefined
69
- private options: Required<AsyncDebouncerOptions>
70
- private timeoutId: ReturnType<typeof setTimeout> | null = null
71
- private canLeadingExecute = true
73
+ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
74
+ private _abortController: AbortController | null = null
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
72
85
 
73
86
  constructor(
74
87
  private fn: TFn,
75
- initialOptions: AsyncDebouncerOptions,
88
+ initialOptions: AsyncDebouncerOptions<TFn>,
76
89
  ) {
77
- this.options = {
90
+ this._options = {
78
91
  ...defaultOptions,
79
92
  ...initialOptions,
80
93
  }
@@ -84,91 +97,146 @@ export class AsyncDebouncer<
84
97
  * Updates the debouncer options
85
98
  * Returns the new options state
86
99
  */
87
- setOptions(
88
- newOptions: Partial<AsyncDebouncerOptions>,
89
- ): Required<AsyncDebouncerOptions> {
90
- this.options = {
91
- ...this.options,
92
- ...newOptions,
93
- }
94
- return this.options
95
- }
100
+ setOptions(newOptions: Partial<AsyncDebouncerOptions<TFn>>): void {
101
+ this._options = { ...this._options, ...newOptions }
96
102
 
97
- /**
98
- * Returns the number of times the function has been executed
99
- */
100
- getExecutionCount(): number {
101
- return this.executionCount
103
+ // End the pending state if the debouncer is disabled
104
+ if (!this._options.enabled) {
105
+ this._isPending = false
106
+ }
102
107
  }
103
108
 
104
109
  /**
105
- * Cancels any pending execution
110
+ * Returns the current debouncer options
106
111
  */
107
- cancel(): void {
108
- if (this.timeoutId) {
109
- clearTimeout(this.timeoutId)
110
- this.timeoutId = null
111
- }
112
- if (this.abortController) {
113
- this.abortController.abort()
114
- this.abortController = null
115
- }
116
- this.lastArgs = undefined
117
- this.canLeadingExecute = true
112
+ getOptions(): Required<AsyncDebouncerOptions<TFn>> {
113
+ return this._options
118
114
  }
119
115
 
120
116
  /**
121
117
  * Attempts to execute the debounced function
122
118
  * If a call is already in progress, it will be queued
123
119
  */
124
- async maybeExecute(...args: TArgs): Promise<void> {
125
- this.cancel()
126
- this.lastArgs = args
120
+ async maybeExecute(
121
+ ...args: Parameters<TFn>
122
+ ): Promise<ReturnType<TFn> | undefined> {
123
+ this._cancel()
124
+ this._lastArgs = args
127
125
 
128
126
  // Handle leading execution
129
- if (this.options.leading && this.canLeadingExecute) {
130
- this.canLeadingExecute = false
127
+ if (this._options.leading && this._canLeadingExecute) {
128
+ this._canLeadingExecute = false
131
129
  await this.executeFunction(...args)
130
+ return this._lastResult
131
+ }
132
+
133
+ // Handle trailing execution
134
+ if (this._options.trailing) {
135
+ this._isPending = true
132
136
  }
133
137
 
134
138
  return new Promise((resolve) => {
135
- this.timeoutId = setTimeout(async () => {
136
- if (this.isExecuting) {
137
- resolve()
138
- return
139
+ this._timeoutId = setTimeout(async () => {
140
+ // Execute trailing if enabled
141
+ if (this._options.trailing && this._lastArgs) {
142
+ await this.executeFunction(...this._lastArgs)
139
143
  }
140
144
 
141
- this.canLeadingExecute = true
142
- // Execute trailing only if enabled
143
- if (this.options.trailing) {
144
- this.abortController = new AbortController()
145
- try {
146
- this.isExecuting = true
147
- if (this.lastArgs) {
148
- await this.executeFunction(...this.lastArgs)
149
- }
150
- } catch (error) {
151
- try {
152
- this.options.onError(error)
153
- } catch {
154
- // Ignore errors from error handler
155
- }
156
- } finally {
157
- this.isExecuting = false
158
- this.abortController = null
159
- resolve()
160
- }
161
- } else {
162
- resolve()
163
- }
164
- }, this.options.wait)
145
+ // Reset state and resolve
146
+ this._canLeadingExecute = true
147
+ resolve(this._lastResult)
148
+ }, this._options.wait)
165
149
  })
166
150
  }
167
151
 
168
- private async executeFunction(...args: TArgs): Promise<void> {
169
- if (!this.options.enabled) return
170
- this.executionCount++
171
- await this.fn(...args)
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
173
+ }
174
+
175
+ /**
176
+ * Cancel without resetting _canLeadingExecute
177
+ */
178
+ private _cancel(): void {
179
+ if (this._timeoutId) {
180
+ clearTimeout(this._timeoutId)
181
+ this._timeoutId = null
182
+ }
183
+ if (this._abortController) {
184
+ this._abortController.abort()
185
+ this._abortController = null
186
+ }
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 {
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
219
+ }
220
+
221
+ /**
222
+ * Returns the number of times the function has errored
223
+ */
224
+ getErrorCount(): number {
225
+ return this._errorCount
226
+ }
227
+
228
+ /**
229
+ * Returns `true` if there is a pending execution queued up for trailing execution
230
+ */
231
+ getIsPending(): boolean {
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
172
240
  }
173
241
  }
174
242
 
@@ -189,9 +257,10 @@ export class AsyncDebouncer<
189
257
  * await debounced("third"); // Executes after 1s
190
258
  * ```
191
259
  */
192
- export function asyncDebounce<
193
- TFn extends (...args: Array<any>) => Promise<any>,
194
- >(fn: TFn, initialOptions: Omit<AsyncDebouncerOptions, 'enabled'>) {
260
+ export function asyncDebounce<TFn extends AnyAsyncFunction>(
261
+ fn: TFn,
262
+ initialOptions: Omit<AsyncDebouncerOptions<TFn>, 'enabled'>,
263
+ ) {
195
264
  const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)
196
265
  return asyncDebouncer.maybeExecute.bind(asyncDebouncer)
197
266
  }