@tanstack/pacer 0.4.0 → 0.5.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 +16 -3
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +14 -4
- package/dist/cjs/async-queuer.cjs +20 -6
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +21 -8
- package/dist/cjs/async-rate-limiter.cjs +28 -7
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +21 -6
- package/dist/cjs/async-throttler.cjs +19 -5
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +14 -4
- package/dist/cjs/debouncer.cjs +16 -3
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +14 -4
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/queuer.cjs +13 -5
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +9 -3
- package/dist/cjs/rate-limiter.cjs +26 -7
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +20 -6
- package/dist/cjs/throttler.cjs +19 -5
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +15 -4
- package/dist/cjs/utils.cjs +18 -7
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +3 -0
- package/dist/esm/async-debouncer.d.ts +14 -4
- package/dist/esm/async-debouncer.js +16 -3
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +21 -8
- package/dist/esm/async-queuer.js +20 -6
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +21 -6
- package/dist/esm/async-rate-limiter.js +28 -7
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +14 -4
- package/dist/esm/async-throttler.js +19 -5
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/debouncer.d.ts +14 -4
- package/dist/esm/debouncer.js +16 -3
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/queuer.d.ts +9 -3
- package/dist/esm/queuer.js +13 -5
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +20 -6
- package/dist/esm/rate-limiter.js +26 -7
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +15 -4
- package/dist/esm/throttler.js +19 -5
- package/dist/esm/throttler.js.map +1 -1
- package/dist/esm/utils.d.ts +3 -0
- package/dist/esm/utils.js +19 -8
- package/dist/esm/utils.js.map +1 -1
- package/package.json +9 -3
- package/src/async-debouncer.ts +24 -7
- package/src/async-queuer.ts +32 -13
- package/src/async-rate-limiter.ts +41 -13
- package/src/async-throttler.ts +27 -9
- package/src/debouncer.ts +24 -7
- package/src/queuer.ts +19 -7
- package/src/rate-limiter.ts +37 -13
- package/src/throttler.ts +28 -9
- package/src/utils.ts +19 -5
package/dist/esm/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["import type { AnyFunction } from './types'\n\nexport function isFunction<T extends AnyFunction>(value: any): value is T {\n return typeof value === 'function'\n}\n\nexport function parseFunctionOrValue<T, TArgs extends Array<any>>(\n value: T | ((...args: TArgs) => T),\n ...args: TArgs\n): T {\n return isFunction(value) ? value(...args) : value\n}\n\nexport function bindInstanceMethods<T extends Record<string, any>>(\n instance: T,\n): T {\n return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).reduce(\n (acc: any, key) => {\n const method = instance[key as keyof T]\n if (isFunction(method)) {\n acc[key] = method.bind(instance)\n }\n return acc\n },\n instance,\n )\n}\n"],"names":[],"mappings":"AAEO,SAAS,WAAkC,OAAwB;AACxE,SAAO,OAAO,UAAU;AAC1B;AAEgB,SAAA,qBACd,UACG,MACA;AACH,SAAO,WAAW,KAAK,IAAI,MAAM,GAAG,IAAI,IAAI;AAC9C;AAEO,SAAS,oBACd,UACG;AACH,SAAO,OAAO,oBAAoB,OAAO,eAAe,QAAQ,CAAC,EAAE;AAAA,IACjE,CAAC,KAAU,QAAQ;AACX,YAAA,SAAS,SAAS,GAAc;AAClC,UAAA,WAAW,MAAM,GAAG;AACtB,YAAI,GAAG,IAAI,OAAO,KAAK,QAAQ;AAAA,MAAA;AAE1B,aAAA;AAAA,IACT;AAAA,IACA;AAAA,EACF;AACF;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/pacer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Utilities for debouncing, throttling, rate-limiting, queuing, and more.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -131,8 +131,14 @@
|
|
|
131
131
|
"types": "./dist/esm/types.d.ts"
|
|
132
132
|
},
|
|
133
133
|
"./utils": {
|
|
134
|
-
"
|
|
135
|
-
|
|
134
|
+
"import": {
|
|
135
|
+
"types": "./dist/esm/utils.d.ts",
|
|
136
|
+
"default": "./dist/esm/utils.js"
|
|
137
|
+
},
|
|
138
|
+
"require": {
|
|
139
|
+
"types": "./dist/cjs/utils.d.cts",
|
|
140
|
+
"default": "./dist/cjs/utils.cjs"
|
|
141
|
+
}
|
|
136
142
|
},
|
|
137
143
|
"./package.json": "./package.json"
|
|
138
144
|
},
|
package/src/async-debouncer.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseFunctionOrValue } from './utils'
|
|
1
2
|
import type { AnyAsyncFunction } from './types'
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -6,9 +7,10 @@ import type { AnyAsyncFunction } from './types'
|
|
|
6
7
|
export interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {
|
|
7
8
|
/**
|
|
8
9
|
* Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
10
|
+
* Can be a boolean or a function that returns a boolean.
|
|
9
11
|
* Defaults to true.
|
|
10
12
|
*/
|
|
11
|
-
enabled?: boolean
|
|
13
|
+
enabled?: boolean | ((debouncer: AsyncDebouncer<TFn>) => boolean)
|
|
12
14
|
/**
|
|
13
15
|
* Whether to execute on the leading edge of the timeout.
|
|
14
16
|
* Defaults to false.
|
|
@@ -32,10 +34,11 @@ export interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {
|
|
|
32
34
|
*/
|
|
33
35
|
trailing?: boolean
|
|
34
36
|
/**
|
|
35
|
-
* Delay in milliseconds to wait after the last call before executing
|
|
37
|
+
* Delay in milliseconds to wait after the last call before executing.
|
|
38
|
+
* Can be a number or a function that returns a number.
|
|
36
39
|
* Defaults to 0ms
|
|
37
40
|
*/
|
|
38
|
-
wait: number
|
|
41
|
+
wait: number | ((debouncer: AsyncDebouncer<TFn>) => number)
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
const defaultOptions: Required<AsyncDebouncerOptions<any>> = {
|
|
@@ -117,6 +120,20 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
|
|
|
117
120
|
return this._options
|
|
118
121
|
}
|
|
119
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Returns the current debouncer enabled state
|
|
125
|
+
*/
|
|
126
|
+
getEnabled(): boolean {
|
|
127
|
+
return parseFunctionOrValue(this._options.enabled, this)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Returns the current debouncer wait state
|
|
132
|
+
*/
|
|
133
|
+
getWait(): number {
|
|
134
|
+
return parseFunctionOrValue(this._options.wait, this)
|
|
135
|
+
}
|
|
136
|
+
|
|
120
137
|
/**
|
|
121
138
|
* Attempts to execute the debounced function
|
|
122
139
|
* If a call is already in progress, it will be queued
|
|
@@ -149,14 +166,14 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
|
|
|
149
166
|
// Reset state and resolve
|
|
150
167
|
this._canLeadingExecute = true
|
|
151
168
|
resolve(this._lastResult)
|
|
152
|
-
}, this.
|
|
169
|
+
}, this.getWait())
|
|
153
170
|
})
|
|
154
171
|
}
|
|
155
172
|
|
|
156
173
|
private async executeFunction(
|
|
157
174
|
...args: Parameters<TFn>
|
|
158
175
|
): Promise<ReturnType<TFn> | undefined> {
|
|
159
|
-
if (!this.
|
|
176
|
+
if (!this.getEnabled()) return undefined
|
|
160
177
|
this._abortController = new AbortController()
|
|
161
178
|
try {
|
|
162
179
|
this._isExecuting = true
|
|
@@ -233,7 +250,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
|
|
|
233
250
|
* Returns `true` if there is a pending execution queued up for trailing execution
|
|
234
251
|
*/
|
|
235
252
|
getIsPending(): boolean {
|
|
236
|
-
return this.
|
|
253
|
+
return this.getEnabled() && this._isPending
|
|
237
254
|
}
|
|
238
255
|
|
|
239
256
|
/**
|
|
@@ -267,7 +284,7 @@ export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
|
|
|
267
284
|
*/
|
|
268
285
|
export function asyncDebounce<TFn extends AnyAsyncFunction>(
|
|
269
286
|
fn: TFn,
|
|
270
|
-
initialOptions:
|
|
287
|
+
initialOptions: AsyncDebouncerOptions<TFn>,
|
|
271
288
|
) {
|
|
272
289
|
const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)
|
|
273
290
|
return asyncDebouncer.maybeExecute.bind(asyncDebouncer)
|
package/src/async-queuer.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseFunctionOrValue } from './utils'
|
|
2
|
+
import type { AnyAsyncFunction } from './types'
|
|
1
3
|
import type { QueuePosition } from './queuer'
|
|
2
4
|
|
|
3
5
|
export interface AsyncQueuerOptions<TValue> {
|
|
@@ -7,9 +9,11 @@ export interface AsyncQueuerOptions<TValue> {
|
|
|
7
9
|
*/
|
|
8
10
|
addItemsTo?: QueuePosition
|
|
9
11
|
/**
|
|
10
|
-
* Maximum number of concurrent tasks to process
|
|
12
|
+
* Maximum number of concurrent tasks to process.
|
|
13
|
+
* Can be a number or a function that returns a number.
|
|
14
|
+
* @default 1
|
|
11
15
|
*/
|
|
12
|
-
concurrency?: number
|
|
16
|
+
concurrency?: number | ((queuer: AsyncQueuer<TValue>) => number)
|
|
13
17
|
/**
|
|
14
18
|
* Maximum time in milliseconds that an item can stay in the queue
|
|
15
19
|
* If not provided, items will never expire
|
|
@@ -67,9 +71,11 @@ export interface AsyncQueuerOptions<TValue> {
|
|
|
67
71
|
*/
|
|
68
72
|
started?: boolean
|
|
69
73
|
/**
|
|
70
|
-
* Time in milliseconds to wait between processing items
|
|
74
|
+
* Time in milliseconds to wait between processing items.
|
|
75
|
+
* Can be a number or a function that returns a number.
|
|
76
|
+
* @default 0
|
|
71
77
|
*/
|
|
72
|
-
wait?: number
|
|
78
|
+
wait?: number | ((queuer: AsyncQueuer<TValue>) => number)
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
const defaultOptions: Required<AsyncQueuerOptions<any>> = {
|
|
@@ -160,6 +166,20 @@ export class AsyncQueuer<TValue> {
|
|
|
160
166
|
return this._options
|
|
161
167
|
}
|
|
162
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Returns the current wait time between processing items
|
|
171
|
+
*/
|
|
172
|
+
getWait(): number {
|
|
173
|
+
return parseFunctionOrValue(this._options.wait, this)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Returns the current concurrency limit
|
|
178
|
+
*/
|
|
179
|
+
getConcurrency(): number {
|
|
180
|
+
return parseFunctionOrValue(this._options.concurrency, this)
|
|
181
|
+
}
|
|
182
|
+
|
|
163
183
|
/**
|
|
164
184
|
* Processes items in the queuer
|
|
165
185
|
*/
|
|
@@ -173,7 +193,7 @@ export class AsyncQueuer<TValue> {
|
|
|
173
193
|
this.checkExpiredItems()
|
|
174
194
|
|
|
175
195
|
while (
|
|
176
|
-
this._activeItems.size < this.
|
|
196
|
+
this._activeItems.size < this.getConcurrency() &&
|
|
177
197
|
!this.getIsEmpty()
|
|
178
198
|
) {
|
|
179
199
|
const nextFn = this.getNextItem()
|
|
@@ -204,8 +224,9 @@ export class AsyncQueuer<TValue> {
|
|
|
204
224
|
}
|
|
205
225
|
this._onSettledCallbacks.forEach((cb) => cb(success ? res : error!))
|
|
206
226
|
|
|
207
|
-
|
|
208
|
-
|
|
227
|
+
const wait = this.getWait()
|
|
228
|
+
if (wait > 0) {
|
|
229
|
+
setTimeout(() => this.tick(), wait)
|
|
209
230
|
return
|
|
210
231
|
}
|
|
211
232
|
|
|
@@ -322,9 +343,9 @@ export class AsyncQueuer<TValue> {
|
|
|
322
343
|
* Adds a task to the queuer
|
|
323
344
|
*/
|
|
324
345
|
addItem(
|
|
325
|
-
fn:
|
|
346
|
+
fn: AnyAsyncFunction & { priority?: number },
|
|
326
347
|
position: QueuePosition = this._options.addItemsTo,
|
|
327
|
-
|
|
348
|
+
runOnItemsChange: boolean = true,
|
|
328
349
|
): Promise<TValue> {
|
|
329
350
|
if (this.getIsFull()) {
|
|
330
351
|
this._rejectionCount++
|
|
@@ -381,7 +402,7 @@ export class AsyncQueuer<TValue> {
|
|
|
381
402
|
}
|
|
382
403
|
}
|
|
383
404
|
|
|
384
|
-
if (
|
|
405
|
+
if (runOnItemsChange) {
|
|
385
406
|
this._options.onItemsChange(this)
|
|
386
407
|
}
|
|
387
408
|
|
|
@@ -557,9 +578,7 @@ export class AsyncQueuer<TValue> {
|
|
|
557
578
|
* @param options - Configuration options for the AsyncQueuer
|
|
558
579
|
* @returns A bound addItem function that can be used to add tasks to the queuer
|
|
559
580
|
*/
|
|
560
|
-
export function asyncQueue<TValue>(
|
|
561
|
-
options: Omit<AsyncQueuerOptions<TValue>, 'started'> = {},
|
|
562
|
-
) {
|
|
581
|
+
export function asyncQueue<TValue>(options: AsyncQueuerOptions<TValue>) {
|
|
563
582
|
const queuer = new AsyncQueuer<TValue>(options)
|
|
564
583
|
return queuer.addItem.bind(queuer)
|
|
565
584
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseFunctionOrValue } from './utils'
|
|
1
2
|
import type { AnyAsyncFunction } from './types'
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -6,13 +7,15 @@ import type { AnyAsyncFunction } from './types'
|
|
|
6
7
|
export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
|
|
7
8
|
/**
|
|
8
9
|
* Whether the rate limiter is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
10
|
+
* Can be a boolean or a function that returns a boolean.
|
|
9
11
|
* Defaults to true.
|
|
10
12
|
*/
|
|
11
|
-
enabled?: boolean
|
|
13
|
+
enabled?: boolean | ((rateLimiter: AsyncRateLimiter<TFn>) => boolean)
|
|
12
14
|
/**
|
|
13
|
-
* Maximum number of executions allowed within the time window
|
|
15
|
+
* Maximum number of executions allowed within the time window.
|
|
16
|
+
* Can be a number or a function that returns a number.
|
|
14
17
|
*/
|
|
15
|
-
limit: number
|
|
18
|
+
limit: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)
|
|
16
19
|
/**
|
|
17
20
|
* Optional error handler for when the rate-limited function throws
|
|
18
21
|
*/
|
|
@@ -33,9 +36,10 @@ export interface AsyncRateLimiterOptions<TFn extends AnyAsyncFunction> {
|
|
|
33
36
|
rateLimiter: AsyncRateLimiter<TFn>,
|
|
34
37
|
) => void
|
|
35
38
|
/**
|
|
36
|
-
* Time window in milliseconds within which the limit applies
|
|
39
|
+
* Time window in milliseconds within which the limit applies.
|
|
40
|
+
* Can be a number or a function that returns a number.
|
|
37
41
|
*/
|
|
38
|
-
window: number
|
|
42
|
+
window: number | ((rateLimiter: AsyncRateLimiter<TFn>) => number)
|
|
39
43
|
/**
|
|
40
44
|
* Type of window to use for rate limiting
|
|
41
45
|
* - 'fixed': Uses a fixed window that resets after the window period
|
|
@@ -127,6 +131,27 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
127
131
|
return this._options as Required<AsyncRateLimiterOptions<TFn>>
|
|
128
132
|
}
|
|
129
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Returns the current enabled state of the rate limiter
|
|
136
|
+
*/
|
|
137
|
+
getEnabled(): boolean {
|
|
138
|
+
return !!parseFunctionOrValue(this._options.enabled, this)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Returns the current limit of executions allowed within the time window
|
|
143
|
+
*/
|
|
144
|
+
getLimit(): number {
|
|
145
|
+
return parseFunctionOrValue(this._options.limit, this)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Returns the current time window in milliseconds
|
|
150
|
+
*/
|
|
151
|
+
getWindow(): number {
|
|
152
|
+
return parseFunctionOrValue(this._options.window, this)
|
|
153
|
+
}
|
|
154
|
+
|
|
130
155
|
/**
|
|
131
156
|
* Attempts to execute the rate-limited function if within the configured limits.
|
|
132
157
|
* Will reject execution if the number of calls in the current window exceeds the limit.
|
|
@@ -148,9 +173,12 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
148
173
|
): Promise<ReturnType<TFn> | undefined> {
|
|
149
174
|
this.cleanupOldExecutions()
|
|
150
175
|
|
|
176
|
+
const limit = this.getLimit()
|
|
177
|
+
const window = this.getWindow()
|
|
178
|
+
|
|
151
179
|
if (this._options.windowType === 'sliding') {
|
|
152
180
|
// For sliding window, we can execute if we have capacity in the current window
|
|
153
|
-
if (this._executionTimes.length <
|
|
181
|
+
if (this._executionTimes.length < limit) {
|
|
154
182
|
await this.executeFunction(...args)
|
|
155
183
|
return this._lastResult
|
|
156
184
|
}
|
|
@@ -158,9 +186,9 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
158
186
|
// For fixed window, we need to check if we're in a new window
|
|
159
187
|
const now = Date.now()
|
|
160
188
|
const oldestExecution = Math.min(...this._executionTimes)
|
|
161
|
-
const isNewWindow = oldestExecution +
|
|
189
|
+
const isNewWindow = oldestExecution + window <= now
|
|
162
190
|
|
|
163
|
-
if (isNewWindow || this._executionTimes.length <
|
|
191
|
+
if (isNewWindow || this._executionTimes.length < limit) {
|
|
164
192
|
await this.executeFunction(...args)
|
|
165
193
|
return this._lastResult
|
|
166
194
|
}
|
|
@@ -173,7 +201,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
173
201
|
private async executeFunction(
|
|
174
202
|
...args: Parameters<TFn>
|
|
175
203
|
): Promise<ReturnType<TFn> | undefined> {
|
|
176
|
-
if (!this.
|
|
204
|
+
if (!this.getEnabled()) return
|
|
177
205
|
this._isExecuting = true
|
|
178
206
|
const now = Date.now()
|
|
179
207
|
this._executionTimes.push(now)
|
|
@@ -203,7 +231,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
203
231
|
|
|
204
232
|
private cleanupOldExecutions(): void {
|
|
205
233
|
const now = Date.now()
|
|
206
|
-
const windowStart = now - this.
|
|
234
|
+
const windowStart = now - this.getWindow()
|
|
207
235
|
this._executionTimes = this._executionTimes.filter(
|
|
208
236
|
(time) => time > windowStart,
|
|
209
237
|
)
|
|
@@ -214,7 +242,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
214
242
|
*/
|
|
215
243
|
getRemainingInWindow(): number {
|
|
216
244
|
this.cleanupOldExecutions()
|
|
217
|
-
return Math.max(0, this.
|
|
245
|
+
return Math.max(0, this.getLimit() - this._executionTimes.length)
|
|
218
246
|
}
|
|
219
247
|
|
|
220
248
|
/**
|
|
@@ -227,7 +255,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
227
255
|
return 0
|
|
228
256
|
}
|
|
229
257
|
const oldestExecution = Math.min(...this._executionTimes)
|
|
230
|
-
return oldestExecution + this.
|
|
258
|
+
return oldestExecution + this.getWindow() - Date.now()
|
|
231
259
|
}
|
|
232
260
|
|
|
233
261
|
/**
|
|
@@ -321,7 +349,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
321
349
|
*/
|
|
322
350
|
export function asyncRateLimit<TFn extends AnyAsyncFunction>(
|
|
323
351
|
fn: TFn,
|
|
324
|
-
initialOptions:
|
|
352
|
+
initialOptions: AsyncRateLimiterOptions<TFn>,
|
|
325
353
|
) {
|
|
326
354
|
const rateLimiter = new AsyncRateLimiter(fn, initialOptions)
|
|
327
355
|
return rateLimiter.maybeExecute.bind(rateLimiter)
|
package/src/async-throttler.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseFunctionOrValue } from './utils'
|
|
1
2
|
import type { AnyAsyncFunction } from './types'
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -6,9 +7,10 @@ import type { AnyAsyncFunction } from './types'
|
|
|
6
7
|
export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
|
|
7
8
|
/**
|
|
8
9
|
* Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
10
|
+
* Can be a boolean or a function that returns a boolean.
|
|
9
11
|
* Defaults to true.
|
|
10
12
|
*/
|
|
11
|
-
enabled?: boolean
|
|
13
|
+
enabled?: boolean | ((throttler: AsyncThrottler<TFn>) => boolean)
|
|
12
14
|
/**
|
|
13
15
|
* Whether to execute the function immediately when called
|
|
14
16
|
* Defaults to true
|
|
@@ -35,10 +37,11 @@ export interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {
|
|
|
35
37
|
*/
|
|
36
38
|
trailing?: boolean
|
|
37
39
|
/**
|
|
38
|
-
* Time window in milliseconds during which the function can only be executed once
|
|
40
|
+
* Time window in milliseconds during which the function can only be executed once.
|
|
41
|
+
* Can be a number or a function that returns a number.
|
|
39
42
|
* Defaults to 0ms
|
|
40
43
|
*/
|
|
41
|
-
wait: number
|
|
44
|
+
wait: number | ((throttler: AsyncThrottler<TFn>) => number)
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
const defaultOptions: Required<AsyncThrottlerOptions<any>> = {
|
|
@@ -120,6 +123,20 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
120
123
|
return this._options
|
|
121
124
|
}
|
|
122
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Returns the current enabled state of the throttler
|
|
128
|
+
*/
|
|
129
|
+
getEnabled(): boolean {
|
|
130
|
+
return parseFunctionOrValue(this._options.enabled, this)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Returns the current wait time in milliseconds
|
|
135
|
+
*/
|
|
136
|
+
getWait(): number {
|
|
137
|
+
return parseFunctionOrValue(this._options.wait, this)
|
|
138
|
+
}
|
|
139
|
+
|
|
123
140
|
/**
|
|
124
141
|
* Attempts to execute the throttled function
|
|
125
142
|
* If a call is already in progress, it may be blocked or queued depending on the `wait` option
|
|
@@ -129,9 +146,10 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
129
146
|
): Promise<ReturnType<TFn> | undefined> {
|
|
130
147
|
const now = Date.now()
|
|
131
148
|
const timeSinceLastExecution = now - this._lastExecutionTime
|
|
149
|
+
const wait = this.getWait()
|
|
132
150
|
|
|
133
151
|
// Handle leading execution
|
|
134
|
-
if (this._options.leading && timeSinceLastExecution >=
|
|
152
|
+
if (this._options.leading && timeSinceLastExecution >= wait) {
|
|
135
153
|
await this.executeFunction(...args)
|
|
136
154
|
return this._lastResult
|
|
137
155
|
} else {
|
|
@@ -149,7 +167,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
149
167
|
const _timeSinceLastExecution = this._lastExecutionTime
|
|
150
168
|
? now - this._lastExecutionTime
|
|
151
169
|
: 0
|
|
152
|
-
const timeoutDuration =
|
|
170
|
+
const timeoutDuration = wait - _timeSinceLastExecution
|
|
153
171
|
this._timeoutId = setTimeout(async () => {
|
|
154
172
|
if (this._lastArgs !== undefined) {
|
|
155
173
|
await this.executeFunction(...this._lastArgs)
|
|
@@ -164,7 +182,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
164
182
|
private async executeFunction(
|
|
165
183
|
...args: Parameters<TFn>
|
|
166
184
|
): Promise<ReturnType<TFn> | undefined> {
|
|
167
|
-
if (!this.
|
|
185
|
+
if (!this.getEnabled() || this._isExecuting) return undefined
|
|
168
186
|
this._abortController = new AbortController()
|
|
169
187
|
try {
|
|
170
188
|
this._isExecuting = true
|
|
@@ -179,7 +197,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
179
197
|
this._settleCount++
|
|
180
198
|
this._abortController = null
|
|
181
199
|
this._lastExecutionTime = Date.now()
|
|
182
|
-
this._nextExecutionTime = this._lastExecutionTime + this.
|
|
200
|
+
this._nextExecutionTime = this._lastExecutionTime + this.getWait()
|
|
183
201
|
this._options.onSettled(this)
|
|
184
202
|
}
|
|
185
203
|
return this._lastResult
|
|
@@ -246,7 +264,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
246
264
|
* Returns the current pending state
|
|
247
265
|
*/
|
|
248
266
|
getIsPending(): boolean {
|
|
249
|
-
return this.
|
|
267
|
+
return this.getEnabled() && !!this._timeoutId
|
|
250
268
|
}
|
|
251
269
|
|
|
252
270
|
/**
|
|
@@ -280,7 +298,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
280
298
|
*/
|
|
281
299
|
export function asyncThrottle<TFn extends AnyAsyncFunction>(
|
|
282
300
|
fn: TFn,
|
|
283
|
-
initialOptions:
|
|
301
|
+
initialOptions: AsyncThrottlerOptions<TFn>,
|
|
284
302
|
) {
|
|
285
303
|
const asyncThrottler = new AsyncThrottler(fn, initialOptions)
|
|
286
304
|
return asyncThrottler.maybeExecute.bind(asyncThrottler)
|
package/src/debouncer.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseFunctionOrValue } from './utils'
|
|
1
2
|
import type { AnyFunction } from './types'
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -6,9 +7,10 @@ import type { AnyFunction } from './types'
|
|
|
6
7
|
export interface DebouncerOptions<TFn extends AnyFunction> {
|
|
7
8
|
/**
|
|
8
9
|
* Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
10
|
+
* Can be a boolean or a function that returns a boolean.
|
|
9
11
|
* Defaults to true.
|
|
10
12
|
*/
|
|
11
|
-
enabled?: boolean
|
|
13
|
+
enabled?: boolean | ((debouncer: Debouncer<TFn>) => boolean)
|
|
12
14
|
/**
|
|
13
15
|
* Whether to execute on the leading edge of the timeout.
|
|
14
16
|
* The first call will execute immediately and the rest will wait the delay.
|
|
@@ -25,10 +27,11 @@ export interface DebouncerOptions<TFn extends AnyFunction> {
|
|
|
25
27
|
*/
|
|
26
28
|
trailing?: boolean
|
|
27
29
|
/**
|
|
28
|
-
* Delay in milliseconds before executing the function
|
|
30
|
+
* Delay in milliseconds before executing the function.
|
|
31
|
+
* Can be a number or a function that returns a number.
|
|
29
32
|
* Defaults to 0ms
|
|
30
33
|
*/
|
|
31
|
-
wait: number
|
|
34
|
+
wait: number | ((debouncer: Debouncer<TFn>) => number)
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
const defaultOptions: Required<DebouncerOptions<any>> = {
|
|
@@ -99,6 +102,20 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
99
102
|
return this._options
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Returns the current enabled state of the debouncer
|
|
107
|
+
*/
|
|
108
|
+
getEnabled(): boolean {
|
|
109
|
+
return parseFunctionOrValue(this._options.enabled, this)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Returns the current wait time in milliseconds
|
|
114
|
+
*/
|
|
115
|
+
getWait(): number {
|
|
116
|
+
return parseFunctionOrValue(this._options.wait, this)
|
|
117
|
+
}
|
|
118
|
+
|
|
102
119
|
/**
|
|
103
120
|
* Attempts to execute the debounced function
|
|
104
121
|
* If a call is already in progress, it will be queued
|
|
@@ -127,11 +144,11 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
127
144
|
if (this._options.trailing && !_didLeadingExecute) {
|
|
128
145
|
this.executeFunction(...args)
|
|
129
146
|
}
|
|
130
|
-
}, this.
|
|
147
|
+
}, this.getWait())
|
|
131
148
|
}
|
|
132
149
|
|
|
133
150
|
private executeFunction(...args: Parameters<TFn>): void {
|
|
134
|
-
if (!this.
|
|
151
|
+
if (!this.getEnabled()) return undefined
|
|
135
152
|
this.fn(...args) // EXECUTE!
|
|
136
153
|
this._isPending = false
|
|
137
154
|
this._executionCount++
|
|
@@ -160,7 +177,7 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
160
177
|
* Returns `true` if debouncing
|
|
161
178
|
*/
|
|
162
179
|
getIsPending(): boolean {
|
|
163
|
-
return this.
|
|
180
|
+
return this.getEnabled() && this._isPending
|
|
164
181
|
}
|
|
165
182
|
}
|
|
166
183
|
|
|
@@ -186,7 +203,7 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
186
203
|
*/
|
|
187
204
|
export function debounce<TFn extends AnyFunction>(
|
|
188
205
|
fn: TFn,
|
|
189
|
-
initialOptions:
|
|
206
|
+
initialOptions: DebouncerOptions<TFn>,
|
|
190
207
|
): (...args: Parameters<TFn>) => void {
|
|
191
208
|
const debouncer = new Debouncer(fn, initialOptions)
|
|
192
209
|
return debouncer.maybeExecute.bind(debouncer)
|
package/src/queuer.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseFunctionOrValue } from './utils'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Options for configuring a Queuer instance
|
|
3
5
|
*/
|
|
@@ -60,9 +62,11 @@ export interface QueuerOptions<TValue> {
|
|
|
60
62
|
*/
|
|
61
63
|
started?: boolean
|
|
62
64
|
/**
|
|
63
|
-
* Time in milliseconds to wait between processing items
|
|
65
|
+
* Time in milliseconds to wait between processing items.
|
|
66
|
+
* Can be a number or a function that returns a number.
|
|
67
|
+
* @default 0
|
|
64
68
|
*/
|
|
65
|
-
wait?: number
|
|
69
|
+
wait?: number | ((queuer: Queuer<TValue>) => number)
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
const defaultOptions: Required<QueuerOptions<any>> = {
|
|
@@ -78,7 +82,7 @@ const defaultOptions: Required<QueuerOptions<any>> = {
|
|
|
78
82
|
onItemsChange: () => {},
|
|
79
83
|
onReject: () => {},
|
|
80
84
|
onExpire: () => {},
|
|
81
|
-
started:
|
|
85
|
+
started: true,
|
|
82
86
|
wait: 0,
|
|
83
87
|
}
|
|
84
88
|
|
|
@@ -178,6 +182,13 @@ export class Queuer<TValue> {
|
|
|
178
182
|
return this._options
|
|
179
183
|
}
|
|
180
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Returns the current wait time in milliseconds
|
|
187
|
+
*/
|
|
188
|
+
getWait(): number {
|
|
189
|
+
return parseFunctionOrValue(this._options.wait, this)
|
|
190
|
+
}
|
|
191
|
+
|
|
181
192
|
/**
|
|
182
193
|
* Processes items in the queuer
|
|
183
194
|
*/
|
|
@@ -197,9 +208,10 @@ export class Queuer<TValue> {
|
|
|
197
208
|
}
|
|
198
209
|
this._onItemsChanges.forEach((cb) => cb(nextItem))
|
|
199
210
|
|
|
200
|
-
|
|
211
|
+
const wait = this.getWait()
|
|
212
|
+
if (wait > 0) {
|
|
201
213
|
// Use setTimeout to wait before processing next item
|
|
202
|
-
setTimeout(() => this.tick(),
|
|
214
|
+
setTimeout(() => this.tick(), wait)
|
|
203
215
|
return
|
|
204
216
|
}
|
|
205
217
|
|
|
@@ -493,7 +505,7 @@ export class Queuer<TValue> {
|
|
|
493
505
|
* processPriority(3) // Processed before 1
|
|
494
506
|
* ```
|
|
495
507
|
*/
|
|
496
|
-
export function queue<TValue>(options: QueuerOptions<TValue>
|
|
497
|
-
const queuer = new Queuer<TValue>(
|
|
508
|
+
export function queue<TValue>(options: QueuerOptions<TValue>) {
|
|
509
|
+
const queuer = new Queuer<TValue>(options)
|
|
498
510
|
return queuer.addItem.bind(queuer)
|
|
499
511
|
}
|