@tanstack/pacer 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/async-debouncer.cjs +78 -45
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +60 -24
- package/dist/cjs/async-queuer.cjs +53 -3
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +28 -3
- package/dist/cjs/async-rate-limiter.cjs +75 -38
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +76 -24
- package/dist/cjs/async-throttler.cjs +85 -57
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +66 -25
- package/dist/cjs/debouncer.cjs +12 -14
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +10 -9
- package/dist/cjs/queuer.cjs +51 -1
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +30 -1
- package/dist/cjs/rate-limiter.cjs +19 -9
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +31 -11
- package/dist/cjs/throttler.cjs +29 -36
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +16 -17
- package/dist/cjs/types.d.cts +2 -6
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +1 -1
- package/dist/esm/async-debouncer.d.ts +60 -24
- package/dist/esm/async-debouncer.js +78 -45
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +28 -3
- package/dist/esm/async-queuer.js +53 -3
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +76 -24
- package/dist/esm/async-rate-limiter.js +75 -38
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +66 -25
- package/dist/esm/async-throttler.js +85 -57
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/debouncer.d.ts +10 -9
- package/dist/esm/debouncer.js +12 -14
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/queuer.d.ts +30 -1
- package/dist/esm/queuer.js +51 -1
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +31 -11
- package/dist/esm/rate-limiter.js +19 -9
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +16 -17
- package/dist/esm/throttler.js +29 -36
- package/dist/esm/throttler.js.map +1 -1
- package/dist/esm/types.d.ts +2 -6
- package/dist/esm/utils.d.ts +1 -1
- package/dist/esm/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/async-debouncer.ts +130 -81
- package/src/async-queuer.ts +93 -8
- package/src/async-rate-limiter.ts +141 -67
- package/src/async-throttler.ts +152 -94
- package/src/debouncer.ts +26 -33
- package/src/queuer.ts +92 -4
- package/src/rate-limiter.ts +56 -32
- package/src/throttler.ts +45 -53
- package/src/types.ts +2 -10
- package/src/utils.ts +1 -1
package/src/queuer.ts
CHANGED
|
@@ -7,6 +7,16 @@ export interface QueuerOptions<TValue> {
|
|
|
7
7
|
* @default 'back'
|
|
8
8
|
*/
|
|
9
9
|
addItemsTo?: QueuePosition
|
|
10
|
+
/**
|
|
11
|
+
* Maximum time in milliseconds that an item can stay in the queue
|
|
12
|
+
* If not provided, items will never expire
|
|
13
|
+
*/
|
|
14
|
+
expirationDuration?: number
|
|
15
|
+
/**
|
|
16
|
+
* Function to determine if an item has expired
|
|
17
|
+
* If provided, this overrides the expirationDuration behavior
|
|
18
|
+
*/
|
|
19
|
+
getIsExpired?: (item: TValue, addedAt: number) => boolean
|
|
10
20
|
/**
|
|
11
21
|
* Default position to get items from during processing
|
|
12
22
|
* @default 'front'
|
|
@@ -25,6 +35,10 @@ export interface QueuerOptions<TValue> {
|
|
|
25
35
|
* Maximum number of items allowed in the queuer
|
|
26
36
|
*/
|
|
27
37
|
maxSize?: number
|
|
38
|
+
/**
|
|
39
|
+
* Callback fired whenever an item expires in the queuer
|
|
40
|
+
*/
|
|
41
|
+
onExpire?: (item: TValue, queuer: Queuer<TValue>) => void
|
|
28
42
|
/**
|
|
29
43
|
* Callback fired whenever an item is removed from the queuer
|
|
30
44
|
*/
|
|
@@ -55,12 +69,15 @@ const defaultOptions: Required<QueuerOptions<any>> = {
|
|
|
55
69
|
addItemsTo: 'back',
|
|
56
70
|
getItemsFrom: 'front',
|
|
57
71
|
getPriority: (item) => item?.priority ?? 0,
|
|
72
|
+
getIsExpired: () => false,
|
|
73
|
+
expirationDuration: Infinity,
|
|
58
74
|
initialItems: [],
|
|
59
75
|
maxSize: Infinity,
|
|
60
76
|
onGetNextItem: () => {},
|
|
61
77
|
onIsRunningChange: () => {},
|
|
62
78
|
onItemsChange: () => {},
|
|
63
79
|
onReject: () => {},
|
|
80
|
+
onExpire: () => {},
|
|
64
81
|
started: false,
|
|
65
82
|
wait: 0,
|
|
66
83
|
}
|
|
@@ -99,6 +116,11 @@ export type QueuePosition = 'front' | 'back'
|
|
|
99
116
|
* - wait: configurable delay between processing items
|
|
100
117
|
* - onItemsChange/onGetNextItem: callbacks for monitoring queuer state
|
|
101
118
|
*
|
|
119
|
+
* Supports item expiration to clear stale items from the queuer
|
|
120
|
+
* - expirationDuration: maximum time in milliseconds that an item can stay in the queue
|
|
121
|
+
* - getIsExpired: function to override default expiration behavior
|
|
122
|
+
* - onExpire: callback for when an item expires
|
|
123
|
+
*
|
|
102
124
|
* @example
|
|
103
125
|
* ```ts
|
|
104
126
|
* // FIFO queuer
|
|
@@ -122,8 +144,10 @@ export type QueuePosition = 'front' | 'back'
|
|
|
122
144
|
export class Queuer<TValue> {
|
|
123
145
|
private _options: Required<QueuerOptions<TValue>>
|
|
124
146
|
private _items: Array<TValue> = []
|
|
147
|
+
private _itemTimestamps: Array<number> = []
|
|
125
148
|
private _executionCount = 0
|
|
126
149
|
private _rejectionCount = 0
|
|
150
|
+
private _expirationCount = 0
|
|
127
151
|
private _onItemsChanges: Array<(item: TValue) => void> = []
|
|
128
152
|
private _running: boolean
|
|
129
153
|
private _pendingTick = false
|
|
@@ -143,11 +167,8 @@ export class Queuer<TValue> {
|
|
|
143
167
|
* Updates the queuer options
|
|
144
168
|
* Returns the new options state
|
|
145
169
|
*/
|
|
146
|
-
setOptions(
|
|
147
|
-
newOptions: Partial<QueuerOptions<TValue>>,
|
|
148
|
-
): QueuerOptions<TValue> {
|
|
170
|
+
setOptions(newOptions: Partial<QueuerOptions<TValue>>): void {
|
|
149
171
|
this._options = { ...this._options, ...newOptions }
|
|
150
|
-
return this._options
|
|
151
172
|
}
|
|
152
173
|
|
|
153
174
|
/**
|
|
@@ -165,6 +186,10 @@ export class Queuer<TValue> {
|
|
|
165
186
|
this._pendingTick = false
|
|
166
187
|
return
|
|
167
188
|
}
|
|
189
|
+
|
|
190
|
+
// Check for expired items
|
|
191
|
+
this.checkExpiredItems()
|
|
192
|
+
|
|
168
193
|
while (!this.getIsEmpty()) {
|
|
169
194
|
const nextItem = this.getNextItem(this._options.getItemsFrom)
|
|
170
195
|
if (nextItem === undefined) {
|
|
@@ -183,6 +208,56 @@ export class Queuer<TValue> {
|
|
|
183
208
|
this._pendingTick = false
|
|
184
209
|
}
|
|
185
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Checks for and removes expired items from the queuer
|
|
213
|
+
*/
|
|
214
|
+
private checkExpiredItems() {
|
|
215
|
+
if (
|
|
216
|
+
this._options.expirationDuration === Infinity &&
|
|
217
|
+
this._options.getIsExpired === defaultOptions.getIsExpired
|
|
218
|
+
)
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
const now = Date.now()
|
|
222
|
+
const expiredIndices: Array<number> = []
|
|
223
|
+
|
|
224
|
+
// Find indices of expired items
|
|
225
|
+
for (let i = 0; i < this._items.length; i++) {
|
|
226
|
+
const timestamp = this._itemTimestamps[i]
|
|
227
|
+
if (timestamp === undefined) continue
|
|
228
|
+
|
|
229
|
+
const item = this._items[i]
|
|
230
|
+
if (item === undefined) continue
|
|
231
|
+
|
|
232
|
+
const isExpired =
|
|
233
|
+
this._options.getIsExpired !== defaultOptions.getIsExpired
|
|
234
|
+
? this._options.getIsExpired(item, timestamp)
|
|
235
|
+
: now - timestamp > this._options.expirationDuration
|
|
236
|
+
|
|
237
|
+
if (isExpired) {
|
|
238
|
+
expiredIndices.push(i)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Remove expired items from back to front to maintain indices
|
|
243
|
+
for (let i = expiredIndices.length - 1; i >= 0; i--) {
|
|
244
|
+
const index = expiredIndices[i]
|
|
245
|
+
if (index === undefined) continue
|
|
246
|
+
|
|
247
|
+
const expiredItem = this._items[index]
|
|
248
|
+
if (expiredItem === undefined) continue
|
|
249
|
+
|
|
250
|
+
this._items.splice(index, 1)
|
|
251
|
+
this._itemTimestamps.splice(index, 1)
|
|
252
|
+
this._expirationCount++
|
|
253
|
+
this._options.onExpire(expiredItem, this)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (expiredIndices.length > 0) {
|
|
257
|
+
this._options.onItemsChange(this)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
186
261
|
/**
|
|
187
262
|
* Stops the queuer from processing items
|
|
188
263
|
*/
|
|
@@ -248,15 +323,19 @@ export class Queuer<TValue> {
|
|
|
248
323
|
|
|
249
324
|
if (insertIndex === -1) {
|
|
250
325
|
this._items.push(item)
|
|
326
|
+
this._itemTimestamps.push(Date.now())
|
|
251
327
|
} else {
|
|
252
328
|
this._items.splice(insertIndex, 0, item)
|
|
329
|
+
this._itemTimestamps.splice(insertIndex, 0, Date.now())
|
|
253
330
|
}
|
|
254
331
|
} else {
|
|
255
332
|
// Default FIFO/LIFO behavior
|
|
256
333
|
if (position === 'front') {
|
|
257
334
|
this._items.unshift(item)
|
|
335
|
+
this._itemTimestamps.unshift(Date.now())
|
|
258
336
|
} else {
|
|
259
337
|
this._items.push(item)
|
|
338
|
+
this._itemTimestamps.push(Date.now())
|
|
260
339
|
}
|
|
261
340
|
}
|
|
262
341
|
|
|
@@ -288,8 +367,10 @@ export class Queuer<TValue> {
|
|
|
288
367
|
|
|
289
368
|
if (position === 'front') {
|
|
290
369
|
item = this._items.shift()
|
|
370
|
+
this._itemTimestamps.shift()
|
|
291
371
|
} else {
|
|
292
372
|
item = this._items.pop()
|
|
373
|
+
this._itemTimestamps.pop()
|
|
293
374
|
}
|
|
294
375
|
|
|
295
376
|
if (item !== undefined) {
|
|
@@ -362,6 +443,13 @@ export class Queuer<TValue> {
|
|
|
362
443
|
return this._rejectionCount
|
|
363
444
|
}
|
|
364
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Returns the number of items that have expired from the queuer
|
|
448
|
+
*/
|
|
449
|
+
getExpirationCount(): number {
|
|
450
|
+
return this._expirationCount
|
|
451
|
+
}
|
|
452
|
+
|
|
365
453
|
/**
|
|
366
454
|
* Returns true if the queuer is running
|
|
367
455
|
*/
|
package/src/rate-limiter.ts
CHANGED
|
@@ -3,10 +3,7 @@ import type { AnyFunction } from './types'
|
|
|
3
3
|
/**
|
|
4
4
|
* Options for configuring a rate-limited function
|
|
5
5
|
*/
|
|
6
|
-
export interface RateLimiterOptions<
|
|
7
|
-
TFn extends AnyFunction,
|
|
8
|
-
TArgs extends Parameters<TFn>,
|
|
9
|
-
> {
|
|
6
|
+
export interface RateLimiterOptions<TFn extends AnyFunction> {
|
|
10
7
|
/**
|
|
11
8
|
* Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
12
9
|
* Defaults to true.
|
|
@@ -19,23 +16,31 @@ export interface RateLimiterOptions<
|
|
|
19
16
|
/**
|
|
20
17
|
* Callback function that is called after the function is executed
|
|
21
18
|
*/
|
|
22
|
-
onExecute?: (rateLimiter: RateLimiter<TFn
|
|
19
|
+
onExecute?: (rateLimiter: RateLimiter<TFn>) => void
|
|
23
20
|
/**
|
|
24
21
|
* Optional callback function that is called when an execution is rejected due to rate limiting
|
|
25
22
|
*/
|
|
26
|
-
onReject?: (rateLimiter: RateLimiter<TFn
|
|
23
|
+
onReject?: (rateLimiter: RateLimiter<TFn>) => void
|
|
27
24
|
/**
|
|
28
25
|
* Time window in milliseconds within which the limit applies
|
|
29
26
|
*/
|
|
30
27
|
window: number
|
|
28
|
+
/**
|
|
29
|
+
* Type of window to use for rate limiting
|
|
30
|
+
* - 'fixed': Uses a fixed window that resets after the window period
|
|
31
|
+
* - 'sliding': Uses a sliding window that allows executions as old ones expire
|
|
32
|
+
* Defaults to 'fixed'
|
|
33
|
+
*/
|
|
34
|
+
windowType?: 'fixed' | 'sliding'
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
const defaultOptions: Required<RateLimiterOptions<any
|
|
37
|
+
const defaultOptions: Required<RateLimiterOptions<any>> = {
|
|
34
38
|
enabled: true,
|
|
35
39
|
limit: 1,
|
|
36
40
|
onExecute: () => {},
|
|
37
41
|
onReject: () => {},
|
|
38
42
|
window: 0,
|
|
43
|
+
windowType: 'fixed',
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
/**
|
|
@@ -45,6 +50,12 @@ const defaultOptions: Required<RateLimiterOptions<any, any>> = {
|
|
|
45
50
|
* then blocks all subsequent calls until the window passes. This can lead to "bursty" behavior where
|
|
46
51
|
* all executions happen immediately, followed by a complete block.
|
|
47
52
|
*
|
|
53
|
+
* The rate limiter supports two types of windows:
|
|
54
|
+
* - 'fixed': A strict window that resets after the window period. All executions within the window count
|
|
55
|
+
* towards the limit, and the window resets completely after the period.
|
|
56
|
+
* - 'sliding': A rolling window that allows executions as old ones expire. This provides a more
|
|
57
|
+
* consistent rate of execution over time.
|
|
58
|
+
*
|
|
48
59
|
* For smoother execution patterns, consider using:
|
|
49
60
|
* - Throttling: Ensures consistent spacing between executions (e.g. max once per 200ms)
|
|
50
61
|
* - Debouncing: Waits for a pause in calls before executing (e.g. after 500ms of no calls)
|
|
@@ -56,25 +67,22 @@ const defaultOptions: Required<RateLimiterOptions<any, any>> = {
|
|
|
56
67
|
* ```ts
|
|
57
68
|
* const rateLimiter = new RateLimiter(
|
|
58
69
|
* (id: string) => api.getData(id),
|
|
59
|
-
* { limit: 5, window: 1000 } // 5 calls per second
|
|
70
|
+
* { limit: 5, window: 1000, windowType: 'sliding' } // 5 calls per second with sliding window
|
|
60
71
|
* );
|
|
61
72
|
*
|
|
62
73
|
* // Will execute immediately until limit reached, then block
|
|
63
74
|
* rateLimiter.maybeExecute('123');
|
|
64
75
|
* ```
|
|
65
76
|
*/
|
|
66
|
-
export class RateLimiter<
|
|
67
|
-
TFn extends AnyFunction,
|
|
68
|
-
TArgs extends Parameters<TFn>,
|
|
69
|
-
> {
|
|
77
|
+
export class RateLimiter<TFn extends AnyFunction> {
|
|
70
78
|
private _executionCount = 0
|
|
71
79
|
private _rejectionCount = 0
|
|
72
80
|
private _executionTimes: Array<number> = []
|
|
73
|
-
private _options: RateLimiterOptions<TFn
|
|
81
|
+
private _options: RateLimiterOptions<TFn>
|
|
74
82
|
|
|
75
83
|
constructor(
|
|
76
84
|
private fn: TFn,
|
|
77
|
-
initialOptions: RateLimiterOptions<TFn
|
|
85
|
+
initialOptions: RateLimiterOptions<TFn>,
|
|
78
86
|
) {
|
|
79
87
|
this._options = {
|
|
80
88
|
...defaultOptions,
|
|
@@ -86,21 +94,15 @@ export class RateLimiter<
|
|
|
86
94
|
* Updates the rate limiter options
|
|
87
95
|
* Returns the new options state
|
|
88
96
|
*/
|
|
89
|
-
setOptions(
|
|
90
|
-
|
|
91
|
-
): RateLimiterOptions<TFn, TArgs> {
|
|
92
|
-
this._options = {
|
|
93
|
-
...this._options,
|
|
94
|
-
...newOptions,
|
|
95
|
-
}
|
|
96
|
-
return this._options
|
|
97
|
+
setOptions(newOptions: Partial<RateLimiterOptions<TFn>>): void {
|
|
98
|
+
this._options = { ...this._options, ...newOptions }
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
/**
|
|
100
102
|
* Returns the current rate limiter options
|
|
101
103
|
*/
|
|
102
|
-
getOptions(): Required<RateLimiterOptions<TFn
|
|
103
|
-
return this._options as Required<RateLimiterOptions<TFn
|
|
104
|
+
getOptions(): Required<RateLimiterOptions<TFn>> {
|
|
105
|
+
return this._options as Required<RateLimiterOptions<TFn>>
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
/**
|
|
@@ -118,20 +120,32 @@ export class RateLimiter<
|
|
|
118
120
|
* rateLimiter.maybeExecute('arg1', 'arg2'); // false
|
|
119
121
|
* ```
|
|
120
122
|
*/
|
|
121
|
-
maybeExecute(...args:
|
|
123
|
+
maybeExecute(...args: Parameters<TFn>): boolean {
|
|
122
124
|
this.cleanupOldExecutions()
|
|
123
125
|
|
|
124
|
-
if (this.
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
if (this._options.windowType === 'sliding') {
|
|
127
|
+
// For sliding window, we can execute if we have capacity in the current window
|
|
128
|
+
if (this._executionTimes.length < this._options.limit) {
|
|
129
|
+
this.executeFunction(...args)
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
// For fixed window, we need to check if we're in a new window
|
|
134
|
+
const now = Date.now()
|
|
135
|
+
const oldestExecution = Math.min(...this._executionTimes)
|
|
136
|
+
const isNewWindow = oldestExecution + this._options.window <= now
|
|
137
|
+
|
|
138
|
+
if (isNewWindow || this._executionTimes.length < this._options.limit) {
|
|
139
|
+
this.executeFunction(...args)
|
|
140
|
+
return true
|
|
141
|
+
}
|
|
127
142
|
}
|
|
128
143
|
|
|
129
144
|
this.rejectFunction()
|
|
130
|
-
|
|
131
145
|
return false
|
|
132
146
|
}
|
|
133
147
|
|
|
134
|
-
private executeFunction(...args:
|
|
148
|
+
private executeFunction(...args: Parameters<TFn>): void {
|
|
135
149
|
if (!this._options.enabled) return
|
|
136
150
|
const now = Date.now()
|
|
137
151
|
this._executionCount++
|
|
@@ -181,6 +195,9 @@ export class RateLimiter<
|
|
|
181
195
|
* Returns the number of milliseconds until the next execution will be possible
|
|
182
196
|
*/
|
|
183
197
|
getMsUntilNextWindow(): number {
|
|
198
|
+
if (this.getRemainingInWindow() > 0) {
|
|
199
|
+
return 0
|
|
200
|
+
}
|
|
184
201
|
const oldestExecution = Math.min(...this._executionTimes)
|
|
185
202
|
return oldestExecution + this._options.window - Date.now()
|
|
186
203
|
}
|
|
@@ -203,15 +220,22 @@ export class RateLimiter<
|
|
|
203
220
|
* - A throttler ensures even spacing between executions, which can be better for consistent performance
|
|
204
221
|
* - A debouncer collapses multiple calls into one, which is better for handling bursts of events
|
|
205
222
|
*
|
|
223
|
+
* The rate limiter supports two types of windows:
|
|
224
|
+
* - 'fixed': A strict window that resets after the window period. All executions within the window count
|
|
225
|
+
* towards the limit, and the window resets completely after the period.
|
|
226
|
+
* - 'sliding': A rolling window that allows executions as old ones expire. This provides a more
|
|
227
|
+
* consistent rate of execution over time.
|
|
228
|
+
*
|
|
206
229
|
* Consider using throttle() or debounce() if you need more intelligent execution control. Use rate limiting when you specifically
|
|
207
230
|
* need to enforce a hard limit on the number of executions within a time period.
|
|
208
231
|
*
|
|
209
232
|
* @example
|
|
210
233
|
* ```ts
|
|
211
|
-
* // Rate limit to 5 calls per minute
|
|
234
|
+
* // Rate limit to 5 calls per minute with a sliding window
|
|
212
235
|
* const rateLimited = rateLimit(makeApiCall, {
|
|
213
236
|
* limit: 5,
|
|
214
237
|
* window: 60000,
|
|
238
|
+
* windowType: 'sliding',
|
|
215
239
|
* onReject: (rateLimiter) => {
|
|
216
240
|
* console.log(`Rate limit exceeded. Try again in ${rateLimiter.getMsUntilNextWindow()}ms`);
|
|
217
241
|
* }
|
|
@@ -227,7 +251,7 @@ export class RateLimiter<
|
|
|
227
251
|
*/
|
|
228
252
|
export function rateLimit<TFn extends AnyFunction>(
|
|
229
253
|
fn: TFn,
|
|
230
|
-
initialOptions: Omit<RateLimiterOptions<TFn
|
|
254
|
+
initialOptions: Omit<RateLimiterOptions<TFn>, 'enabled'>,
|
|
231
255
|
) {
|
|
232
256
|
const rateLimiter = new RateLimiter(fn, initialOptions)
|
|
233
257
|
return rateLimiter.maybeExecute.bind(rateLimiter)
|
package/src/throttler.ts
CHANGED
|
@@ -3,10 +3,7 @@ import type { AnyFunction } from './types'
|
|
|
3
3
|
/**
|
|
4
4
|
* Options for configuring a throttled function
|
|
5
5
|
*/
|
|
6
|
-
export interface ThrottlerOptions<
|
|
7
|
-
TFn extends AnyFunction,
|
|
8
|
-
TArgs extends Parameters<TFn>,
|
|
9
|
-
> {
|
|
6
|
+
export interface ThrottlerOptions<TFn extends AnyFunction> {
|
|
10
7
|
/**
|
|
11
8
|
* Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
12
9
|
* Defaults to true.
|
|
@@ -20,7 +17,7 @@ export interface ThrottlerOptions<
|
|
|
20
17
|
/**
|
|
21
18
|
* Callback function that is called after the function is executed
|
|
22
19
|
*/
|
|
23
|
-
onExecute?: (throttler: Throttler<TFn
|
|
20
|
+
onExecute?: (throttler: Throttler<TFn>) => void
|
|
24
21
|
/**
|
|
25
22
|
* Whether to execute on the trailing edge of the timeout.
|
|
26
23
|
* Defaults to true.
|
|
@@ -32,12 +29,12 @@ export interface ThrottlerOptions<
|
|
|
32
29
|
wait: number
|
|
33
30
|
}
|
|
34
31
|
|
|
35
|
-
const defaultOptions: Required<ThrottlerOptions<any
|
|
32
|
+
const defaultOptions: Required<ThrottlerOptions<any>> = {
|
|
36
33
|
enabled: true,
|
|
37
34
|
leading: true,
|
|
35
|
+
onExecute: () => {},
|
|
38
36
|
trailing: true,
|
|
39
37
|
wait: 0,
|
|
40
|
-
onExecute: () => {},
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
/**
|
|
@@ -67,17 +64,16 @@ const defaultOptions: Required<ThrottlerOptions<any, any>> = {
|
|
|
67
64
|
* throttler.maybeExecute('123'); // Throttled
|
|
68
65
|
* ```
|
|
69
66
|
*/
|
|
70
|
-
export class Throttler<TFn extends AnyFunction
|
|
67
|
+
export class Throttler<TFn extends AnyFunction> {
|
|
71
68
|
private _executionCount = 0
|
|
72
|
-
private _lastArgs:
|
|
69
|
+
private _lastArgs: Parameters<TFn> | undefined
|
|
73
70
|
private _lastExecutionTime = 0
|
|
74
|
-
private _options: Required<ThrottlerOptions<TFn
|
|
71
|
+
private _options: Required<ThrottlerOptions<TFn>>
|
|
75
72
|
private _timeoutId: NodeJS.Timeout | undefined
|
|
76
|
-
private _isPending = false
|
|
77
73
|
|
|
78
74
|
constructor(
|
|
79
75
|
private fn: TFn,
|
|
80
|
-
initialOptions: ThrottlerOptions<TFn
|
|
76
|
+
initialOptions: ThrottlerOptions<TFn>,
|
|
81
77
|
) {
|
|
82
78
|
this._options = {
|
|
83
79
|
...defaultOptions,
|
|
@@ -89,20 +85,19 @@ export class Throttler<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {
|
|
|
89
85
|
* Updates the throttler options
|
|
90
86
|
* Returns the new options state
|
|
91
87
|
*/
|
|
92
|
-
setOptions(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
88
|
+
setOptions(newOptions: Partial<ThrottlerOptions<TFn>>): void {
|
|
89
|
+
this._options = { ...this._options, ...newOptions }
|
|
90
|
+
|
|
91
|
+
// End the pending state if the debouncer is disabled
|
|
92
|
+
if (!this._options.enabled) {
|
|
93
|
+
this.cancel()
|
|
98
94
|
}
|
|
99
|
-
return this._options
|
|
100
95
|
}
|
|
101
96
|
|
|
102
97
|
/**
|
|
103
98
|
* Returns the current throttler options
|
|
104
99
|
*/
|
|
105
|
-
getOptions(): Required<ThrottlerOptions<TFn
|
|
100
|
+
getOptions(): Required<ThrottlerOptions<TFn>> {
|
|
106
101
|
return this._options
|
|
107
102
|
}
|
|
108
103
|
|
|
@@ -128,42 +123,40 @@ export class Throttler<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {
|
|
|
128
123
|
* throttled.maybeExecute('c', 'd');
|
|
129
124
|
* ```
|
|
130
125
|
*/
|
|
131
|
-
maybeExecute(...args:
|
|
126
|
+
maybeExecute(...args: Parameters<TFn>): void {
|
|
132
127
|
const now = Date.now()
|
|
133
128
|
const timeSinceLastExecution = now - this._lastExecutionTime
|
|
134
129
|
|
|
135
130
|
// Handle leading execution
|
|
136
|
-
if (timeSinceLastExecution >= this._options.wait) {
|
|
137
|
-
|
|
138
|
-
this.executeFunction(...args)
|
|
139
|
-
}
|
|
140
|
-
this._lastExecutionTime = now
|
|
141
|
-
this._isPending = false
|
|
131
|
+
if (this._options.leading && timeSinceLastExecution >= this._options.wait) {
|
|
132
|
+
this.executeFunction(...args)
|
|
142
133
|
} else {
|
|
143
134
|
// Store the most recent arguments for potential trailing execution
|
|
144
135
|
this._lastArgs = args
|
|
145
136
|
|
|
146
137
|
// Set up trailing execution if not already scheduled
|
|
147
138
|
if (!this._timeoutId && this._options.trailing) {
|
|
148
|
-
|
|
139
|
+
const _timeSinceLastExecution = this._lastExecutionTime
|
|
140
|
+
? now - this._lastExecutionTime
|
|
141
|
+
: 0
|
|
142
|
+
const timeoutDuration = this._options.wait - _timeSinceLastExecution
|
|
149
143
|
this._timeoutId = setTimeout(() => {
|
|
150
|
-
if (this._lastArgs) {
|
|
144
|
+
if (this._lastArgs !== undefined) {
|
|
151
145
|
this.executeFunction(...this._lastArgs)
|
|
152
|
-
this._lastArgs = undefined
|
|
153
146
|
}
|
|
154
|
-
|
|
155
|
-
this._timeoutId = undefined
|
|
156
|
-
this._isPending = false
|
|
157
|
-
this._options.onExecute(this)
|
|
158
|
-
}, this._options.wait - timeSinceLastExecution)
|
|
147
|
+
}, timeoutDuration)
|
|
159
148
|
}
|
|
160
149
|
}
|
|
161
150
|
}
|
|
162
151
|
|
|
163
|
-
private executeFunction(...args:
|
|
152
|
+
private executeFunction(...args: Parameters<TFn>): void {
|
|
164
153
|
if (!this._options.enabled) return
|
|
165
154
|
this.fn(...args) // EXECUTE!
|
|
166
155
|
this._executionCount++
|
|
156
|
+
this._lastExecutionTime = Date.now()
|
|
157
|
+
this._timeoutId = undefined
|
|
158
|
+
this._lastArgs = undefined
|
|
159
|
+
this._options.onExecute(this)
|
|
167
160
|
}
|
|
168
161
|
|
|
169
162
|
/**
|
|
@@ -180,36 +173,35 @@ export class Throttler<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {
|
|
|
180
173
|
clearTimeout(this._timeoutId)
|
|
181
174
|
this._timeoutId = undefined
|
|
182
175
|
this._lastArgs = undefined
|
|
183
|
-
this._isPending = false
|
|
184
176
|
}
|
|
185
177
|
}
|
|
186
178
|
|
|
187
179
|
/**
|
|
188
|
-
* Returns the
|
|
180
|
+
* Returns the last execution time
|
|
189
181
|
*/
|
|
190
|
-
|
|
191
|
-
return this.
|
|
182
|
+
getLastExecutionTime(): number {
|
|
183
|
+
return this._lastExecutionTime
|
|
192
184
|
}
|
|
193
185
|
|
|
194
186
|
/**
|
|
195
|
-
* Returns
|
|
187
|
+
* Returns the next execution time
|
|
196
188
|
*/
|
|
197
|
-
|
|
198
|
-
return this.
|
|
189
|
+
getNextExecutionTime(): number {
|
|
190
|
+
return this._lastExecutionTime + this._options.wait
|
|
199
191
|
}
|
|
200
192
|
|
|
201
193
|
/**
|
|
202
|
-
* Returns the
|
|
194
|
+
* Returns the number of times the function has been executed
|
|
203
195
|
*/
|
|
204
|
-
|
|
205
|
-
return this.
|
|
196
|
+
getExecutionCount(): number {
|
|
197
|
+
return this._executionCount
|
|
206
198
|
}
|
|
207
199
|
|
|
208
200
|
/**
|
|
209
|
-
* Returns
|
|
201
|
+
* Returns `true` if there is a pending execution
|
|
210
202
|
*/
|
|
211
|
-
|
|
212
|
-
return this.
|
|
203
|
+
getIsPending(): boolean {
|
|
204
|
+
return this._options.enabled && !!this._timeoutId
|
|
213
205
|
}
|
|
214
206
|
}
|
|
215
207
|
|
|
@@ -239,10 +231,10 @@ export class Throttler<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {
|
|
|
239
231
|
* });
|
|
240
232
|
* ```
|
|
241
233
|
*/
|
|
242
|
-
export function throttle<
|
|
243
|
-
TFn
|
|
244
|
-
|
|
245
|
-
|
|
234
|
+
export function throttle<TFn extends AnyFunction>(
|
|
235
|
+
fn: TFn,
|
|
236
|
+
initialOptions: Omit<ThrottlerOptions<TFn>, 'enabled'>,
|
|
237
|
+
) {
|
|
246
238
|
const throttler = new Throttler(fn, initialOptions)
|
|
247
239
|
return throttler.maybeExecute.bind(throttler)
|
|
248
240
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
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
|
|
7
|
-
...args: TArgs
|
|
8
|
-
) => any
|
|
4
|
+
export type AnyFunction = (...args: Array<any>) => any
|
|
9
5
|
|
|
10
6
|
/**
|
|
11
7
|
* Represents an asynchronous function that can be called with any arguments and returns a promise.
|
|
12
|
-
* @template TArgs - The type of the arguments the function can be called with.
|
|
13
|
-
* @returns A promise that resolves to the return value of the function.
|
|
14
8
|
*/
|
|
15
|
-
export type AnyAsyncFunction
|
|
16
|
-
...args: TArgs
|
|
17
|
-
) => Promise<any>
|
|
9
|
+
export type AnyAsyncFunction = (...args: Array<any>) => Promise<any>
|
package/src/utils.ts
CHANGED