@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
@@ -1,29 +1,9 @@
1
- /**
2
- * Information about a rate limit rejection
3
- */
4
- export interface RateLimitRejectionInfo {
5
- /**
6
- * Number of milliseconds until the next execution will be possible
7
- */
8
- msUntilNextWindow: number
9
- /**
10
- * Current number of executions in the window
11
- */
12
- currentExecutions: number
13
- /**
14
- * Maximum allowed executions per window
15
- */
16
- limit: number
17
- /**
18
- * Total number of rejections that have occurred
19
- */
20
- rejectionCount: number
21
- }
1
+ import type { AnyFunction } from './types'
22
2
 
23
3
  /**
24
4
  * Options for configuring a rate-limited function
25
5
  */
26
- export interface RateLimiterOptions {
6
+ export interface RateLimiterOptions<TFn extends AnyFunction> {
27
7
  /**
28
8
  * Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.
29
9
  * Defaults to true.
@@ -34,18 +14,24 @@ export interface RateLimiterOptions {
34
14
  */
35
15
  limit: number
36
16
  /**
37
- * Time window in milliseconds within which the limit applies
17
+ * Callback function that is called after the function is executed
38
18
  */
39
- window: number
19
+ onExecute?: (rateLimiter: RateLimiter<TFn>) => void
40
20
  /**
41
21
  * Optional callback function that is called when an execution is rejected due to rate limiting
42
22
  */
43
- onReject?: (info: RateLimitRejectionInfo) => void
23
+ onReject?: (rateLimiter: RateLimiter<TFn>) => void
24
+ /**
25
+ * Time window in milliseconds within which the limit applies
26
+ */
27
+ window: number
44
28
  }
45
29
 
46
- const defaultOptions: Required<Omit<RateLimiterOptions, 'onReject'>> = {
30
+ const defaultOptions: Required<RateLimiterOptions<any>> = {
47
31
  enabled: true,
48
32
  limit: 1,
33
+ onExecute: () => {},
34
+ onReject: () => {},
49
35
  window: 0,
50
36
  }
51
37
 
@@ -74,20 +60,17 @@ const defaultOptions: Required<Omit<RateLimiterOptions, 'onReject'>> = {
74
60
  * rateLimiter.maybeExecute('123');
75
61
  * ```
76
62
  */
77
- export class RateLimiter<
78
- TFn extends (...args: Array<any>) => any,
79
- TArgs extends Parameters<TFn>,
80
- > {
81
- private executionCount = 0
82
- private rejectionCount = 0
83
- private executionTimes: Array<number> = []
84
- private options: RateLimiterOptions
63
+ export class RateLimiter<TFn extends AnyFunction> {
64
+ private _executionCount = 0
65
+ private _rejectionCount = 0
66
+ private _executionTimes: Array<number> = []
67
+ private _options: RateLimiterOptions<TFn>
85
68
 
86
69
  constructor(
87
70
  private fn: TFn,
88
- initialOptions: RateLimiterOptions,
71
+ initialOptions: RateLimiterOptions<TFn>,
89
72
  ) {
90
- this.options = {
73
+ this._options = {
91
74
  ...defaultOptions,
92
75
  ...initialOptions,
93
76
  }
@@ -97,34 +80,15 @@ export class RateLimiter<
97
80
  * Updates the rate limiter options
98
81
  * Returns the new options state
99
82
  */
100
- setOptions(newOptions: Partial<RateLimiterOptions>): RateLimiterOptions {
101
- this.options = {
102
- ...this.options,
103
- ...newOptions,
104
- }
105
- return this.options
106
- }
107
-
108
- /**
109
- * Returns the number of times the function has been executed
110
- */
111
- getExecutionCount(): number {
112
- return this.executionCount
83
+ setOptions(newOptions: Partial<RateLimiterOptions<TFn>>): void {
84
+ this._options = { ...this._options, ...newOptions }
113
85
  }
114
86
 
115
87
  /**
116
- * Returns the number of times the function has been rejected
88
+ * Returns the current rate limiter options
117
89
  */
118
- getRejectionCount(): number {
119
- return this.rejectionCount
120
- }
121
-
122
- /**
123
- * Returns the number of remaining executions allowed in the current window
124
- */
125
- getRemainingInWindow(): number {
126
- this.cleanupOldExecutions()
127
- return Math.max(0, this.options.limit - this.executionTimes.length)
90
+ getOptions(): Required<RateLimiterOptions<TFn>> {
91
+ return this._options as Required<RateLimiterOptions<TFn>>
128
92
  }
129
93
 
130
94
  /**
@@ -142,10 +106,10 @@ export class RateLimiter<
142
106
  * rateLimiter.maybeExecute('arg1', 'arg2'); // false
143
107
  * ```
144
108
  */
145
- maybeExecute(...args: TArgs): boolean {
109
+ maybeExecute(...args: Parameters<TFn>): boolean {
146
110
  this.cleanupOldExecutions()
147
111
 
148
- if (this.executionTimes.length < this.options.limit) {
112
+ if (this._executionTimes.length < this._options.limit) {
149
113
  this.executeFunction(...args)
150
114
  return true
151
115
  }
@@ -155,45 +119,67 @@ export class RateLimiter<
155
119
  return false
156
120
  }
157
121
 
158
- private executeFunction(...args: TArgs): void {
159
- if (!this.options.enabled) return
122
+ private executeFunction(...args: Parameters<TFn>): void {
123
+ if (!this._options.enabled) return
160
124
  const now = Date.now()
161
- this.executionCount++
162
- this.executionTimes.push(now)
163
- this.fn(...args)
125
+ this._executionCount++
126
+ this._executionTimes.push(now)
127
+ this.fn(...args) // execute the function
128
+ this._options.onExecute?.(this)
164
129
  }
165
130
 
166
131
  private rejectFunction(): void {
167
- this.rejectionCount++
168
- if (this.options.onReject) {
169
- const oldestExecution = Math.min(...this.executionTimes)
170
- const msUntilNextWindow =
171
- oldestExecution + this.options.window - Date.now()
172
-
173
- this.options.onReject({
174
- msUntilNextWindow,
175
- currentExecutions: this.executionTimes.length,
176
- limit: this.options.limit,
177
- rejectionCount: this.rejectionCount,
178
- })
132
+ this._rejectionCount++
133
+ if (this._options.onReject) {
134
+ this._options.onReject(this)
179
135
  }
180
136
  }
181
137
 
182
138
  private cleanupOldExecutions(): void {
183
139
  const now = Date.now()
184
- const windowStart = now - this.options.window
185
- this.executionTimes = this.executionTimes.filter(
140
+ const windowStart = now - this._options.window
141
+ this._executionTimes = this._executionTimes.filter(
186
142
  (time) => time > windowStart,
187
143
  )
188
144
  }
189
145
 
146
+ /**
147
+ * Returns the number of times the function has been executed
148
+ */
149
+ getExecutionCount(): number {
150
+ return this._executionCount
151
+ }
152
+
153
+ /**
154
+ * Returns the number of times the function has been rejected
155
+ */
156
+ getRejectionCount(): number {
157
+ return this._rejectionCount
158
+ }
159
+
160
+ /**
161
+ * Returns the number of remaining executions allowed in the current window
162
+ */
163
+ getRemainingInWindow(): number {
164
+ this.cleanupOldExecutions()
165
+ return Math.max(0, this._options.limit - this._executionTimes.length)
166
+ }
167
+
168
+ /**
169
+ * Returns the number of milliseconds until the next execution will be possible
170
+ */
171
+ getMsUntilNextWindow(): number {
172
+ const oldestExecution = Math.min(...this._executionTimes)
173
+ return oldestExecution + this._options.window - Date.now()
174
+ }
175
+
190
176
  /**
191
177
  * Resets the rate limiter state
192
178
  */
193
179
  reset(): void {
194
- this.executionTimes = []
195
- this.executionCount = 0
196
- this.rejectionCount = 0
180
+ this._executionTimes = []
181
+ this._executionCount = 0
182
+ this._rejectionCount = 0
197
183
  }
198
184
  }
199
185
 
@@ -214,8 +200,8 @@ export class RateLimiter<
214
200
  * const rateLimited = rateLimit(makeApiCall, {
215
201
  * limit: 5,
216
202
  * window: 60000,
217
- * onReject: ({ msUntilNextWindow }) => {
218
- * console.log(`Rate limit exceeded. Try again in ${msUntilNextWindow}ms`);
203
+ * onReject: (rateLimiter) => {
204
+ * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);
219
205
  * }
220
206
  * });
221
207
  *
@@ -227,9 +213,9 @@ export class RateLimiter<
227
213
  * const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds
228
214
  * ```
229
215
  */
230
- export function rateLimit<TFn extends (...args: Array<any>) => any>(
216
+ export function rateLimit<TFn extends AnyFunction>(
231
217
  fn: TFn,
232
- initialOptions: Omit<RateLimiterOptions, 'enabled'>,
218
+ initialOptions: Omit<RateLimiterOptions<TFn>, 'enabled'>,
233
219
  ) {
234
220
  const rateLimiter = new RateLimiter(fn, initialOptions)
235
221
  return rateLimiter.maybeExecute.bind(rateLimiter)
package/src/throttler.ts CHANGED
@@ -1,7 +1,9 @@
1
+ import type { AnyFunction } from './types'
2
+
1
3
  /**
2
4
  * Options for configuring a throttled function
3
5
  */
4
- export interface ThrottlerOptions {
6
+ export interface ThrottlerOptions<TFn extends AnyFunction> {
5
7
  /**
6
8
  * Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.
7
9
  * Defaults to true.
@@ -12,6 +14,10 @@ export interface ThrottlerOptions {
12
14
  * Defaults to true.
13
15
  */
14
16
  leading?: boolean
17
+ /**
18
+ * Callback function that is called after the function is executed
19
+ */
20
+ onExecute?: (throttler: Throttler<TFn>) => void
15
21
  /**
16
22
  * Whether to execute on the trailing edge of the timeout.
17
23
  * Defaults to true.
@@ -23,9 +29,10 @@ export interface ThrottlerOptions {
23
29
  wait: number
24
30
  }
25
31
 
26
- const defaultOptions: Required<ThrottlerOptions> = {
32
+ const defaultOptions: Required<ThrottlerOptions<any>> = {
27
33
  enabled: true,
28
34
  leading: true,
35
+ onExecute: () => {},
29
36
  trailing: true,
30
37
  wait: 0,
31
38
  }
@@ -41,8 +48,7 @@ const defaultOptions: Required<ThrottlerOptions> = {
41
48
  * - Leading: Execute immediately on first call (default: true)
42
49
  * - Trailing: Execute after wait period if called during throttle (default: true)
43
50
  *
44
- * For rate limiting or hard API limits, consider using RateLimiter instead.
45
- * For collapsing rapid-fire events, consider using Debouncer.
51
+ * For collapsing rapid-fire events where you only care about the last call, consider using Debouncer.
46
52
  *
47
53
  * @example
48
54
  * ```ts
@@ -58,21 +64,18 @@ const defaultOptions: Required<ThrottlerOptions> = {
58
64
  * throttler.maybeExecute('123'); // Throttled
59
65
  * ```
60
66
  */
61
- export class Throttler<
62
- TFn extends (...args: Array<any>) => any,
63
- TArgs extends Parameters<TFn>,
64
- > {
65
- private executionCount = 0
66
- private lastArgs: TArgs | undefined
67
- private lastExecutionTime = 0
68
- private options: Required<ThrottlerOptions>
69
- private timeoutId: NodeJS.Timeout | undefined
67
+ export class Throttler<TFn extends AnyFunction> {
68
+ private _executionCount = 0
69
+ private _lastArgs: Parameters<TFn> | undefined
70
+ private _lastExecutionTime = 0
71
+ private _options: Required<ThrottlerOptions<TFn>>
72
+ private _timeoutId: NodeJS.Timeout | undefined
70
73
 
71
74
  constructor(
72
75
  private fn: TFn,
73
- initialOptions: ThrottlerOptions,
76
+ initialOptions: ThrottlerOptions<TFn>,
74
77
  ) {
75
- this.options = {
78
+ this._options = {
76
79
  ...defaultOptions,
77
80
  ...initialOptions,
78
81
  }
@@ -82,35 +85,20 @@ export class Throttler<
82
85
  * Updates the throttler options
83
86
  * Returns the new options state
84
87
  */
85
- setOptions(
86
- newOptions: Partial<ThrottlerOptions>,
87
- ): Required<ThrottlerOptions> {
88
- this.options = {
89
- ...this.options,
90
- ...newOptions,
91
- }
92
- return this.options
93
- }
94
-
95
- /**
96
- * Returns the number of times the function has been executed
97
- */
98
- getExecutionCount(): number {
99
- return this.executionCount
100
- }
88
+ setOptions(newOptions: Partial<ThrottlerOptions<TFn>>): void {
89
+ this._options = { ...this._options, ...newOptions }
101
90
 
102
- /**
103
- * Returns the last execution time
104
- */
105
- getLastExecutionTime(): number {
106
- return this.lastExecutionTime
91
+ // End the pending state if the debouncer is disabled
92
+ if (!this._options.enabled) {
93
+ this.cancel()
94
+ }
107
95
  }
108
96
 
109
97
  /**
110
- * Returns the next execution time
98
+ * Returns the current throttler options
111
99
  */
112
- getNextExecutionTime(): number {
113
- return this.lastExecutionTime + this.options.wait
100
+ getOptions(): Required<ThrottlerOptions<TFn>> {
101
+ return this._options
114
102
  }
115
103
 
116
104
  /**
@@ -135,38 +123,40 @@ export class Throttler<
135
123
  * throttled.maybeExecute('c', 'd');
136
124
  * ```
137
125
  */
138
- maybeExecute(...args: TArgs): void {
126
+ maybeExecute(...args: Parameters<TFn>): void {
139
127
  const now = Date.now()
140
- const timeSinceLastExecution = now - this.lastExecutionTime
128
+ const timeSinceLastExecution = now - this._lastExecutionTime
141
129
 
142
130
  // Handle leading execution
143
- if (timeSinceLastExecution >= this.options.wait) {
144
- if (this.options.leading) {
145
- this.executeFunction(...args)
146
- }
147
- this.lastExecutionTime = now
131
+ if (this._options.leading && timeSinceLastExecution >= this._options.wait) {
132
+ this.executeFunction(...args)
148
133
  } else {
149
134
  // Store the most recent arguments for potential trailing execution
150
- this.lastArgs = args
135
+ this._lastArgs = args
151
136
 
152
137
  // Set up trailing execution if not already scheduled
153
- if (!this.timeoutId && this.options.trailing) {
154
- this.timeoutId = setTimeout(() => {
155
- if (this.lastArgs) {
156
- this.executeFunction(...this.lastArgs)
157
- this.lastArgs = undefined
138
+ if (!this._timeoutId && this._options.trailing) {
139
+ const _timeSinceLastExecution = this._lastExecutionTime
140
+ ? now - this._lastExecutionTime
141
+ : 0
142
+ const timeoutDuration = this._options.wait - _timeSinceLastExecution
143
+ this._timeoutId = setTimeout(() => {
144
+ if (this._lastArgs !== undefined) {
145
+ this.executeFunction(...this._lastArgs)
158
146
  }
159
- this.lastExecutionTime = Date.now()
160
- this.timeoutId = undefined
161
- }, this.options.wait - timeSinceLastExecution)
147
+ }, timeoutDuration)
162
148
  }
163
149
  }
164
150
  }
165
151
 
166
- private executeFunction(...args: TArgs): void {
167
- if (!this.options.enabled) return
168
- this.executionCount++
169
- this.fn(...args)
152
+ private executeFunction(...args: Parameters<TFn>): void {
153
+ if (!this._options.enabled) return
154
+ this.fn(...args) // EXECUTE!
155
+ this._executionCount++
156
+ this._lastExecutionTime = Date.now()
157
+ this._timeoutId = undefined
158
+ this._lastArgs = undefined
159
+ this._options.onExecute(this)
170
160
  }
171
161
 
172
162
  /**
@@ -179,12 +169,40 @@ export class Throttler<
179
169
  * Has no effect if there is no pending execution.
180
170
  */
181
171
  cancel(): void {
182
- if (this.timeoutId) {
183
- clearTimeout(this.timeoutId)
184
- this.timeoutId = undefined
185
- this.lastArgs = undefined
172
+ if (this._timeoutId) {
173
+ clearTimeout(this._timeoutId)
174
+ this._timeoutId = undefined
175
+ this._lastArgs = undefined
186
176
  }
187
177
  }
178
+
179
+ /**
180
+ * Returns the last execution time
181
+ */
182
+ getLastExecutionTime(): number {
183
+ return this._lastExecutionTime
184
+ }
185
+
186
+ /**
187
+ * Returns the next execution time
188
+ */
189
+ getNextExecutionTime(): number {
190
+ return this._lastExecutionTime + this._options.wait
191
+ }
192
+
193
+ /**
194
+ * Returns the number of times the function has been executed
195
+ */
196
+ getExecutionCount(): number {
197
+ return this._executionCount
198
+ }
199
+
200
+ /**
201
+ * Returns `true` if there is a pending execution
202
+ */
203
+ getIsPending(): boolean {
204
+ return this._options.enabled && !!this._timeoutId
205
+ }
188
206
  }
189
207
 
190
208
  /**
@@ -213,9 +231,9 @@ export class Throttler<
213
231
  * });
214
232
  * ```
215
233
  */
216
- export function throttle<TFn extends (...args: Array<any>) => any>(
234
+ export function throttle<TFn extends AnyFunction>(
217
235
  fn: TFn,
218
- initialOptions: Omit<ThrottlerOptions, 'enabled'>,
236
+ initialOptions: Omit<ThrottlerOptions<TFn>, 'enabled'>,
219
237
  ) {
220
238
  const throttler = new Throttler(fn, initialOptions)
221
239
  return throttler.maybeExecute.bind(throttler)
package/src/types.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Represents a function that can be called with any arguments and returns any value.
3
+ */
4
+ export type AnyFunction = (...args: Array<any>) => any
5
+
6
+ /**
7
+ * Represents an asynchronous function that can be called with any arguments and returns a promise.
8
+ */
9
+ export type AnyAsyncFunction = (...args: Array<any>) => Promise<any>
package/src/utils.ts ADDED
@@ -0,0 +1,13 @@
1
+ export function bindInstanceMethods<T extends Record<string, any>>(
2
+ instance: T,
3
+ ): T {
4
+ return Object.getOwnPropertyNames(Object.getPrototypeOf(instance))
5
+ .filter((key) => typeof instance[key as keyof T] === 'function')
6
+ .reduce((acc: any, key) => {
7
+ const method = instance[key as keyof T]
8
+ if (typeof method === 'function') {
9
+ acc[key] = method.bind(instance)
10
+ }
11
+ return acc
12
+ }, {} as T)
13
+ }