@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.
- package/dist/cjs/async-debouncer.cjs +60 -44
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +37 -24
- package/dist/cjs/async-queuer.cjs +149 -125
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +65 -48
- package/dist/cjs/async-rate-limiter.cjs +63 -46
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +39 -27
- package/dist/cjs/async-throttler.cjs +70 -47
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +43 -25
- package/dist/cjs/debouncer.cjs +46 -22
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +25 -11
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +2 -0
- package/dist/cjs/queuer.cjs +114 -104
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +53 -40
- package/dist/cjs/rate-limiter.cjs +54 -42
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +37 -45
- package/dist/cjs/throttler.cjs +61 -41
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +35 -22
- package/dist/cjs/types.d.cts +12 -0
- package/dist/cjs/utils.cjs +13 -0
- package/dist/cjs/utils.cjs.map +1 -0
- package/dist/cjs/utils.d.cts +1 -0
- package/dist/esm/async-debouncer.d.ts +37 -24
- package/dist/esm/async-debouncer.js +60 -44
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +65 -48
- package/dist/esm/async-queuer.js +149 -125
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +39 -27
- package/dist/esm/async-rate-limiter.js +63 -46
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +43 -25
- package/dist/esm/async-throttler.js +70 -47
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/debouncer.d.ts +25 -11
- package/dist/esm/debouncer.js +46 -22
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/queuer.d.ts +53 -40
- package/dist/esm/queuer.js +114 -104
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +37 -45
- package/dist/esm/rate-limiter.js +54 -42
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +35 -22
- package/dist/esm/throttler.js +61 -41
- package/dist/esm/throttler.js.map +1 -1
- package/dist/esm/types.d.ts +12 -0
- package/dist/esm/utils.d.ts +1 -0
- package/dist/esm/utils.js +13 -0
- package/dist/esm/utils.js.map +1 -0
- package/package.json +8 -1
- package/src/async-debouncer.ts +90 -62
- package/src/async-queuer.ts +178 -145
- package/src/async-rate-limiter.ts +93 -67
- package/src/async-throttler.ts +98 -63
- package/src/debouncer.ts +71 -35
- package/src/index.ts +2 -0
- package/src/queuer.ts +135 -118
- package/src/rate-limiter.ts +79 -81
- package/src/throttler.ts +87 -61
- package/src/types.ts +17 -0
- package/src/utils.ts +13 -0
package/src/index.ts
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
-
* -
|
|
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
|
-
|
|
114
|
-
private
|
|
115
|
-
private
|
|
116
|
-
private
|
|
117
|
-
private
|
|
118
|
-
private
|
|
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.
|
|
122
|
-
this.
|
|
132
|
+
this._options = { ...defaultOptions, ...initialOptions }
|
|
133
|
+
this._running = this._options.started
|
|
123
134
|
|
|
124
|
-
for (let i = 0; i < this.
|
|
125
|
-
const item = this.
|
|
126
|
-
const isLast = i === this.
|
|
127
|
-
this.addItem(item, this.
|
|
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
|
-
|
|
135
|
-
if (!this.
|
|
136
|
-
this.
|
|
163
|
+
private tick() {
|
|
164
|
+
if (!this._running) {
|
|
165
|
+
this._pendingTick = false
|
|
137
166
|
return
|
|
138
167
|
}
|
|
139
|
-
while (!this.
|
|
140
|
-
const nextItem = this.getNextItem(this.
|
|
168
|
+
while (!this.getIsEmpty()) {
|
|
169
|
+
const nextItem = this.getNextItem(this._options.getItemsFrom)
|
|
141
170
|
if (nextItem === undefined) {
|
|
142
171
|
break
|
|
143
172
|
}
|
|
144
|
-
this.
|
|
173
|
+
this._onItemsChanges.forEach((cb) => cb(nextItem))
|
|
145
174
|
|
|
146
|
-
if (this.
|
|
175
|
+
if (this._options.wait > 0) {
|
|
147
176
|
// Use setTimeout to wait before processing next item
|
|
148
|
-
setTimeout(() => this.tick(), this.
|
|
177
|
+
setTimeout(() => this.tick(), this._options.wait)
|
|
149
178
|
return
|
|
150
179
|
}
|
|
151
180
|
|
|
152
181
|
this.tick()
|
|
153
182
|
}
|
|
154
|
-
this.
|
|
183
|
+
this._pendingTick = false
|
|
155
184
|
}
|
|
156
185
|
|
|
157
186
|
/**
|
|
158
|
-
*
|
|
159
|
-
* Returns the new options state
|
|
187
|
+
* Stops the queuer from processing items
|
|
160
188
|
*/
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
this.
|
|
165
|
-
|
|
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.
|
|
233
|
+
position: QueuePosition = this._options.addItemsTo,
|
|
175
234
|
runOnUpdate: boolean = true,
|
|
176
235
|
): boolean {
|
|
177
|
-
if (this.
|
|
236
|
+
if (this.getIsFull()) {
|
|
237
|
+
this._rejectionCount++
|
|
238
|
+
this._options.onReject(item, this)
|
|
178
239
|
return false
|
|
179
240
|
}
|
|
180
241
|
|
|
181
|
-
if (this.
|
|
242
|
+
if (this._options.getPriority !== defaultOptions.getPriority) {
|
|
182
243
|
// If custom priority function is provided, insert based on priority
|
|
183
|
-
const priority = this.
|
|
184
|
-
const insertIndex = this.
|
|
185
|
-
(existing) => this.
|
|
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.
|
|
250
|
+
this._items.push(item)
|
|
190
251
|
} else {
|
|
191
|
-
this.
|
|
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.
|
|
257
|
+
this._items.unshift(item)
|
|
197
258
|
} else {
|
|
198
|
-
this.
|
|
259
|
+
this._items.push(item)
|
|
199
260
|
}
|
|
200
261
|
}
|
|
201
262
|
|
|
202
|
-
if (this.
|
|
203
|
-
this.
|
|
263
|
+
if (this._running && !this._pendingTick) {
|
|
264
|
+
this._pendingTick = true
|
|
204
265
|
this.tick()
|
|
205
266
|
}
|
|
206
267
|
if (runOnUpdate) {
|
|
207
|
-
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.
|
|
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.
|
|
290
|
+
item = this._items.shift()
|
|
230
291
|
} else {
|
|
231
|
-
item = this.
|
|
292
|
+
item = this._items.pop()
|
|
232
293
|
}
|
|
233
294
|
|
|
234
295
|
if (item !== undefined) {
|
|
235
|
-
this.
|
|
236
|
-
this.
|
|
237
|
-
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.
|
|
309
|
+
* queuer.getPeek()
|
|
249
310
|
* // Look at last item (like stack top)
|
|
250
|
-
* queuer.
|
|
311
|
+
* queuer.getPeek('back')
|
|
251
312
|
* ```
|
|
252
313
|
*/
|
|
253
|
-
|
|
254
|
-
position: QueuePosition = this.
|
|
314
|
+
getPeek(
|
|
315
|
+
position: QueuePosition = this._options.getItemsFrom,
|
|
255
316
|
): TValue | undefined {
|
|
256
317
|
if (position === 'front') {
|
|
257
|
-
return this.
|
|
318
|
+
return this._items[0]
|
|
258
319
|
}
|
|
259
|
-
return this.
|
|
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
|
-
|
|
266
|
-
return this.
|
|
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
|
-
|
|
273
|
-
return this.
|
|
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
|
-
|
|
280
|
-
return this.
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
359
|
+
* Returns the number of items that have been rejected from the queuer
|
|
329
360
|
*/
|
|
330
|
-
|
|
331
|
-
this.
|
|
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
|
-
|
|
352
|
-
return this.
|
|
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
|
-
|
|
359
|
-
return this.
|
|
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
|
-
*
|
|
394
|
+
* onItemsChange: (queuer) => console.log(queuer.getAllItems())
|
|
378
395
|
* })
|
|
379
396
|
* processItems(1) // Logs: 1
|
|
380
397
|
* processItems(2) // Logs: 2 after 1 completes
|
package/src/rate-limiter.ts
CHANGED
|
@@ -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
|
-
*
|
|
20
|
+
* Callback function that is called after the function is executed
|
|
38
21
|
*/
|
|
39
|
-
|
|
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?: (
|
|
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<
|
|
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
|
|
67
|
+
TFn extends AnyFunction,
|
|
79
68
|
TArgs extends Parameters<TFn>,
|
|
80
69
|
> {
|
|
81
|
-
private
|
|
82
|
-
private
|
|
83
|
-
private
|
|
84
|
-
private
|
|
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.
|
|
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(
|
|
101
|
-
|
|
102
|
-
|
|
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.
|
|
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
|
|
100
|
+
* Returns the current rate limiter options
|
|
124
101
|
*/
|
|
125
|
-
|
|
126
|
-
this.
|
|
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.
|
|
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.
|
|
135
|
+
if (!this._options.enabled) return
|
|
160
136
|
const now = Date.now()
|
|
161
|
-
this.
|
|
162
|
-
this.
|
|
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.
|
|
168
|
-
if (this.
|
|
169
|
-
|
|
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.
|
|
185
|
-
this.
|
|
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.
|
|
195
|
-
this.
|
|
196
|
-
this.
|
|
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: (
|
|
218
|
-
* console.log(`Rate limit exceeded. Try again in ${
|
|
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
|
|
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)
|