@tanstack/pacer 0.1.0 → 0.2.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 +60 -44
  2. package/dist/cjs/async-debouncer.cjs.map +1 -1
  3. package/dist/cjs/async-debouncer.d.cts +37 -24
  4. package/dist/cjs/async-queuer.cjs +149 -125
  5. package/dist/cjs/async-queuer.cjs.map +1 -1
  6. package/dist/cjs/async-queuer.d.cts +65 -48
  7. package/dist/cjs/async-rate-limiter.cjs +63 -46
  8. package/dist/cjs/async-rate-limiter.cjs.map +1 -1
  9. package/dist/cjs/async-rate-limiter.d.cts +39 -27
  10. package/dist/cjs/async-throttler.cjs +70 -47
  11. package/dist/cjs/async-throttler.cjs.map +1 -1
  12. package/dist/cjs/async-throttler.d.cts +43 -25
  13. package/dist/cjs/debouncer.cjs +46 -22
  14. package/dist/cjs/debouncer.cjs.map +1 -1
  15. package/dist/cjs/debouncer.d.cts +25 -11
  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 +114 -104
  20. package/dist/cjs/queuer.cjs.map +1 -1
  21. package/dist/cjs/queuer.d.cts +53 -40
  22. package/dist/cjs/rate-limiter.cjs +54 -42
  23. package/dist/cjs/rate-limiter.cjs.map +1 -1
  24. package/dist/cjs/rate-limiter.d.cts +37 -45
  25. package/dist/cjs/throttler.cjs +61 -41
  26. package/dist/cjs/throttler.cjs.map +1 -1
  27. package/dist/cjs/throttler.d.cts +35 -22
  28. package/dist/cjs/types.d.cts +12 -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 +37 -24
  33. package/dist/esm/async-debouncer.js +60 -44
  34. package/dist/esm/async-debouncer.js.map +1 -1
  35. package/dist/esm/async-queuer.d.ts +65 -48
  36. package/dist/esm/async-queuer.js +149 -125
  37. package/dist/esm/async-queuer.js.map +1 -1
  38. package/dist/esm/async-rate-limiter.d.ts +39 -27
  39. package/dist/esm/async-rate-limiter.js +63 -46
  40. package/dist/esm/async-rate-limiter.js.map +1 -1
  41. package/dist/esm/async-throttler.d.ts +43 -25
  42. package/dist/esm/async-throttler.js +70 -47
  43. package/dist/esm/async-throttler.js.map +1 -1
  44. package/dist/esm/debouncer.d.ts +25 -11
  45. package/dist/esm/debouncer.js +46 -22
  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 +53 -40
  51. package/dist/esm/queuer.js +114 -104
  52. package/dist/esm/queuer.js.map +1 -1
  53. package/dist/esm/rate-limiter.d.ts +37 -45
  54. package/dist/esm/rate-limiter.js +54 -42
  55. package/dist/esm/rate-limiter.js.map +1 -1
  56. package/dist/esm/throttler.d.ts +35 -22
  57. package/dist/esm/throttler.js +61 -41
  58. package/dist/esm/throttler.js.map +1 -1
  59. package/dist/esm/types.d.ts +12 -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 +90 -62
  65. package/src/async-queuer.ts +178 -145
  66. package/src/async-rate-limiter.ts +93 -67
  67. package/src/async-throttler.ts +98 -63
  68. package/src/debouncer.ts +71 -35
  69. package/src/index.ts +2 -0
  70. package/src/queuer.ts +135 -118
  71. package/src/rate-limiter.ts +79 -81
  72. package/src/throttler.ts +87 -61
  73. package/src/types.ts +17 -0
  74. package/src/utils.ts +13 -0
package/src/index.ts CHANGED
@@ -7,3 +7,5 @@ export * from './debouncer'
7
7
  export * from './queuer'
8
8
  export * from './rate-limiter'
9
9
  export * from './throttler'
10
+ export * from './types'
11
+ export * from './utils'
package/src/queuer.ts CHANGED
@@ -29,10 +29,18 @@ export interface QueuerOptions<TValue> {
29
29
  * Callback fired whenever an item is removed from the queuer
30
30
  */
31
31
  onGetNextItem?: (item: TValue, queuer: Queuer<TValue>) => void
32
+ /**
33
+ * Callback fired whenever the queuer's running state changes
34
+ */
35
+ onIsRunningChange?: (queuer: Queuer<TValue>) => void
32
36
  /**
33
37
  * Callback fired whenever an item is added or removed from the queuer
34
38
  */
35
- onUpdate?: (queuer: Queuer<TValue>) => void
39
+ onItemsChange?: (queuer: Queuer<TValue>) => void
40
+ /**
41
+ * Callback fired whenever an item is rejected from being added to the queuer
42
+ */
43
+ onReject?: (item: TValue, queuer: Queuer<TValue>) => void
36
44
  /**
37
45
  * Whether the queuer should start processing tasks immediately
38
46
  */
@@ -46,11 +54,13 @@ export interface QueuerOptions<TValue> {
46
54
  const defaultOptions: Required<QueuerOptions<any>> = {
47
55
  addItemsTo: 'back',
48
56
  getItemsFrom: 'front',
49
- getPriority: () => 0,
57
+ getPriority: (item) => item?.priority ?? 0,
50
58
  initialItems: [],
51
59
  maxSize: Infinity,
52
60
  onGetNextItem: () => {},
53
- onUpdate: () => {},
61
+ onIsRunningChange: () => {},
62
+ onItemsChange: () => {},
63
+ onReject: () => {},
54
64
  started: false,
55
65
  wait: 0,
56
66
  }
@@ -87,7 +97,7 @@ export type QueuePosition = 'front' | 'back'
87
97
  * - start(): begins processing items in the queuer
88
98
  * - stop(): pauses processing
89
99
  * - wait: configurable delay between processing items
90
- * - onUpdate/onGetNextItem: callbacks for monitoring queuer state
100
+ * - onItemsChange/onGetNextItem: callbacks for monitoring queuer state
91
101
  *
92
102
  * @example
93
103
  * ```ts
@@ -102,7 +112,7 @@ export type QueuePosition = 'front' | 'back'
102
112
  * getPriority: (n) => n, // Higher numbers have priority
103
113
  * started: true, // Begin processing immediately
104
114
  * wait: 1000, // Wait 1s between items
105
- * onGetNextItem: (item) => console.log(item)
115
+ * onGetNextItem: (item, queuer) => console.log(item)
106
116
  * });
107
117
  * priorityQueue.addItem(1); // [1]
108
118
  * priorityQueue.addItem(3); // [3, 1] - 3 processed first
@@ -110,59 +120,108 @@ export type QueuePosition = 'front' | 'back'
110
120
  * ```
111
121
  */
112
122
  export class Queuer<TValue> {
113
- protected options: Required<QueuerOptions<TValue>>
114
- private items: Array<TValue> = []
115
- private executionCount = 0
116
- private onUpdates: Array<(item: TValue) => void> = []
117
- private running: boolean
118
- private pendingTick = false
123
+ private _options: Required<QueuerOptions<TValue>>
124
+ private _items: Array<TValue> = []
125
+ private _executionCount = 0
126
+ private _rejectionCount = 0
127
+ private _onItemsChanges: Array<(item: TValue) => void> = []
128
+ private _running: boolean
129
+ private _pendingTick = false
119
130
 
120
131
  constructor(initialOptions: QueuerOptions<TValue> = defaultOptions) {
121
- this.options = { ...defaultOptions, ...initialOptions }
122
- this.running = this.options.started
132
+ this._options = { ...defaultOptions, ...initialOptions }
133
+ this._running = this._options.started
123
134
 
124
- for (let i = 0; i < this.options.initialItems.length; i++) {
125
- const item = this.options.initialItems[i]!
126
- const isLast = i === this.options.initialItems.length - 1
127
- this.addItem(item, this.options.addItemsTo, isLast)
135
+ for (let i = 0; i < this._options.initialItems.length; i++) {
136
+ const item = this._options.initialItems[i]!
137
+ const isLast = i === this._options.initialItems.length - 1
138
+ this.addItem(item, this._options.addItemsTo, isLast)
128
139
  }
129
140
  }
130
141
 
142
+ /**
143
+ * Updates the queuer options
144
+ * Returns the new options state
145
+ */
146
+ setOptions(
147
+ newOptions: Partial<QueuerOptions<TValue>>,
148
+ ): QueuerOptions<TValue> {
149
+ this._options = { ...this._options, ...newOptions }
150
+ return this._options
151
+ }
152
+
153
+ /**
154
+ * Returns the current queuer options
155
+ */
156
+ getOptions(): Required<QueuerOptions<TValue>> {
157
+ return this._options
158
+ }
159
+
131
160
  /**
132
161
  * Processes items in the queuer
133
162
  */
134
- protected tick() {
135
- if (!this.running) {
136
- this.pendingTick = false
163
+ private tick() {
164
+ if (!this._running) {
165
+ this._pendingTick = false
137
166
  return
138
167
  }
139
- while (!this.isEmpty()) {
140
- const nextItem = this.getNextItem(this.options.getItemsFrom)
168
+ while (!this.getIsEmpty()) {
169
+ const nextItem = this.getNextItem(this._options.getItemsFrom)
141
170
  if (nextItem === undefined) {
142
171
  break
143
172
  }
144
- this.onUpdates.forEach((cb) => cb(nextItem))
173
+ this._onItemsChanges.forEach((cb) => cb(nextItem))
145
174
 
146
- if (this.options.wait > 0) {
175
+ if (this._options.wait > 0) {
147
176
  // Use setTimeout to wait before processing next item
148
- setTimeout(() => this.tick(), this.options.wait)
177
+ setTimeout(() => this.tick(), this._options.wait)
149
178
  return
150
179
  }
151
180
 
152
181
  this.tick()
153
182
  }
154
- this.pendingTick = false
183
+ this._pendingTick = false
155
184
  }
156
185
 
157
186
  /**
158
- * Updates the queuer options
159
- * Returns the new options state
187
+ * Stops the queuer from processing items
160
188
  */
161
- setOptions(
162
- newOptions: Partial<QueuerOptions<TValue>>,
163
- ): QueuerOptions<TValue> {
164
- this.options = { ...this.options, ...newOptions }
165
- return this.options
189
+ stop() {
190
+ this._running = false
191
+ this._pendingTick = false
192
+ this._options.onIsRunningChange(this)
193
+ }
194
+
195
+ /**
196
+ * Starts the queuer and processes items
197
+ */
198
+ start() {
199
+ this._running = true
200
+ if (!this._pendingTick && !this.getIsEmpty()) {
201
+ this._pendingTick = true
202
+ this.tick()
203
+ }
204
+ this._options.onIsRunningChange(this)
205
+ }
206
+
207
+ /**
208
+ * Removes all items from the queuer
209
+ */
210
+ clear(): void {
211
+ this._items = []
212
+ this._options.onItemsChange(this)
213
+ }
214
+
215
+ /**
216
+ * Resets the queuer to its initial state
217
+ */
218
+ reset(withInitialItems?: boolean): void {
219
+ this.clear()
220
+ this._executionCount = 0
221
+ if (withInitialItems) {
222
+ this._items = [...this._options.initialItems]
223
+ }
224
+ this._running = this._options.started
166
225
  }
167
226
 
168
227
  /**
@@ -171,40 +230,42 @@ export class Queuer<TValue> {
171
230
  */
172
231
  addItem(
173
232
  item: TValue,
174
- position: QueuePosition = this.options.addItemsTo,
233
+ position: QueuePosition = this._options.addItemsTo,
175
234
  runOnUpdate: boolean = true,
176
235
  ): boolean {
177
- if (this.isFull()) {
236
+ if (this.getIsFull()) {
237
+ this._rejectionCount++
238
+ this._options.onReject(item, this)
178
239
  return false
179
240
  }
180
241
 
181
- if (this.options.getPriority !== defaultOptions.getPriority) {
242
+ if (this._options.getPriority !== defaultOptions.getPriority) {
182
243
  // If custom priority function is provided, insert based on priority
183
- const priority = this.options.getPriority(item)
184
- const insertIndex = this.items.findIndex(
185
- (existing) => this.options.getPriority(existing) > priority,
244
+ const priority = this._options.getPriority(item)
245
+ const insertIndex = this._items.findIndex(
246
+ (existing) => this._options.getPriority(existing) > priority,
186
247
  )
187
248
 
188
249
  if (insertIndex === -1) {
189
- this.items.push(item)
250
+ this._items.push(item)
190
251
  } else {
191
- this.items.splice(insertIndex, 0, item)
252
+ this._items.splice(insertIndex, 0, item)
192
253
  }
193
254
  } else {
194
255
  // Default FIFO/LIFO behavior
195
256
  if (position === 'front') {
196
- this.items.unshift(item)
257
+ this._items.unshift(item)
197
258
  } else {
198
- this.items.push(item)
259
+ this._items.push(item)
199
260
  }
200
261
  }
201
262
 
202
- if (this.running && !this.pendingTick) {
203
- this.pendingTick = true
263
+ if (this._running && !this._pendingTick) {
264
+ this._pendingTick = true
204
265
  this.tick()
205
266
  }
206
267
  if (runOnUpdate) {
207
- this.options.onUpdate(this)
268
+ this._options.onItemsChange(this)
208
269
  }
209
270
  return true
210
271
  }
@@ -221,20 +282,20 @@ export class Queuer<TValue> {
221
282
  * ```
222
283
  */
223
284
  getNextItem(
224
- position: QueuePosition = this.options.getItemsFrom,
285
+ position: QueuePosition = this._options.getItemsFrom,
225
286
  ): TValue | undefined {
226
287
  let item: TValue | undefined
227
288
 
228
289
  if (position === 'front') {
229
- item = this.items.shift()
290
+ item = this._items.shift()
230
291
  } else {
231
- item = this.items.pop()
292
+ item = this._items.pop()
232
293
  }
233
294
 
234
295
  if (item !== undefined) {
235
- this.executionCount++
236
- this.options.onUpdate(this)
237
- this.options.onGetNextItem(item, this)
296
+ this._executionCount++
297
+ this._options.onItemsChange(this)
298
+ this._options.onGetNextItem(item, this)
238
299
  }
239
300
  return item
240
301
  }
@@ -245,118 +306,74 @@ export class Queuer<TValue> {
245
306
  * @example
246
307
  * ```ts
247
308
  * // Look at next item to getNextItem
248
- * queuer.peek()
309
+ * queuer.getPeek()
249
310
  * // Look at last item (like stack top)
250
- * queuer.peek('back')
311
+ * queuer.getPeek('back')
251
312
  * ```
252
313
  */
253
- peek(
254
- position: QueuePosition = this.options.getItemsFrom,
314
+ getPeek(
315
+ position: QueuePosition = this._options.getItemsFrom,
255
316
  ): TValue | undefined {
256
317
  if (position === 'front') {
257
- return this.items[0]
318
+ return this._items[0]
258
319
  }
259
- return this.items[this.items.length - 1]
320
+ return this._items[this._items.length - 1]
260
321
  }
261
322
 
262
323
  /**
263
324
  * Returns true if the queuer is empty
264
325
  */
265
- isEmpty(): boolean {
266
- return this.items.length === 0
326
+ getIsEmpty(): boolean {
327
+ return this._items.length === 0
267
328
  }
268
329
 
269
330
  /**
270
331
  * Returns true if the queuer is full
271
332
  */
272
- isFull(): boolean {
273
- return this.items.length >= this.options.maxSize
333
+ getIsFull(): boolean {
334
+ return this._items.length >= this._options.maxSize
274
335
  }
275
336
 
276
337
  /**
277
338
  * Returns the current size of the queuer
278
339
  */
279
- size(): number {
280
- return this.items.length
281
- }
282
-
283
- /**
284
- * Removes all items from the queuer
285
- */
286
- clear(): void {
287
- this.items = []
288
- this.options.onUpdate(this)
289
- }
290
-
291
- /**
292
- * Resets the queuer to its initial state
293
- */
294
- reset(withInitialItems?: boolean): void {
295
- this.clear()
296
- this.executionCount = 0
297
- if (withInitialItems) {
298
- this.items = [...this.options.initialItems]
299
- }
300
- this.running = this.options.started
340
+ getSize(): number {
341
+ return this._items.length
301
342
  }
302
343
 
303
344
  /**
304
345
  * Returns a copy of all items in the queuer
305
346
  */
306
347
  getAllItems(): Array<TValue> {
307
- return [...this.items]
348
+ return [...this._items]
308
349
  }
309
350
 
310
351
  /**
311
352
  * Returns the number of items that have been removed from the queuer
312
353
  */
313
354
  getExecutionCount(): number {
314
- return this.executionCount
315
- }
316
-
317
- /**
318
- * Adds a callback to be called when an item is processed
319
- */
320
- onUpdate(cb: (item: TValue) => void) {
321
- this.onUpdates.push(cb)
322
- return () => {
323
- this.onUpdates = this.onUpdates.filter((d) => d !== cb)
324
- }
355
+ return this._executionCount
325
356
  }
326
357
 
327
358
  /**
328
- * Stops the queuer from processing items
359
+ * Returns the number of items that have been rejected from the queuer
329
360
  */
330
- stop() {
331
- this.running = false
332
- this.pendingTick = false
333
- this.options.onUpdate(this)
334
- }
335
-
336
- /**
337
- * Starts the queuer and processes items
338
- */
339
- start() {
340
- this.running = true
341
- if (!this.pendingTick && !this.isEmpty()) {
342
- this.pendingTick = true
343
- this.tick()
344
- }
345
- this.options.onUpdate(this)
361
+ getRejectionCount(): number {
362
+ return this._rejectionCount
346
363
  }
347
364
 
348
365
  /**
349
366
  * Returns true if the queuer is running
350
367
  */
351
- isRunning() {
352
- return this.running
368
+ getIsRunning() {
369
+ return this._running
353
370
  }
354
371
 
355
372
  /**
356
373
  * Returns true if the queuer is running but has no items to process
357
374
  */
358
- isIdle() {
359
- return this.running && this.isEmpty()
375
+ getIsIdle() {
376
+ return this._running && this.getIsEmpty()
360
377
  }
361
378
  }
362
379
 
@@ -374,7 +391,7 @@ export class Queuer<TValue> {
374
391
  * // Basic sequential processing
375
392
  * const processItems = queuer<number>({
376
393
  * wait: 1000,
377
- * onUpdate: (queuer) => console.log(queuer.getAllItems())
394
+ * onItemsChange: (queuer) => console.log(queuer.getAllItems())
378
395
  * })
379
396
  * processItems(1) // Logs: 1
380
397
  * processItems(2) // Logs: 2 after 1 completes
@@ -1,29 +1,12 @@
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<
7
+ TFn extends AnyFunction,
8
+ TArgs extends Parameters<TFn>,
9
+ > {
27
10
  /**
28
11
  * Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.
29
12
  * Defaults to true.
@@ -34,18 +17,24 @@ export interface RateLimiterOptions {
34
17
  */
35
18
  limit: number
36
19
  /**
37
- * Time window in milliseconds within which the limit applies
20
+ * Callback function that is called after the function is executed
38
21
  */
39
- window: number
22
+ onExecute?: (rateLimiter: RateLimiter<TFn, TArgs>) => void
40
23
  /**
41
24
  * Optional callback function that is called when an execution is rejected due to rate limiting
42
25
  */
43
- onReject?: (info: RateLimitRejectionInfo) => void
26
+ onReject?: (rateLimiter: RateLimiter<TFn, TArgs>) => void
27
+ /**
28
+ * Time window in milliseconds within which the limit applies
29
+ */
30
+ window: number
44
31
  }
45
32
 
46
- const defaultOptions: Required<Omit<RateLimiterOptions, 'onReject'>> = {
33
+ const defaultOptions: Required<RateLimiterOptions<any, any>> = {
47
34
  enabled: true,
48
35
  limit: 1,
36
+ onExecute: () => {},
37
+ onReject: () => {},
49
38
  window: 0,
50
39
  }
51
40
 
@@ -75,19 +64,19 @@ const defaultOptions: Required<Omit<RateLimiterOptions, 'onReject'>> = {
75
64
  * ```
76
65
  */
77
66
  export class RateLimiter<
78
- TFn extends (...args: Array<any>) => any,
67
+ TFn extends AnyFunction,
79
68
  TArgs extends Parameters<TFn>,
80
69
  > {
81
- private executionCount = 0
82
- private rejectionCount = 0
83
- private executionTimes: Array<number> = []
84
- private options: RateLimiterOptions
70
+ private _executionCount = 0
71
+ private _rejectionCount = 0
72
+ private _executionTimes: Array<number> = []
73
+ private _options: RateLimiterOptions<TFn, TArgs>
85
74
 
86
75
  constructor(
87
76
  private fn: TFn,
88
- initialOptions: RateLimiterOptions,
77
+ initialOptions: RateLimiterOptions<TFn, TArgs>,
89
78
  ) {
90
- this.options = {
79
+ this._options = {
91
80
  ...defaultOptions,
92
81
  ...initialOptions,
93
82
  }
@@ -97,34 +86,21 @@ export class RateLimiter<
97
86
  * Updates the rate limiter options
98
87
  * Returns the new options state
99
88
  */
100
- setOptions(newOptions: Partial<RateLimiterOptions>): RateLimiterOptions {
101
- this.options = {
102
- ...this.options,
89
+ setOptions(
90
+ newOptions: Partial<RateLimiterOptions<TFn, TArgs>>,
91
+ ): RateLimiterOptions<TFn, TArgs> {
92
+ this._options = {
93
+ ...this._options,
103
94
  ...newOptions,
104
95
  }
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
113
- }
114
-
115
- /**
116
- * Returns the number of times the function has been rejected
117
- */
118
- getRejectionCount(): number {
119
- return this.rejectionCount
96
+ return this._options
120
97
  }
121
98
 
122
99
  /**
123
- * Returns the number of remaining executions allowed in the current window
100
+ * Returns the current rate limiter options
124
101
  */
125
- getRemainingInWindow(): number {
126
- this.cleanupOldExecutions()
127
- return Math.max(0, this.options.limit - this.executionTimes.length)
102
+ getOptions(): Required<RateLimiterOptions<TFn, TArgs>> {
103
+ return this._options as Required<RateLimiterOptions<TFn, TArgs>>
128
104
  }
129
105
 
130
106
  /**
@@ -145,7 +121,7 @@ export class RateLimiter<
145
121
  maybeExecute(...args: TArgs): boolean {
146
122
  this.cleanupOldExecutions()
147
123
 
148
- if (this.executionTimes.length < this.options.limit) {
124
+ if (this._executionTimes.length < this._options.limit) {
149
125
  this.executeFunction(...args)
150
126
  return true
151
127
  }
@@ -156,44 +132,66 @@ export class RateLimiter<
156
132
  }
157
133
 
158
134
  private executeFunction(...args: TArgs): void {
159
- if (!this.options.enabled) return
135
+ if (!this._options.enabled) return
160
136
  const now = Date.now()
161
- this.executionCount++
162
- this.executionTimes.push(now)
163
- this.fn(...args)
137
+ this._executionCount++
138
+ this._executionTimes.push(now)
139
+ this.fn(...args) // execute the function
140
+ this._options.onExecute?.(this)
164
141
  }
165
142
 
166
143
  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
- })
144
+ this._rejectionCount++
145
+ if (this._options.onReject) {
146
+ this._options.onReject(this)
179
147
  }
180
148
  }
181
149
 
182
150
  private cleanupOldExecutions(): void {
183
151
  const now = Date.now()
184
- const windowStart = now - this.options.window
185
- this.executionTimes = this.executionTimes.filter(
152
+ const windowStart = now - this._options.window
153
+ this._executionTimes = this._executionTimes.filter(
186
154
  (time) => time > windowStart,
187
155
  )
188
156
  }
189
157
 
158
+ /**
159
+ * Returns the number of times the function has been executed
160
+ */
161
+ getExecutionCount(): number {
162
+ return this._executionCount
163
+ }
164
+
165
+ /**
166
+ * Returns the number of times the function has been rejected
167
+ */
168
+ getRejectionCount(): number {
169
+ return this._rejectionCount
170
+ }
171
+
172
+ /**
173
+ * Returns the number of remaining executions allowed in the current window
174
+ */
175
+ getRemainingInWindow(): number {
176
+ this.cleanupOldExecutions()
177
+ return Math.max(0, this._options.limit - this._executionTimes.length)
178
+ }
179
+
180
+ /**
181
+ * Returns the number of milliseconds until the next execution will be possible
182
+ */
183
+ getMsUntilNextWindow(): number {
184
+ const oldestExecution = Math.min(...this._executionTimes)
185
+ return oldestExecution + this._options.window - Date.now()
186
+ }
187
+
190
188
  /**
191
189
  * Resets the rate limiter state
192
190
  */
193
191
  reset(): void {
194
- this.executionTimes = []
195
- this.executionCount = 0
196
- this.rejectionCount = 0
192
+ this._executionTimes = []
193
+ this._executionCount = 0
194
+ this._rejectionCount = 0
197
195
  }
198
196
  }
199
197
 
@@ -214,8 +212,8 @@ export class RateLimiter<
214
212
  * const rateLimited = rateLimit(makeApiCall, {
215
213
  * limit: 5,
216
214
  * window: 60000,
217
- * onReject: ({ msUntilNextWindow }) => {
218
- * console.log(`Rate limit exceeded. Try again in ${msUntilNextWindow}ms`);
215
+ * onReject: (rateLimiter) => {
216
+ * console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);
219
217
  * }
220
218
  * });
221
219
  *
@@ -227,9 +225,9 @@ export class RateLimiter<
227
225
  * const throttled = throttle(makeApiCall, { wait: 12000 }); // One call every 12 seconds
228
226
  * ```
229
227
  */
230
- export function rateLimit<TFn extends (...args: Array<any>) => any>(
228
+ export function rateLimit<TFn extends AnyFunction>(
231
229
  fn: TFn,
232
- initialOptions: Omit<RateLimiterOptions, 'enabled'>,
230
+ initialOptions: Omit<RateLimiterOptions<TFn, Parameters<TFn>>, 'enabled'>,
233
231
  ) {
234
232
  const rateLimiter = new RateLimiter(fn, initialOptions)
235
233
  return rateLimiter.maybeExecute.bind(rateLimiter)