@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.
- package/dist/cjs/async-debouncer.cjs +78 -45
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +44 -16
- 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 +46 -35
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +34 -19
- package/dist/cjs/async-throttler.cjs +85 -57
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +49 -17
- 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 +1 -5
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +9 -9
- 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 +44 -16
- 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 +34 -19
- package/dist/esm/async-rate-limiter.js +46 -35
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +49 -17
- 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 +9 -9
- package/dist/esm/rate-limiter.js +1 -5
- 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 +114 -73
- package/src/async-queuer.ts +93 -8
- package/src/async-rate-limiter.ts +74 -60
- package/src/async-throttler.ts +135 -86
- package/src/debouncer.ts +26 -33
- package/src/queuer.ts +92 -4
- package/src/rate-limiter.ts +14 -26
- package/src/throttler.ts +45 -53
- package/src/types.ts +2 -10
- package/src/utils.ts +1 -1
package/src/debouncer.ts
CHANGED
|
@@ -3,10 +3,7 @@ import type { AnyFunction } from './types'
|
|
|
3
3
|
/**
|
|
4
4
|
* Options for configuring a debounced function
|
|
5
5
|
*/
|
|
6
|
-
export interface DebouncerOptions<
|
|
7
|
-
TFn extends AnyFunction,
|
|
8
|
-
TArgs extends Parameters<TFn>,
|
|
9
|
-
> {
|
|
6
|
+
export interface DebouncerOptions<TFn extends AnyFunction> {
|
|
10
7
|
/**
|
|
11
8
|
* Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
12
9
|
* Defaults to true.
|
|
@@ -14,13 +11,14 @@ export interface DebouncerOptions<
|
|
|
14
11
|
enabled?: boolean
|
|
15
12
|
/**
|
|
16
13
|
* Whether to execute on the leading edge of the timeout.
|
|
14
|
+
* The first call will execute immediately and the rest will wait the delay.
|
|
17
15
|
* Defaults to false.
|
|
18
16
|
*/
|
|
19
17
|
leading?: boolean
|
|
20
18
|
/**
|
|
21
19
|
* Callback function that is called after the function is executed
|
|
22
20
|
*/
|
|
23
|
-
onExecute?: (debouncer: Debouncer<TFn
|
|
21
|
+
onExecute?: (debouncer: Debouncer<TFn>) => void
|
|
24
22
|
/**
|
|
25
23
|
* Whether to execute on the trailing edge of the timeout.
|
|
26
24
|
* Defaults to true.
|
|
@@ -33,12 +31,12 @@ export interface DebouncerOptions<
|
|
|
33
31
|
wait: number
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
const defaultOptions: Required<DebouncerOptions<any
|
|
34
|
+
const defaultOptions: Required<DebouncerOptions<any>> = {
|
|
37
35
|
enabled: true,
|
|
38
36
|
leading: false,
|
|
37
|
+
onExecute: () => {},
|
|
39
38
|
trailing: true,
|
|
40
39
|
wait: 0,
|
|
41
|
-
onExecute: () => {},
|
|
42
40
|
}
|
|
43
41
|
|
|
44
42
|
/**
|
|
@@ -64,16 +62,16 @@ const defaultOptions: Required<DebouncerOptions<any, any>> = {
|
|
|
64
62
|
* });
|
|
65
63
|
* ```
|
|
66
64
|
*/
|
|
67
|
-
export class Debouncer<TFn extends AnyFunction
|
|
65
|
+
export class Debouncer<TFn extends AnyFunction> {
|
|
68
66
|
private _canLeadingExecute = true
|
|
69
|
-
private _isPending = false
|
|
70
67
|
private _executionCount = 0
|
|
71
|
-
private
|
|
68
|
+
private _isPending = false
|
|
69
|
+
private _options: Required<DebouncerOptions<TFn>>
|
|
72
70
|
private _timeoutId: NodeJS.Timeout | undefined
|
|
73
71
|
|
|
74
72
|
constructor(
|
|
75
73
|
private fn: TFn,
|
|
76
|
-
initialOptions: DebouncerOptions<TFn
|
|
74
|
+
initialOptions: DebouncerOptions<TFn>,
|
|
77
75
|
) {
|
|
78
76
|
this._options = {
|
|
79
77
|
...defaultOptions,
|
|
@@ -85,26 +83,19 @@ export class Debouncer<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {
|
|
|
85
83
|
* Updates the debouncer options
|
|
86
84
|
* Returns the new options state
|
|
87
85
|
*/
|
|
88
|
-
setOptions(
|
|
89
|
-
|
|
90
|
-
): Required<DebouncerOptions<TFn, TArgs>> {
|
|
91
|
-
this._options = {
|
|
92
|
-
...this._options,
|
|
93
|
-
...newOptions,
|
|
94
|
-
}
|
|
86
|
+
setOptions(newOptions: Partial<DebouncerOptions<TFn>>): void {
|
|
87
|
+
this._options = { ...this._options, ...newOptions }
|
|
95
88
|
|
|
96
89
|
// End the pending state if the debouncer is disabled
|
|
97
90
|
if (!this._options.enabled) {
|
|
98
91
|
this._isPending = false
|
|
99
92
|
}
|
|
100
|
-
|
|
101
|
-
return this._options
|
|
102
93
|
}
|
|
103
94
|
|
|
104
95
|
/**
|
|
105
96
|
* Returns the current debouncer options
|
|
106
97
|
*/
|
|
107
|
-
getOptions(): Required<DebouncerOptions<TFn
|
|
98
|
+
getOptions(): Required<DebouncerOptions<TFn>> {
|
|
108
99
|
return this._options
|
|
109
100
|
}
|
|
110
101
|
|
|
@@ -112,35 +103,37 @@ export class Debouncer<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {
|
|
|
112
103
|
* Attempts to execute the debounced function
|
|
113
104
|
* If a call is already in progress, it will be queued
|
|
114
105
|
*/
|
|
115
|
-
maybeExecute(...args:
|
|
106
|
+
maybeExecute(...args: Parameters<TFn>): void {
|
|
107
|
+
let _didLeadingExecute = false
|
|
108
|
+
|
|
116
109
|
// Handle leading execution
|
|
117
110
|
if (this._options.leading && this._canLeadingExecute) {
|
|
118
|
-
this.executeFunction(...args)
|
|
119
111
|
this._canLeadingExecute = false
|
|
112
|
+
_didLeadingExecute = true
|
|
113
|
+
this.executeFunction(...args)
|
|
120
114
|
}
|
|
121
115
|
|
|
122
|
-
// Start pending state
|
|
123
|
-
if (this._options.
|
|
116
|
+
// Start pending state to indicate that the debouncer is waiting for the trailing edge
|
|
117
|
+
if (this._options.trailing) {
|
|
124
118
|
this._isPending = true
|
|
125
119
|
}
|
|
126
120
|
|
|
127
121
|
// Clear any existing timeout
|
|
128
122
|
if (this._timeoutId) clearTimeout(this._timeoutId)
|
|
129
123
|
|
|
130
|
-
// Set new timeout that will reset canLeadingExecute
|
|
124
|
+
// Set new timeout that will reset canLeadingExecute and execute trailing only if enabled and did not execute leading
|
|
131
125
|
this._timeoutId = setTimeout(() => {
|
|
132
126
|
this._canLeadingExecute = true
|
|
133
|
-
this.
|
|
134
|
-
// Execute trailing only if enabled
|
|
135
|
-
if (this._options.trailing) {
|
|
127
|
+
if (this._options.trailing && !_didLeadingExecute) {
|
|
136
128
|
this.executeFunction(...args)
|
|
137
129
|
}
|
|
138
130
|
}, this._options.wait)
|
|
139
131
|
}
|
|
140
132
|
|
|
141
|
-
private executeFunction(...args:
|
|
142
|
-
if (!this._options.enabled) return
|
|
133
|
+
private executeFunction(...args: Parameters<TFn>): void {
|
|
134
|
+
if (!this._options.enabled) return undefined
|
|
143
135
|
this.fn(...args) // EXECUTE!
|
|
136
|
+
this._isPending = false
|
|
144
137
|
this._executionCount++
|
|
145
138
|
this._options.onExecute(this)
|
|
146
139
|
}
|
|
@@ -193,8 +186,8 @@ export class Debouncer<TFn extends AnyFunction, TArgs extends Parameters<TFn>> {
|
|
|
193
186
|
*/
|
|
194
187
|
export function debounce<TFn extends AnyFunction>(
|
|
195
188
|
fn: TFn,
|
|
196
|
-
initialOptions: Omit<DebouncerOptions<TFn
|
|
197
|
-
) {
|
|
189
|
+
initialOptions: Omit<DebouncerOptions<TFn>, 'enabled'>,
|
|
190
|
+
): (...args: Parameters<TFn>) => void {
|
|
198
191
|
const debouncer = new Debouncer(fn, initialOptions)
|
|
199
192
|
return debouncer.maybeExecute.bind(debouncer)
|
|
200
193
|
}
|
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,18 +16,18 @@ 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
|
|
31
28
|
}
|
|
32
29
|
|
|
33
|
-
const defaultOptions: Required<RateLimiterOptions<any
|
|
30
|
+
const defaultOptions: Required<RateLimiterOptions<any>> = {
|
|
34
31
|
enabled: true,
|
|
35
32
|
limit: 1,
|
|
36
33
|
onExecute: () => {},
|
|
@@ -63,18 +60,15 @@ const defaultOptions: Required<RateLimiterOptions<any, any>> = {
|
|
|
63
60
|
* rateLimiter.maybeExecute('123');
|
|
64
61
|
* ```
|
|
65
62
|
*/
|
|
66
|
-
export class RateLimiter<
|
|
67
|
-
TFn extends AnyFunction,
|
|
68
|
-
TArgs extends Parameters<TFn>,
|
|
69
|
-
> {
|
|
63
|
+
export class RateLimiter<TFn extends AnyFunction> {
|
|
70
64
|
private _executionCount = 0
|
|
71
65
|
private _rejectionCount = 0
|
|
72
66
|
private _executionTimes: Array<number> = []
|
|
73
|
-
private _options: RateLimiterOptions<TFn
|
|
67
|
+
private _options: RateLimiterOptions<TFn>
|
|
74
68
|
|
|
75
69
|
constructor(
|
|
76
70
|
private fn: TFn,
|
|
77
|
-
initialOptions: RateLimiterOptions<TFn
|
|
71
|
+
initialOptions: RateLimiterOptions<TFn>,
|
|
78
72
|
) {
|
|
79
73
|
this._options = {
|
|
80
74
|
...defaultOptions,
|
|
@@ -86,21 +80,15 @@ export class RateLimiter<
|
|
|
86
80
|
* Updates the rate limiter options
|
|
87
81
|
* Returns the new options state
|
|
88
82
|
*/
|
|
89
|
-
setOptions(
|
|
90
|
-
|
|
91
|
-
): RateLimiterOptions<TFn, TArgs> {
|
|
92
|
-
this._options = {
|
|
93
|
-
...this._options,
|
|
94
|
-
...newOptions,
|
|
95
|
-
}
|
|
96
|
-
return this._options
|
|
83
|
+
setOptions(newOptions: Partial<RateLimiterOptions<TFn>>): void {
|
|
84
|
+
this._options = { ...this._options, ...newOptions }
|
|
97
85
|
}
|
|
98
86
|
|
|
99
87
|
/**
|
|
100
88
|
* Returns the current rate limiter options
|
|
101
89
|
*/
|
|
102
|
-
getOptions(): Required<RateLimiterOptions<TFn
|
|
103
|
-
return this._options as Required<RateLimiterOptions<TFn
|
|
90
|
+
getOptions(): Required<RateLimiterOptions<TFn>> {
|
|
91
|
+
return this._options as Required<RateLimiterOptions<TFn>>
|
|
104
92
|
}
|
|
105
93
|
|
|
106
94
|
/**
|
|
@@ -118,7 +106,7 @@ export class RateLimiter<
|
|
|
118
106
|
* rateLimiter.maybeExecute('arg1', 'arg2'); // false
|
|
119
107
|
* ```
|
|
120
108
|
*/
|
|
121
|
-
maybeExecute(...args:
|
|
109
|
+
maybeExecute(...args: Parameters<TFn>): boolean {
|
|
122
110
|
this.cleanupOldExecutions()
|
|
123
111
|
|
|
124
112
|
if (this._executionTimes.length < this._options.limit) {
|
|
@@ -131,7 +119,7 @@ export class RateLimiter<
|
|
|
131
119
|
return false
|
|
132
120
|
}
|
|
133
121
|
|
|
134
|
-
private executeFunction(...args:
|
|
122
|
+
private executeFunction(...args: Parameters<TFn>): void {
|
|
135
123
|
if (!this._options.enabled) return
|
|
136
124
|
const now = Date.now()
|
|
137
125
|
this._executionCount++
|
|
@@ -227,7 +215,7 @@ export class RateLimiter<
|
|
|
227
215
|
*/
|
|
228
216
|
export function rateLimit<TFn extends AnyFunction>(
|
|
229
217
|
fn: TFn,
|
|
230
|
-
initialOptions: Omit<RateLimiterOptions<TFn
|
|
218
|
+
initialOptions: Omit<RateLimiterOptions<TFn>, 'enabled'>,
|
|
231
219
|
) {
|
|
232
220
|
const rateLimiter = new RateLimiter(fn, initialOptions)
|
|
233
221
|
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