@tanstack/pacer 0.1.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 +112 -63
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +66 -25
- package/dist/cjs/async-queuer.cjs +198 -124
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +91 -49
- package/dist/cjs/async-rate-limiter.cjs +83 -55
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +55 -28
- package/dist/cjs/async-throttler.cjs +121 -70
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +75 -25
- package/dist/cjs/debouncer.cjs +45 -23
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +27 -12
- 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 +161 -101
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +80 -38
- package/dist/cjs/rate-limiter.cjs +52 -44
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +38 -46
- package/dist/cjs/throttler.cjs +57 -44
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +35 -23
- package/dist/cjs/types.d.cts +8 -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 +66 -25
- package/dist/esm/async-debouncer.js +112 -63
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +91 -49
- package/dist/esm/async-queuer.js +198 -124
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +55 -28
- package/dist/esm/async-rate-limiter.js +83 -55
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +75 -25
- package/dist/esm/async-throttler.js +121 -70
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/debouncer.d.ts +27 -12
- package/dist/esm/debouncer.js +45 -23
- 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 +80 -38
- package/dist/esm/queuer.js +161 -101
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +38 -46
- package/dist/esm/rate-limiter.js +52 -44
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +35 -23
- package/dist/esm/throttler.js +57 -44
- package/dist/esm/throttler.js.map +1 -1
- package/dist/esm/types.d.ts +8 -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 +157 -88
- package/src/async-queuer.ts +266 -148
- package/src/async-rate-limiter.ts +123 -83
- package/src/async-throttler.ts +173 -89
- package/src/debouncer.ts +71 -42
- package/src/index.ts +2 -0
- package/src/queuer.ts +219 -114
- package/src/rate-limiter.ts +74 -88
- package/src/throttler.ts +83 -65
- package/src/types.ts +9 -0
- package/src/utils.ts +13 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function bindInstanceMethods(instance) {
|
|
2
|
+
return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).filter((key) => typeof instance[key] === "function").reduce((acc, key) => {
|
|
3
|
+
const method = instance[key];
|
|
4
|
+
if (typeof method === "function") {
|
|
5
|
+
acc[key] = method.bind(instance);
|
|
6
|
+
}
|
|
7
|
+
return acc;
|
|
8
|
+
}, {});
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
bindInstanceMethods
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["export function bindInstanceMethods<T extends Record<string, any>>(\n instance: T,\n): T {\n return Object.getOwnPropertyNames(Object.getPrototypeOf(instance))\n .filter((key) => typeof instance[key as keyof T] === 'function')\n .reduce((acc: any, key) => {\n const method = instance[key as keyof T]\n if (typeof method === 'function') {\n acc[key] = method.bind(instance)\n }\n return acc\n }, {} as T)\n}\n"],"names":[],"mappings":"AAAO,SAAS,oBACd,UACG;AACH,SAAO,OAAO,oBAAoB,OAAO,eAAe,QAAQ,CAAC,EAC9D,OAAO,CAAC,QAAQ,OAAO,SAAS,GAAc,MAAM,UAAU,EAC9D,OAAO,CAAC,KAAU,QAAQ;AACnB,UAAA,SAAS,SAAS,GAAc;AAClC,QAAA,OAAO,WAAW,YAAY;AAChC,UAAI,GAAG,IAAI,OAAO,KAAK,QAAQ;AAAA,IAAA;AAE1B,WAAA;AAAA,EACT,GAAG,EAAO;AACd;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/pacer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Utilities for debouncing, throttling, rate-limiting, queuing, and more.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -127,6 +127,13 @@
|
|
|
127
127
|
"default": "./dist/cjs/throttler.cjs"
|
|
128
128
|
}
|
|
129
129
|
},
|
|
130
|
+
"./types": {
|
|
131
|
+
"types": "./dist/esm/types.d.ts"
|
|
132
|
+
},
|
|
133
|
+
"./utils": {
|
|
134
|
+
"types": "./dist/esm/utils.d.ts",
|
|
135
|
+
"default": "./dist/esm/utils.js"
|
|
136
|
+
},
|
|
130
137
|
"./package.json": "./package.json"
|
|
131
138
|
},
|
|
132
139
|
"sideEffects": false,
|
package/src/async-debouncer.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import type { AnyAsyncFunction } from './types'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Options for configuring an async debounced function
|
|
3
5
|
*/
|
|
4
|
-
export interface AsyncDebouncerOptions {
|
|
6
|
+
export interface AsyncDebouncerOptions<TFn extends AnyAsyncFunction> {
|
|
5
7
|
/**
|
|
6
8
|
* Whether the debouncer is enabled. When disabled, maybeExecute will not trigger any executions.
|
|
7
9
|
* Defaults to true.
|
|
@@ -12,6 +14,18 @@ export interface AsyncDebouncerOptions {
|
|
|
12
14
|
* Defaults to false.
|
|
13
15
|
*/
|
|
14
16
|
leading?: boolean
|
|
17
|
+
/**
|
|
18
|
+
* Optional error handler for when the debounced function throws
|
|
19
|
+
*/
|
|
20
|
+
onError?: (error: unknown, debouncer: AsyncDebouncer<TFn>) => void
|
|
21
|
+
/**
|
|
22
|
+
* Optional callback to call when the debounced function is executed
|
|
23
|
+
*/
|
|
24
|
+
onSettled?: (debouncer: AsyncDebouncer<TFn>) => void
|
|
25
|
+
/**
|
|
26
|
+
* Optional callback to call when the debounced function is executed
|
|
27
|
+
*/
|
|
28
|
+
onSuccess?: (result: ReturnType<TFn>, debouncer: AsyncDebouncer<TFn>) => void
|
|
15
29
|
/**
|
|
16
30
|
* Whether to execute on the trailing edge of the timeout.
|
|
17
31
|
* Defaults to true.
|
|
@@ -22,17 +36,15 @@ export interface AsyncDebouncerOptions {
|
|
|
22
36
|
* Defaults to 0ms
|
|
23
37
|
*/
|
|
24
38
|
wait: number
|
|
25
|
-
/**
|
|
26
|
-
* Optional error handler for when the debounced function throws
|
|
27
|
-
*/
|
|
28
|
-
onError?: (error: unknown) => void
|
|
29
39
|
}
|
|
30
40
|
|
|
31
|
-
const defaultOptions: Required<AsyncDebouncerOptions
|
|
41
|
+
const defaultOptions: Required<AsyncDebouncerOptions<any>> = {
|
|
32
42
|
enabled: true,
|
|
33
43
|
leading: false,
|
|
34
|
-
trailing: true,
|
|
35
44
|
onError: () => {},
|
|
45
|
+
onSettled: () => {},
|
|
46
|
+
onSuccess: () => {},
|
|
47
|
+
trailing: true,
|
|
36
48
|
wait: 0,
|
|
37
49
|
}
|
|
38
50
|
|
|
@@ -48,33 +60,34 @@ const defaultOptions: Required<AsyncDebouncerOptions> = {
|
|
|
48
60
|
*
|
|
49
61
|
* @example
|
|
50
62
|
* ```ts
|
|
51
|
-
* const
|
|
63
|
+
* const asyncDebouncer = new AsyncDebouncer(async (value: string) => {
|
|
52
64
|
* await searchAPI(value);
|
|
53
65
|
* }, { wait: 500 });
|
|
54
66
|
*
|
|
55
67
|
* // Called on each keystroke but only executes after 500ms of no typing
|
|
56
68
|
* inputElement.addEventListener('input', () => {
|
|
57
|
-
*
|
|
69
|
+
* asyncDebouncer.maybeExecute(inputElement.value);
|
|
58
70
|
* });
|
|
59
71
|
* ```
|
|
60
72
|
*/
|
|
61
|
-
export class AsyncDebouncer<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
private
|
|
66
|
-
private
|
|
67
|
-
private
|
|
68
|
-
private
|
|
69
|
-
private
|
|
70
|
-
private
|
|
71
|
-
private
|
|
73
|
+
export class AsyncDebouncer<TFn extends AnyAsyncFunction> {
|
|
74
|
+
private _abortController: AbortController | null = null
|
|
75
|
+
private _canLeadingExecute = true
|
|
76
|
+
private _errorCount = 0
|
|
77
|
+
private _isExecuting = false
|
|
78
|
+
private _isPending = false
|
|
79
|
+
private _lastArgs: Parameters<TFn> | undefined
|
|
80
|
+
private _lastResult: ReturnType<TFn> | undefined
|
|
81
|
+
private _options: Required<AsyncDebouncerOptions<TFn>>
|
|
82
|
+
private _settleCount = 0
|
|
83
|
+
private _successCount = 0
|
|
84
|
+
private _timeoutId: NodeJS.Timeout | null = null
|
|
72
85
|
|
|
73
86
|
constructor(
|
|
74
87
|
private fn: TFn,
|
|
75
|
-
initialOptions: AsyncDebouncerOptions
|
|
88
|
+
initialOptions: AsyncDebouncerOptions<TFn>,
|
|
76
89
|
) {
|
|
77
|
-
this.
|
|
90
|
+
this._options = {
|
|
78
91
|
...defaultOptions,
|
|
79
92
|
...initialOptions,
|
|
80
93
|
}
|
|
@@ -84,91 +97,146 @@ export class AsyncDebouncer<
|
|
|
84
97
|
* Updates the debouncer options
|
|
85
98
|
* Returns the new options state
|
|
86
99
|
*/
|
|
87
|
-
setOptions(
|
|
88
|
-
newOptions
|
|
89
|
-
): Required<AsyncDebouncerOptions> {
|
|
90
|
-
this.options = {
|
|
91
|
-
...this.options,
|
|
92
|
-
...newOptions,
|
|
93
|
-
}
|
|
94
|
-
return this.options
|
|
95
|
-
}
|
|
100
|
+
setOptions(newOptions: Partial<AsyncDebouncerOptions<TFn>>): void {
|
|
101
|
+
this._options = { ...this._options, ...newOptions }
|
|
96
102
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return this.executionCount
|
|
103
|
+
// End the pending state if the debouncer is disabled
|
|
104
|
+
if (!this._options.enabled) {
|
|
105
|
+
this._isPending = false
|
|
106
|
+
}
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
/**
|
|
105
|
-
*
|
|
110
|
+
* Returns the current debouncer options
|
|
106
111
|
*/
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
clearTimeout(this.timeoutId)
|
|
110
|
-
this.timeoutId = null
|
|
111
|
-
}
|
|
112
|
-
if (this.abortController) {
|
|
113
|
-
this.abortController.abort()
|
|
114
|
-
this.abortController = null
|
|
115
|
-
}
|
|
116
|
-
this.lastArgs = undefined
|
|
117
|
-
this.canLeadingExecute = true
|
|
112
|
+
getOptions(): Required<AsyncDebouncerOptions<TFn>> {
|
|
113
|
+
return this._options
|
|
118
114
|
}
|
|
119
115
|
|
|
120
116
|
/**
|
|
121
117
|
* Attempts to execute the debounced function
|
|
122
118
|
* If a call is already in progress, it will be queued
|
|
123
119
|
*/
|
|
124
|
-
async maybeExecute(
|
|
125
|
-
|
|
126
|
-
|
|
120
|
+
async maybeExecute(
|
|
121
|
+
...args: Parameters<TFn>
|
|
122
|
+
): Promise<ReturnType<TFn> | undefined> {
|
|
123
|
+
this._cancel()
|
|
124
|
+
this._lastArgs = args
|
|
127
125
|
|
|
128
126
|
// Handle leading execution
|
|
129
|
-
if (this.
|
|
130
|
-
this.
|
|
127
|
+
if (this._options.leading && this._canLeadingExecute) {
|
|
128
|
+
this._canLeadingExecute = false
|
|
131
129
|
await this.executeFunction(...args)
|
|
130
|
+
return this._lastResult
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Handle trailing execution
|
|
134
|
+
if (this._options.trailing) {
|
|
135
|
+
this._isPending = true
|
|
132
136
|
}
|
|
133
137
|
|
|
134
138
|
return new Promise((resolve) => {
|
|
135
|
-
this.
|
|
136
|
-
if
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
this._timeoutId = setTimeout(async () => {
|
|
140
|
+
// Execute trailing if enabled
|
|
141
|
+
if (this._options.trailing && this._lastArgs) {
|
|
142
|
+
await this.executeFunction(...this._lastArgs)
|
|
139
143
|
}
|
|
140
144
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
this.isExecuting = true
|
|
147
|
-
if (this.lastArgs) {
|
|
148
|
-
await this.executeFunction(...this.lastArgs)
|
|
149
|
-
}
|
|
150
|
-
} catch (error) {
|
|
151
|
-
try {
|
|
152
|
-
this.options.onError(error)
|
|
153
|
-
} catch {
|
|
154
|
-
// Ignore errors from error handler
|
|
155
|
-
}
|
|
156
|
-
} finally {
|
|
157
|
-
this.isExecuting = false
|
|
158
|
-
this.abortController = null
|
|
159
|
-
resolve()
|
|
160
|
-
}
|
|
161
|
-
} else {
|
|
162
|
-
resolve()
|
|
163
|
-
}
|
|
164
|
-
}, this.options.wait)
|
|
145
|
+
// Reset state and resolve
|
|
146
|
+
this._canLeadingExecute = true
|
|
147
|
+
resolve(this._lastResult)
|
|
148
|
+
}, this._options.wait)
|
|
165
149
|
})
|
|
166
150
|
}
|
|
167
151
|
|
|
168
|
-
private async executeFunction(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
152
|
+
private async executeFunction(
|
|
153
|
+
...args: Parameters<TFn>
|
|
154
|
+
): Promise<ReturnType<TFn> | undefined> {
|
|
155
|
+
if (!this._options.enabled) return undefined
|
|
156
|
+
this._abortController = new AbortController()
|
|
157
|
+
try {
|
|
158
|
+
this._isExecuting = true
|
|
159
|
+
this._lastResult = await this.fn(...args) // EXECUTE!
|
|
160
|
+
this._successCount++
|
|
161
|
+
this._options.onSuccess(this._lastResult!, this)
|
|
162
|
+
} catch (error) {
|
|
163
|
+
this._errorCount++
|
|
164
|
+
this._options.onError(error, this)
|
|
165
|
+
} finally {
|
|
166
|
+
this._isExecuting = false
|
|
167
|
+
this._isPending = false
|
|
168
|
+
this._settleCount++
|
|
169
|
+
this._abortController = null
|
|
170
|
+
this._options.onSettled(this)
|
|
171
|
+
}
|
|
172
|
+
return this._lastResult
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Cancel without resetting _canLeadingExecute
|
|
177
|
+
*/
|
|
178
|
+
private _cancel(): void {
|
|
179
|
+
if (this._timeoutId) {
|
|
180
|
+
clearTimeout(this._timeoutId)
|
|
181
|
+
this._timeoutId = null
|
|
182
|
+
}
|
|
183
|
+
if (this._abortController) {
|
|
184
|
+
this._abortController.abort()
|
|
185
|
+
this._abortController = null
|
|
186
|
+
}
|
|
187
|
+
this._lastArgs = undefined
|
|
188
|
+
this._isPending = false
|
|
189
|
+
this._isExecuting = false
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Cancels any pending execution or aborts any execution in progress
|
|
194
|
+
*/
|
|
195
|
+
cancel(): void {
|
|
196
|
+
this._canLeadingExecute = true
|
|
197
|
+
this._cancel()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Returns the last result of the debounced function
|
|
202
|
+
*/
|
|
203
|
+
getLastResult(): ReturnType<TFn> | undefined {
|
|
204
|
+
return this._lastResult
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Returns the number of times the function has been executed successfully
|
|
209
|
+
*/
|
|
210
|
+
getSuccessCount(): number {
|
|
211
|
+
return this._successCount
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Returns the number of times the function has settled (completed or errored)
|
|
216
|
+
*/
|
|
217
|
+
getSettleCount(): number {
|
|
218
|
+
return this._settleCount
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Returns the number of times the function has errored
|
|
223
|
+
*/
|
|
224
|
+
getErrorCount(): number {
|
|
225
|
+
return this._errorCount
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Returns `true` if there is a pending execution queued up for trailing execution
|
|
230
|
+
*/
|
|
231
|
+
getIsPending(): boolean {
|
|
232
|
+
return this._options.enabled && this._isPending
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Returns `true` if there is currently an execution in progress
|
|
237
|
+
*/
|
|
238
|
+
getIsExecuting(): boolean {
|
|
239
|
+
return this._isExecuting
|
|
172
240
|
}
|
|
173
241
|
}
|
|
174
242
|
|
|
@@ -189,9 +257,10 @@ export class AsyncDebouncer<
|
|
|
189
257
|
* await debounced("third"); // Executes after 1s
|
|
190
258
|
* ```
|
|
191
259
|
*/
|
|
192
|
-
export function asyncDebounce<
|
|
193
|
-
|
|
194
|
-
|
|
260
|
+
export function asyncDebounce<TFn extends AnyAsyncFunction>(
|
|
261
|
+
fn: TFn,
|
|
262
|
+
initialOptions: Omit<AsyncDebouncerOptions<TFn>, 'enabled'>,
|
|
263
|
+
) {
|
|
195
264
|
const asyncDebouncer = new AsyncDebouncer(fn, initialOptions)
|
|
196
265
|
return asyncDebouncer.maybeExecute.bind(asyncDebouncer)
|
|
197
266
|
}
|