@tanstack/pacer 0.8.0 → 0.9.1
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-batcher.cjs +163 -0
- package/dist/cjs/async-batcher.cjs.map +1 -0
- package/dist/cjs/async-batcher.d.cts +273 -0
- package/dist/cjs/async-debouncer.cjs +149 -162
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +76 -57
- package/dist/cjs/async-queuer.cjs +282 -343
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +121 -100
- package/dist/cjs/async-rate-limiter.cjs +128 -185
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +72 -61
- package/dist/cjs/async-throttler.cjs +168 -178
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +97 -69
- package/dist/cjs/batcher.cjs +110 -119
- package/dist/cjs/batcher.cjs.map +1 -1
- package/dist/cjs/batcher.d.cts +76 -51
- package/dist/cjs/debouncer.cjs +97 -85
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +54 -26
- package/dist/cjs/index.cjs +3 -6
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -1
- package/dist/cjs/queuer.cjs +247 -294
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +102 -81
- package/dist/cjs/rate-limiter.cjs +97 -130
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +50 -37
- package/dist/cjs/throttler.cjs +107 -123
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +59 -35
- package/dist/cjs/utils.cjs +0 -13
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +0 -1
- package/dist/esm/async-batcher.d.ts +273 -0
- package/dist/esm/async-batcher.js +163 -0
- package/dist/esm/async-batcher.js.map +1 -0
- package/dist/esm/async-debouncer.d.ts +76 -57
- package/dist/esm/async-debouncer.js +149 -162
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +121 -100
- package/dist/esm/async-queuer.js +282 -343
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +72 -61
- package/dist/esm/async-rate-limiter.js +128 -185
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +97 -69
- package/dist/esm/async-throttler.js +168 -178
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/batcher.d.ts +76 -51
- package/dist/esm/batcher.js +110 -119
- package/dist/esm/batcher.js.map +1 -1
- package/dist/esm/debouncer.d.ts +54 -26
- package/dist/esm/debouncer.js +97 -85
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +4 -7
- package/dist/esm/queuer.d.ts +102 -81
- package/dist/esm/queuer.js +247 -294
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +50 -37
- package/dist/esm/rate-limiter.js +97 -130
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +59 -35
- package/dist/esm/throttler.js +107 -123
- package/dist/esm/throttler.js.map +1 -1
- package/dist/esm/utils.d.ts +0 -1
- package/dist/esm/utils.js +0 -13
- package/dist/esm/utils.js.map +1 -1
- package/package.json +14 -11
- package/src/async-batcher.ts +475 -0
- package/src/async-debouncer.ts +201 -121
- package/src/async-queuer.ts +337 -216
- package/src/async-rate-limiter.ts +176 -136
- package/src/async-throttler.ts +233 -139
- package/src/batcher.ts +158 -92
- package/src/debouncer.ts +135 -52
- package/src/index.ts +1 -1
- package/src/queuer.ts +349 -226
- package/src/rate-limiter.ts +125 -80
- package/src/throttler.ts +152 -78
- package/src/utils.ts +0 -15
- package/dist/cjs/compare.cjs +0 -72
- package/dist/cjs/compare.cjs.map +0 -1
- package/dist/cjs/compare.d.cts +0 -12
- package/dist/esm/compare.d.ts +0 -12
- package/dist/esm/compare.js +0 -72
- package/dist/esm/compare.js.map +0 -1
- package/src/compare.ts +0 -105
|
@@ -1,4 +1,19 @@
|
|
|
1
|
+
import { Store } from "@tanstack/store";
|
|
1
2
|
import { parseFunctionOrValue } from "./utils.js";
|
|
3
|
+
function getDefaultAsyncThrottlerState() {
|
|
4
|
+
return structuredClone({
|
|
5
|
+
errorCount: 0,
|
|
6
|
+
isExecuting: false,
|
|
7
|
+
isPending: false,
|
|
8
|
+
lastArgs: void 0,
|
|
9
|
+
lastExecutionTime: 0,
|
|
10
|
+
lastResult: void 0,
|
|
11
|
+
nextExecutionTime: 0,
|
|
12
|
+
settleCount: 0,
|
|
13
|
+
status: "idle",
|
|
14
|
+
successCount: 0
|
|
15
|
+
});
|
|
16
|
+
}
|
|
2
17
|
const defaultOptions = {
|
|
3
18
|
enabled: true,
|
|
4
19
|
leading: true,
|
|
@@ -8,191 +23,166 @@ const defaultOptions = {
|
|
|
8
23
|
class AsyncThrottler {
|
|
9
24
|
constructor(fn, initialOptions) {
|
|
10
25
|
this.fn = fn;
|
|
11
|
-
this.
|
|
12
|
-
this
|
|
13
|
-
this
|
|
14
|
-
this
|
|
15
|
-
this.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
this._options = {
|
|
21
|
-
...defaultOptions,
|
|
22
|
-
...initialOptions,
|
|
23
|
-
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError
|
|
26
|
+
this.store = new Store(getDefaultAsyncThrottlerState());
|
|
27
|
+
this.#abortController = null;
|
|
28
|
+
this.#timeoutId = null;
|
|
29
|
+
this.#resolvePreviousPromise = null;
|
|
30
|
+
this.setOptions = (newOptions) => {
|
|
31
|
+
this.options = { ...this.options, ...newOptions };
|
|
32
|
+
if (!this.#getEnabled()) {
|
|
33
|
+
this.cancel();
|
|
34
|
+
}
|
|
24
35
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* Returns the current options
|
|
37
|
-
*/
|
|
38
|
-
getOptions() {
|
|
39
|
-
return this._options;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Returns the current enabled state of the throttler
|
|
43
|
-
*/
|
|
44
|
-
getEnabled() {
|
|
45
|
-
return !!parseFunctionOrValue(this._options.enabled, this);
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Returns the current wait time in milliseconds
|
|
49
|
-
*/
|
|
50
|
-
getWait() {
|
|
51
|
-
return parseFunctionOrValue(this._options.wait, this);
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Attempts to execute the throttled function.
|
|
55
|
-
* If a call is already in progress, it may be blocked or queued depending on the `wait` option.
|
|
56
|
-
*
|
|
57
|
-
* Error Handling:
|
|
58
|
-
* - If the throttled function throws and no `onError` handler is configured,
|
|
59
|
-
* the error will be thrown from this method.
|
|
60
|
-
* - If an `onError` handler is configured, errors will be caught and passed to the handler,
|
|
61
|
-
* and this method will return undefined.
|
|
62
|
-
* - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.
|
|
63
|
-
*
|
|
64
|
-
* @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError
|
|
65
|
-
* @throws The error from the throttled function if no onError handler is configured
|
|
66
|
-
*/
|
|
67
|
-
async maybeExecute(...args) {
|
|
68
|
-
const now = Date.now();
|
|
69
|
-
const timeSinceLastExecution = now - this._lastExecutionTime;
|
|
70
|
-
const wait = this.getWait();
|
|
71
|
-
this.resolvePreviousPromise();
|
|
72
|
-
if (this._options.leading && timeSinceLastExecution >= wait) {
|
|
73
|
-
await this.execute(...args);
|
|
74
|
-
return this._lastResult;
|
|
75
|
-
} else {
|
|
76
|
-
this._lastArgs = args;
|
|
77
|
-
return new Promise((resolve) => {
|
|
78
|
-
this._resolvePreviousPromise = resolve;
|
|
79
|
-
if (this._timeoutId) {
|
|
80
|
-
clearTimeout(this._timeoutId);
|
|
81
|
-
}
|
|
82
|
-
if (this._options.trailing) {
|
|
83
|
-
const _timeSinceLastExecution = this._lastExecutionTime ? now - this._lastExecutionTime : 0;
|
|
84
|
-
const timeoutDuration = wait - _timeSinceLastExecution;
|
|
85
|
-
this._timeoutId = setTimeout(async () => {
|
|
86
|
-
if (this._lastArgs !== void 0) {
|
|
87
|
-
await this.execute(...this._lastArgs);
|
|
88
|
-
}
|
|
89
|
-
this._resolvePreviousPromise = null;
|
|
90
|
-
resolve(this._lastResult);
|
|
91
|
-
}, timeoutDuration);
|
|
92
|
-
}
|
|
36
|
+
this.#setState = (newState) => {
|
|
37
|
+
this.store.setState((state) => {
|
|
38
|
+
const combinedState = {
|
|
39
|
+
...state,
|
|
40
|
+
...newState
|
|
41
|
+
};
|
|
42
|
+
const { isPending, isExecuting, settleCount } = combinedState;
|
|
43
|
+
return {
|
|
44
|
+
...combinedState,
|
|
45
|
+
status: !this.#getEnabled() ? "disabled" : isPending ? "pending" : isExecuting ? "executing" : settleCount > 0 ? "settled" : "idle"
|
|
46
|
+
};
|
|
93
47
|
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
this
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
this
|
|
107
|
-
|
|
108
|
-
if (this.
|
|
109
|
-
|
|
48
|
+
};
|
|
49
|
+
this.#getEnabled = () => {
|
|
50
|
+
return !!parseFunctionOrValue(this.options.enabled, this);
|
|
51
|
+
};
|
|
52
|
+
this.#getWait = () => {
|
|
53
|
+
return parseFunctionOrValue(this.options.wait, this);
|
|
54
|
+
};
|
|
55
|
+
this.maybeExecute = async (...args) => {
|
|
56
|
+
if (!this.#getEnabled()) return void 0;
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const timeSinceLastExecution = now - this.store.state.lastExecutionTime;
|
|
59
|
+
const wait = this.#getWait();
|
|
60
|
+
this.#setState({ lastArgs: args });
|
|
61
|
+
this.#resolvePreviousPromiseInternal();
|
|
62
|
+
if (this.options.leading && timeSinceLastExecution >= wait) {
|
|
63
|
+
await this.#execute(...args);
|
|
64
|
+
return this.store.state.lastResult;
|
|
110
65
|
} else {
|
|
111
|
-
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
this.#resolvePreviousPromise = resolve;
|
|
68
|
+
this.#clearTimeout();
|
|
69
|
+
if (this.options.trailing) {
|
|
70
|
+
const _timeSinceLastExecution = this.store.state.lastExecutionTime ? now - this.store.state.lastExecutionTime : 0;
|
|
71
|
+
const timeoutDuration = wait - _timeSinceLastExecution;
|
|
72
|
+
this.#setState({ isPending: true });
|
|
73
|
+
this.#timeoutId = setTimeout(async () => {
|
|
74
|
+
if (this.store.state.lastArgs !== void 0) {
|
|
75
|
+
await this.#execute(...this.store.state.lastArgs);
|
|
76
|
+
}
|
|
77
|
+
this.#resolvePreviousPromise = null;
|
|
78
|
+
resolve(this.store.state.lastResult);
|
|
79
|
+
}, timeoutDuration);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
112
82
|
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
this.
|
|
116
|
-
this
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
83
|
+
};
|
|
84
|
+
this.#execute = async (...args) => {
|
|
85
|
+
if (!this.#getEnabled() || this.store.state.isExecuting) return void 0;
|
|
86
|
+
this.#abortController = new AbortController();
|
|
87
|
+
try {
|
|
88
|
+
this.#setState({ isExecuting: true });
|
|
89
|
+
const result = await this.fn(...args);
|
|
90
|
+
this.#setState({
|
|
91
|
+
lastResult: result,
|
|
92
|
+
successCount: this.store.state.successCount + 1
|
|
93
|
+
});
|
|
94
|
+
this.options.onSuccess?.(result, this);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
this.#setState({
|
|
97
|
+
errorCount: this.store.state.errorCount + 1
|
|
98
|
+
});
|
|
99
|
+
this.options.onError?.(error, this);
|
|
100
|
+
if (this.options.throwOnError) {
|
|
101
|
+
throw error;
|
|
102
|
+
} else {
|
|
103
|
+
console.error(error);
|
|
104
|
+
}
|
|
105
|
+
} finally {
|
|
106
|
+
const lastExecutionTime = Date.now();
|
|
107
|
+
const nextExecutionTime = lastExecutionTime + this.#getWait();
|
|
108
|
+
this.#setState({
|
|
109
|
+
isExecuting: false,
|
|
110
|
+
isPending: false,
|
|
111
|
+
settleCount: this.store.state.settleCount + 1,
|
|
112
|
+
lastExecutionTime,
|
|
113
|
+
nextExecutionTime
|
|
114
|
+
});
|
|
115
|
+
this.#abortController = null;
|
|
116
|
+
this.options.onSettled?.(this);
|
|
117
|
+
}
|
|
118
|
+
return this.store.state.lastResult;
|
|
119
|
+
};
|
|
120
|
+
this.flush = () => {
|
|
121
|
+
if (this.store.state.isPending && this.store.state.lastArgs) {
|
|
122
|
+
this.#abortExecution();
|
|
123
|
+
this.#clearTimeout();
|
|
124
|
+
this.#execute(...this.store.state.lastArgs);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
this.#resolvePreviousPromiseInternal = () => {
|
|
128
|
+
if (this.#resolvePreviousPromise) {
|
|
129
|
+
this.#resolvePreviousPromise(this.store.state.lastResult);
|
|
130
|
+
this.#resolvePreviousPromise = null;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
this.#clearTimeout = () => {
|
|
134
|
+
if (this.#timeoutId) {
|
|
135
|
+
clearTimeout(this.#timeoutId);
|
|
136
|
+
this.#timeoutId = null;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
this.#cancelPendingExecution = () => {
|
|
140
|
+
this.#clearTimeout();
|
|
141
|
+
if (this.#resolvePreviousPromise) {
|
|
142
|
+
this.#resolvePreviousPromise(this.store.state.lastResult);
|
|
143
|
+
this.#resolvePreviousPromise = null;
|
|
144
|
+
}
|
|
145
|
+
this.#setState({
|
|
146
|
+
isPending: false,
|
|
147
|
+
isExecuting: false,
|
|
148
|
+
lastArgs: void 0
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
this.#abortExecution = () => {
|
|
152
|
+
if (this.#abortController) {
|
|
153
|
+
this.#abortController.abort();
|
|
154
|
+
this.#abortController = null;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
this.cancel = () => {
|
|
158
|
+
this.#cancelPendingExecution();
|
|
159
|
+
this.#abortExecution();
|
|
160
|
+
};
|
|
161
|
+
this.reset = () => {
|
|
162
|
+
this.#setState(getDefaultAsyncThrottlerState());
|
|
163
|
+
};
|
|
164
|
+
this.options = {
|
|
165
|
+
...defaultOptions,
|
|
166
|
+
...initialOptions,
|
|
167
|
+
throwOnError: initialOptions.throwOnError ?? !initialOptions.onError
|
|
168
|
+
};
|
|
169
|
+
this.#setState(this.options.initialState ?? {});
|
|
170
|
+
}
|
|
171
|
+
#abortController;
|
|
172
|
+
#timeoutId;
|
|
173
|
+
#resolvePreviousPromise;
|
|
174
|
+
#setState;
|
|
175
|
+
#getEnabled;
|
|
176
|
+
#getWait;
|
|
177
|
+
#execute;
|
|
178
|
+
#resolvePreviousPromiseInternal;
|
|
179
|
+
#clearTimeout;
|
|
180
|
+
#cancelPendingExecution;
|
|
181
|
+
#abortExecution;
|
|
192
182
|
}
|
|
193
183
|
function asyncThrottle(fn, initialOptions) {
|
|
194
184
|
const asyncThrottler = new AsyncThrottler(fn, initialOptions);
|
|
195
|
-
return asyncThrottler.maybeExecute
|
|
185
|
+
return asyncThrottler.maybeExecute;
|
|
196
186
|
}
|
|
197
187
|
export {
|
|
198
188
|
AsyncThrottler,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"async-throttler.js","sources":["../../src/async-throttler.ts"],"sourcesContent":["import { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\n/**\n * Options for configuring an async throttled function\n */\nexport interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.\n * Can be a boolean or a function that returns a boolean.\n * Defaults to true.\n */\n enabled?: boolean | ((throttler: AsyncThrottler<TFn>) => boolean)\n /**\n * Whether to execute the function immediately when called\n * Defaults to true\n */\n leading?: boolean\n /**\n * Optional error handler for when the throttled function throws.\n * If provided, the handler will be called with the error and throttler instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: unknown, asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSettled?: (asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n asyncThrottler: AsyncThrottler<TFn>,\n ) => void\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Whether to execute the function on the trailing edge of the wait period\n * Defaults to true\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: AsyncThrottler<TFn>) => number)\n}\n\ntype AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncThrottlerOptions<any>,\n 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async throttled function.\n *\n * Throttling limits how often a function can be executed, allowing only one execution within a specified time window.\n * Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a\n * regular interval regardless of how often it's called.\n *\n * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\n *\n * This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to\n * ensure a maximum execution frequency.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncThrottler instance\n *\n * @example\n * ```ts\n * const throttler = new AsyncThrottler(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // Will only execute once per second no matter how often called\n * // Returns the API response directly\n * const result = await throttler.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncThrottler<TFn extends AnyAsyncFunction> {\n private _options: AsyncThrottlerOptionsWithOptionalCallbacks\n private _abortController: AbortController | null = null\n private _errorCount = 0\n private _isExecuting = false\n private _lastArgs: Parameters<TFn> | undefined\n private _lastExecutionTime = 0\n private _lastResult: ReturnType<TFn> | undefined\n private _nextExecutionTime = 0\n private _settleCount = 0\n private _successCount = 0\n private _timeoutId: NodeJS.Timeout | null = null\n private _resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n ) {\n this._options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n }\n\n /**\n * Updates the throttler options\n */\n setOptions(newOptions: Partial<AsyncThrottlerOptions<TFn>>): void {\n this._options = { ...this._options, ...newOptions }\n\n // End the pending state if the debouncer is disabled\n if (!this._options.enabled) {\n this.cancel()\n }\n }\n\n /**\n * Returns the current options\n */\n getOptions(): AsyncThrottlerOptions<TFn> {\n return this._options\n }\n\n /**\n * Returns the current enabled state of the throttler\n */\n getEnabled(): boolean {\n return !!parseFunctionOrValue(this._options.enabled, this)\n }\n\n /**\n * Returns the current wait time in milliseconds\n */\n getWait(): number {\n return parseFunctionOrValue(this._options.wait, this)\n }\n\n /**\n * Attempts to execute the throttled function.\n * If a call is already in progress, it may be blocked or queued depending on the `wait` option.\n *\n * Error Handling:\n * - If the throttled function throws and no `onError` handler is configured,\n * the error will be thrown from this method.\n * - If an `onError` handler is configured, errors will be caught and passed to the handler,\n * and this method will return undefined.\n * - The error state can be checked using `getErrorCount()` and `getIsExecuting()`.\n *\n * @returns A promise that resolves with the function's return value, or undefined if an error occurred and was handled by onError\n * @throws The error from the throttled function if no onError handler is configured\n */\n async maybeExecute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n const now = Date.now()\n const timeSinceLastExecution = now - this._lastExecutionTime\n const wait = this.getWait()\n\n this.resolvePreviousPromise()\n\n // Handle leading execution\n if (this._options.leading && timeSinceLastExecution >= wait) {\n await this.execute(...args)\n return this._lastResult\n } else {\n // Store the most recent arguments for potential trailing execution\n this._lastArgs = args\n\n return new Promise((resolve) => {\n this._resolvePreviousPromise = resolve\n // Clear any existing timeout to ensure we use the latest arguments\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n }\n\n // Set up trailing execution if enabled\n if (this._options.trailing) {\n const _timeSinceLastExecution = this._lastExecutionTime\n ? now - this._lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this._timeoutId = setTimeout(async () => {\n if (this._lastArgs !== undefined) {\n await this.execute(...this._lastArgs)\n }\n this._resolvePreviousPromise = null\n resolve(this._lastResult)\n }, timeoutDuration)\n }\n })\n }\n }\n\n private async execute(\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> {\n if (!this.getEnabled() || this._isExecuting) return undefined\n this._abortController = new AbortController()\n try {\n this._isExecuting = true\n this._lastResult = await this.fn(...args) // EXECUTE!\n this._successCount++\n this._options.onSuccess?.(this._lastResult!, this)\n } catch (error) {\n this._errorCount++\n this._options.onError?.(error, this)\n if (this._options.throwOnError) {\n throw error\n } else {\n console.error(error)\n }\n } finally {\n this._isExecuting = false\n this._settleCount++\n this._abortController = null\n this._lastExecutionTime = Date.now()\n this._nextExecutionTime = this._lastExecutionTime + this.getWait()\n this._options.onSettled?.(this)\n }\n return this._lastResult\n }\n\n private resolvePreviousPromise(): void {\n if (this._resolvePreviousPromise) {\n this._resolvePreviousPromise(this._lastResult)\n this._resolvePreviousPromise = null\n }\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel(): void {\n if (this._timeoutId) {\n clearTimeout(this._timeoutId)\n this._timeoutId = null\n }\n if (this._abortController) {\n this._abortController.abort()\n this._abortController = null\n }\n this.resolvePreviousPromise()\n this._lastArgs = undefined\n }\n\n /**\n * Returns the last execution time\n */\n getLastExecutionTime(): number {\n return this._lastExecutionTime\n }\n\n /**\n * Returns the next execution time\n */\n getNextExecutionTime(): number {\n return this._nextExecutionTime\n }\n\n /**\n * Returns the last result of the debounced function\n */\n getLastResult(): ReturnType<TFn> | undefined {\n return this._lastResult\n }\n\n /**\n * Returns the number of times the function has been executed successfully\n */\n getSuccessCount(): number {\n return this._successCount\n }\n\n /**\n * Returns the number of times the function has settled (completed or errored)\n */\n getSettleCount(): number {\n return this._settleCount\n }\n\n /**\n * Returns the number of times the function has errored\n */\n getErrorCount(): number {\n return this._errorCount\n }\n\n /**\n * Returns the current pending state\n */\n getIsPending(): boolean {\n return this.getEnabled() && !!this._timeoutId\n }\n\n /**\n * Returns the current executing state\n */\n getIsExecuting(): boolean {\n return this._isExecuting\n }\n}\n\n/**\n * Creates an async throttled function that limits how often the function can execute.\n * The throttled function will execute at most once per wait period, even if called multiple times.\n * If called while executing, it will wait until execution completes before scheduling the next call.\n *\n * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncThrottler instance\n *\n * @example\n * ```ts\n * const throttled = asyncThrottle(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // This will execute at most once per second\n * // Returns the API response directly\n * const result = await throttled(inputElement.value);\n * ```\n */\nexport function asyncThrottle<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n) {\n const asyncThrottler = new AsyncThrottler(fn, initialOptions)\n return asyncThrottler.maybeExecute.bind(asyncThrottler)\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"async-throttler.js","sources":["../../src/async-throttler.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { parseFunctionOrValue } from './utils'\nimport type { AnyAsyncFunction, OptionalKeys } from './types'\n\nexport interface AsyncThrottlerState<TFn extends AnyAsyncFunction> {\n /**\n * Number of function executions that have resulted in errors\n */\n errorCount: number\n /**\n * Whether the throttled function is currently executing asynchronously\n */\n isExecuting: boolean\n /**\n * Whether the throttler is waiting for the timeout to trigger execution\n */\n isPending: boolean\n /**\n * The arguments from the most recent call to maybeExecute\n */\n lastArgs: Parameters<TFn> | undefined\n /**\n * Timestamp of the last function execution in milliseconds\n */\n lastExecutionTime: number\n /**\n * The result from the most recent successful function execution\n */\n lastResult: ReturnType<TFn> | undefined\n /**\n * Timestamp when the next execution can occur in milliseconds\n */\n nextExecutionTime: number\n /**\n * Number of function executions that have completed (either successfully or with errors)\n */\n settleCount: number\n /**\n * Current execution status - 'idle' when not active, 'pending' when waiting, 'executing' when running, 'settled' when completed\n */\n status: 'disabled' | 'idle' | 'pending' | 'executing' | 'settled'\n /**\n * Number of function executions that have completed successfully\n */\n successCount: number\n}\n\nfunction getDefaultAsyncThrottlerState<\n TFn extends AnyAsyncFunction,\n>(): AsyncThrottlerState<TFn> {\n return structuredClone({\n errorCount: 0,\n isExecuting: false,\n isPending: false,\n lastArgs: undefined,\n lastExecutionTime: 0,\n lastResult: undefined,\n nextExecutionTime: 0,\n settleCount: 0,\n status: 'idle',\n successCount: 0,\n })\n}\n\n/**\n * Options for configuring an async throttled function\n */\nexport interface AsyncThrottlerOptions<TFn extends AnyAsyncFunction> {\n /**\n * Whether the throttler is enabled. When disabled, maybeExecute will not trigger any executions.\n * Can be a boolean or a function that returns a boolean.\n * Defaults to true.\n */\n enabled?: boolean | ((throttler: AsyncThrottler<TFn>) => boolean)\n /**\n * Initial state for the async throttler\n */\n initialState?: Partial<AsyncThrottlerState<TFn>>\n /**\n * Whether to execute the function immediately when called\n * Defaults to true\n */\n leading?: boolean\n /**\n * Optional error handler for when the throttled function throws.\n * If provided, the handler will be called with the error and throttler instance.\n * This can be used alongside throwOnError - the handler will be called before any error is thrown.\n */\n onError?: (error: unknown, asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSettled?: (asyncThrottler: AsyncThrottler<TFn>) => void\n /**\n * Optional function to call when the throttled function is executed\n */\n onSuccess?: (\n result: ReturnType<TFn>,\n asyncThrottler: AsyncThrottler<TFn>,\n ) => void\n /**\n * Whether to throw errors when they occur.\n * Defaults to true if no onError handler is provided, false if an onError handler is provided.\n * Can be explicitly set to override these defaults.\n */\n throwOnError?: boolean\n /**\n * Whether to execute the function on the trailing edge of the wait period\n * Defaults to true\n */\n trailing?: boolean\n /**\n * Time window in milliseconds during which the function can only be executed once.\n * Can be a number or a function that returns a number.\n * Defaults to 0ms\n */\n wait: number | ((throttler: AsyncThrottler<TFn>) => number)\n}\n\ntype AsyncThrottlerOptionsWithOptionalCallbacks = OptionalKeys<\n AsyncThrottlerOptions<any>,\n 'initialState' | 'onError' | 'onSettled' | 'onSuccess'\n>\n\nconst defaultOptions: AsyncThrottlerOptionsWithOptionalCallbacks = {\n enabled: true,\n leading: true,\n trailing: true,\n wait: 0,\n}\n\n/**\n * A class that creates an async throttled function.\n *\n * Throttling limits how often a function can be executed, allowing only one execution within a specified time window.\n * Unlike debouncing which resets the delay timer on each call, throttling ensures the function executes at a\n * regular interval regardless of how often it's called.\n *\n * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\n *\n * This is useful for rate-limiting API calls, handling scroll/resize events, or any scenario where you want to\n * ensure a maximum execution frequency.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncThrottler instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async throttler\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - The state includes error count, execution status, last execution time, and success/settle counts\n * - State can be accessed via `asyncThrottler.store.state` when using the class directly\n * - When using framework adapters (React/Solid), state is accessed from `asyncThrottler.state`\n *\n * @example\n * ```ts\n * const throttler = new AsyncThrottler(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // Will only execute once per second no matter how often called\n * // Returns the API response directly\n * const result = await throttler.maybeExecute(inputElement.value);\n * ```\n */\nexport class AsyncThrottler<TFn extends AnyAsyncFunction> {\n readonly store: Store<Readonly<AsyncThrottlerState<TFn>>> = new Store<\n AsyncThrottlerState<TFn>\n >(getDefaultAsyncThrottlerState<TFn>())\n options: AsyncThrottlerOptions<TFn>\n #abortController: AbortController | null = null\n #timeoutId: NodeJS.Timeout | null = null\n #resolvePreviousPromise:\n | ((value?: ReturnType<TFn> | undefined) => void)\n | null = null\n\n constructor(\n private fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n ) {\n this.options = {\n ...defaultOptions,\n ...initialOptions,\n throwOnError: initialOptions.throwOnError ?? !initialOptions.onError,\n }\n this.#setState(this.options.initialState ?? {})\n }\n\n /**\n * Updates the async throttler options\n */\n setOptions = (newOptions: Partial<AsyncThrottlerOptions<TFn>>): void => {\n this.options = { ...this.options, ...newOptions }\n\n // End the pending state if the throttler is disabled\n if (!this.#getEnabled()) {\n this.cancel()\n }\n }\n\n #setState = (newState: Partial<AsyncThrottlerState<TFn>>): void => {\n this.store.setState((state) => {\n const combinedState = {\n ...state,\n ...newState,\n }\n const { isPending, isExecuting, settleCount } = combinedState\n return {\n ...combinedState,\n status: !this.#getEnabled()\n ? 'disabled'\n : isPending\n ? 'pending'\n : isExecuting\n ? 'executing'\n : settleCount > 0\n ? 'settled'\n : 'idle',\n }\n })\n }\n\n /**\n * Returns the current enabled state of the async throttler\n */\n #getEnabled = (): boolean => {\n return !!parseFunctionOrValue(this.options.enabled, this)\n }\n\n /**\n * Returns the current wait time in milliseconds\n */\n #getWait = (): number => {\n return parseFunctionOrValue(this.options.wait, this)\n }\n\n /**\n * Attempts to execute the throttled function. The execution behavior depends on the throttler options:\n *\n * - If enough time has passed since the last execution (>= wait period):\n * - With leading=true: Executes immediately\n * - With leading=false: Waits for the next trailing execution\n *\n * - If within the wait period:\n * - With trailing=true: Schedules execution for end of wait period\n * - With trailing=false: Drops the execution\n *\n * @example\n * ```ts\n * const throttled = new AsyncThrottler(fn, { wait: 1000 });\n *\n * // First call executes immediately\n * await throttled.maybeExecute('a', 'b');\n *\n * // Call during wait period - gets throttled\n * await throttled.maybeExecute('c', 'd');\n * ```\n */\n maybeExecute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled()) return undefined\n const now = Date.now()\n const timeSinceLastExecution = now - this.store.state.lastExecutionTime\n const wait = this.#getWait()\n // Store the most recent arguments for potential trailing execution\n this.#setState({ lastArgs: args })\n\n this.#resolvePreviousPromiseInternal()\n\n // Handle leading execution\n if (this.options.leading && timeSinceLastExecution >= wait) {\n await this.#execute(...args)\n return this.store.state.lastResult\n } else {\n return new Promise((resolve) => {\n this.#resolvePreviousPromise = resolve\n // Clear any existing timeout to ensure we use the latest arguments\n this.#clearTimeout()\n\n // Set up trailing execution if enabled\n if (this.options.trailing) {\n const _timeSinceLastExecution = this.store.state.lastExecutionTime\n ? now - this.store.state.lastExecutionTime\n : 0\n const timeoutDuration = wait - _timeSinceLastExecution\n this.#setState({ isPending: true })\n this.#timeoutId = setTimeout(async () => {\n if (this.store.state.lastArgs !== undefined) {\n await this.#execute(...this.store.state.lastArgs)\n }\n this.#resolvePreviousPromise = null\n resolve(this.store.state.lastResult)\n }, timeoutDuration)\n }\n })\n }\n }\n\n #execute = async (\n ...args: Parameters<TFn>\n ): Promise<ReturnType<TFn> | undefined> => {\n if (!this.#getEnabled() || this.store.state.isExecuting) return undefined\n this.#abortController = new AbortController()\n try {\n this.#setState({ isExecuting: true })\n const result = await this.fn(...args) // EXECUTE!\n this.#setState({\n lastResult: result,\n successCount: this.store.state.successCount + 1,\n })\n this.options.onSuccess?.(result, this)\n } catch (error) {\n this.#setState({\n errorCount: this.store.state.errorCount + 1,\n })\n this.options.onError?.(error, this)\n if (this.options.throwOnError) {\n throw error\n } else {\n console.error(error)\n }\n } finally {\n const lastExecutionTime = Date.now()\n const nextExecutionTime = lastExecutionTime + this.#getWait()\n this.#setState({\n isExecuting: false,\n isPending: false,\n settleCount: this.store.state.settleCount + 1,\n lastExecutionTime,\n nextExecutionTime,\n })\n this.#abortController = null\n this.options.onSettled?.(this)\n }\n return this.store.state.lastResult\n }\n\n /**\n * Processes the current pending execution immediately\n */\n flush = (): void => {\n if (this.store.state.isPending && this.store.state.lastArgs) {\n this.#abortExecution() // abort any current execution\n this.#clearTimeout() // clear any existing timeout\n this.#execute(...this.store.state.lastArgs)\n }\n }\n\n #resolvePreviousPromiseInternal = (): void => {\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n }\n\n #clearTimeout = (): void => {\n if (this.#timeoutId) {\n clearTimeout(this.#timeoutId)\n this.#timeoutId = null\n }\n }\n\n #cancelPendingExecution = (): void => {\n this.#clearTimeout()\n if (this.#resolvePreviousPromise) {\n this.#resolvePreviousPromise(this.store.state.lastResult)\n this.#resolvePreviousPromise = null\n }\n this.#setState({\n isPending: false,\n isExecuting: false,\n lastArgs: undefined,\n })\n }\n\n #abortExecution = (): void => {\n if (this.#abortController) {\n this.#abortController.abort()\n this.#abortController = null\n }\n }\n\n /**\n * Cancels any pending execution or aborts any execution in progress\n */\n cancel = (): void => {\n this.#cancelPendingExecution()\n this.#abortExecution()\n }\n\n /**\n * Resets the debouncer state to its default values\n */\n reset = (): void => {\n this.#setState(getDefaultAsyncThrottlerState<TFn>())\n }\n}\n\n/**\n * Creates an async throttled function that limits how often the function can execute.\n * The throttled function will execute at most once per wait period, even if called multiple times.\n * If called while executing, it will wait until execution completes before scheduling the next call.\n *\n * Unlike the non-async Throttler, this async version supports returning values from the throttled function,\n * making it ideal for API calls and other async operations where you want the result of the `maybeExecute` call\n * instead of setting the result on a state variable from within the throttled function.\n *\n * Error Handling:\n * - If an `onError` handler is provided, it will be called with the error and throttler instance\n * - If `throwOnError` is true (default when no onError handler is provided), the error will be thrown\n * - If `throwOnError` is false (default when onError handler is provided), the error will be swallowed\n * - Both onError and throwOnError can be used together - the handler will be called before any error is thrown\n * - The error state can be checked using the underlying AsyncThrottler instance\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - Use `initialState` to provide initial state values when creating the async throttler\n * - Use `onSuccess` callback to react to successful function execution and implement custom logic\n * - Use `onError` callback to react to function execution errors and implement custom error handling\n * - Use `onSettled` callback to react to function execution completion (success or error) and implement custom logic\n * - The state includes error count, execution status, last execution time, and success/settle counts\n * - State can be accessed via the underlying AsyncThrottler instance's `store.state` property\n * - When using framework adapters (React/Solid), state is accessed from the hook's state property\n *\n * @example\n * ```ts\n * const throttled = asyncThrottle(async (value: string) => {\n * const result = await saveToAPI(value);\n * return result; // Return value is preserved\n * }, {\n * wait: 1000,\n * onError: (error) => {\n * console.error('API call failed:', error);\n * }\n * });\n *\n * // This will execute at most once per second\n * // Returns the API response directly\n * const result = await throttled(inputElement.value);\n * ```\n */\nexport function asyncThrottle<TFn extends AnyAsyncFunction>(\n fn: TFn,\n initialOptions: AsyncThrottlerOptions<TFn>,\n) {\n const asyncThrottler = new AsyncThrottler(fn, initialOptions)\n return asyncThrottler.maybeExecute\n}\n"],"names":[],"mappings":"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"}
|
package/dist/esm/batcher.d.ts
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
import { Store } from '@tanstack/store';
|
|
2
|
+
import { OptionalKeys } from './types.js';
|
|
3
|
+
export interface BatcherState<TValue> {
|
|
4
|
+
/**
|
|
5
|
+
* Number of batch executions that have been completed
|
|
6
|
+
*/
|
|
7
|
+
executionCount: number;
|
|
8
|
+
/**
|
|
9
|
+
* Whether the batcher has no items to process (items array is empty)
|
|
10
|
+
*/
|
|
11
|
+
isEmpty: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Whether the batcher is waiting for the timeout to trigger batch processing
|
|
14
|
+
*/
|
|
15
|
+
isPending: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Whether the batcher is active and will process items automatically
|
|
18
|
+
*/
|
|
19
|
+
isRunning: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Total number of items that have been processed across all batches
|
|
22
|
+
*/
|
|
23
|
+
totalItemsProcessed: number;
|
|
24
|
+
/**
|
|
25
|
+
* Array of items currently queued for batch processing
|
|
26
|
+
*/
|
|
27
|
+
items: Array<TValue>;
|
|
28
|
+
/**
|
|
29
|
+
* Number of items currently in the batch queue
|
|
30
|
+
*/
|
|
31
|
+
size: number;
|
|
32
|
+
/**
|
|
33
|
+
* Current processing status - 'idle' when not processing, 'pending' when waiting for timeout
|
|
34
|
+
*/
|
|
35
|
+
status: 'idle' | 'pending';
|
|
36
|
+
}
|
|
1
37
|
/**
|
|
2
38
|
* Options for configuring a Batcher instance
|
|
3
39
|
*/
|
|
@@ -7,6 +43,10 @@ export interface BatcherOptions<TValue> {
|
|
|
7
43
|
* Return true to process the batch immediately
|
|
8
44
|
*/
|
|
9
45
|
getShouldExecute?: (items: Array<TValue>, batcher: Batcher<TValue>) => boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Initial state for the batcher
|
|
48
|
+
*/
|
|
49
|
+
initialState?: Partial<BatcherState<TValue>>;
|
|
10
50
|
/**
|
|
11
51
|
* Maximum number of items in a batch
|
|
12
52
|
* @default Infinity
|
|
@@ -16,10 +56,6 @@ export interface BatcherOptions<TValue> {
|
|
|
16
56
|
* Callback fired after a batch is processed
|
|
17
57
|
*/
|
|
18
58
|
onExecute?: (batcher: Batcher<TValue>) => void;
|
|
19
|
-
/**
|
|
20
|
-
* Callback fired when the batcher's running state changes
|
|
21
|
-
*/
|
|
22
|
-
onIsRunningChange?: (batcher: Batcher<TValue>) => void;
|
|
23
59
|
/**
|
|
24
60
|
* Callback fired after items are added to the batcher
|
|
25
61
|
*/
|
|
@@ -35,8 +71,9 @@ export interface BatcherOptions<TValue> {
|
|
|
35
71
|
* If not provided, the batch will not be triggered by a timeout.
|
|
36
72
|
* @default Infinity
|
|
37
73
|
*/
|
|
38
|
-
wait?: number;
|
|
74
|
+
wait?: number | ((batcher: Batcher<TValue>) => number);
|
|
39
75
|
}
|
|
76
|
+
type BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<Required<BatcherOptions<TValue>>, 'initialState' | 'onExecute' | 'onItemsChange'>;
|
|
40
77
|
/**
|
|
41
78
|
* A class that collects items and processes them in batches.
|
|
42
79
|
*
|
|
@@ -48,6 +85,15 @@ export interface BatcherOptions<TValue> {
|
|
|
48
85
|
* - Custom batch processing logic via getShouldExecute
|
|
49
86
|
* - Event callbacks for monitoring batch operations
|
|
50
87
|
*
|
|
88
|
+
* State Management:
|
|
89
|
+
* - Uses TanStack Store for reactive state management
|
|
90
|
+
* - Use `initialState` to provide initial state values when creating the batcher
|
|
91
|
+
* - Use `onExecute` callback to react to batch execution and implement custom logic
|
|
92
|
+
* - Use `onItemsChange` callback to react to items being added or removed from the batcher
|
|
93
|
+
* - The state includes batch execution count, total items processed, items, and running status
|
|
94
|
+
* - State can be accessed via `batcher.store.state` when using the class directly
|
|
95
|
+
* - When using framework adapters (React/Solid), state is accessed from `batcher.state`
|
|
96
|
+
*
|
|
51
97
|
* @example
|
|
52
98
|
* ```ts
|
|
53
99
|
* const batcher = new Batcher<number>(
|
|
@@ -55,7 +101,7 @@ export interface BatcherOptions<TValue> {
|
|
|
55
101
|
* {
|
|
56
102
|
* maxSize: 5,
|
|
57
103
|
* wait: 2000,
|
|
58
|
-
*
|
|
104
|
+
* onExecute: (batcher) => console.log('Batch executed:', batcher.peekAllItems())
|
|
59
105
|
* }
|
|
60
106
|
* );
|
|
61
107
|
*
|
|
@@ -63,83 +109,61 @@ export interface BatcherOptions<TValue> {
|
|
|
63
109
|
* batcher.addItem(2);
|
|
64
110
|
* // After 2 seconds or when 5 items are added, whichever comes first,
|
|
65
111
|
* // the batch will be processed
|
|
66
|
-
* // batcher.
|
|
112
|
+
* // batcher.flush() // manually trigger a batch
|
|
67
113
|
* ```
|
|
68
114
|
*/
|
|
69
115
|
export declare class Batcher<TValue> {
|
|
116
|
+
#private;
|
|
70
117
|
private fn;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
private _itemExecutionCount;
|
|
74
|
-
private _items;
|
|
75
|
-
private _running;
|
|
76
|
-
private _timeoutId;
|
|
118
|
+
readonly store: Store<Readonly<BatcherState<TValue>>>;
|
|
119
|
+
options: BatcherOptionsWithOptionalCallbacks<TValue>;
|
|
77
120
|
constructor(fn: (items: Array<TValue>) => void, initialOptions: BatcherOptions<TValue>);
|
|
78
121
|
/**
|
|
79
122
|
* Updates the batcher options
|
|
80
123
|
*/
|
|
81
|
-
setOptions(newOptions: Partial<BatcherOptions<TValue>>)
|
|
82
|
-
/**
|
|
83
|
-
* Returns the current batcher options
|
|
84
|
-
*/
|
|
85
|
-
getOptions(): BatcherOptions<TValue>;
|
|
124
|
+
setOptions: (newOptions: Partial<BatcherOptions<TValue>>) => void;
|
|
86
125
|
/**
|
|
87
126
|
* Adds an item to the batcher
|
|
88
127
|
* If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed
|
|
89
128
|
*/
|
|
90
|
-
addItem(item: TValue)
|
|
129
|
+
addItem: (item: TValue) => void;
|
|
91
130
|
/**
|
|
92
|
-
* Processes the current batch of items
|
|
93
|
-
* This method will automatically be triggered if the batcher is running and any of these conditions are met:
|
|
94
|
-
* - The number of items reaches batchSize
|
|
95
|
-
* - The wait duration has elapsed
|
|
96
|
-
* - The getShouldExecute function returns true upon adding an item
|
|
97
|
-
*
|
|
98
|
-
* You can also call this method manually to process the current batch at any time.
|
|
131
|
+
* Processes the current batch of items immediately
|
|
99
132
|
*/
|
|
100
|
-
|
|
133
|
+
flush: () => void;
|
|
101
134
|
/**
|
|
102
135
|
* Stops the batcher from processing batches
|
|
103
136
|
*/
|
|
104
|
-
stop()
|
|
137
|
+
stop: () => void;
|
|
105
138
|
/**
|
|
106
139
|
* Starts the batcher and processes any pending items
|
|
107
140
|
*/
|
|
108
|
-
start()
|
|
109
|
-
/**
|
|
110
|
-
* Returns the current number of items in the batcher
|
|
111
|
-
*/
|
|
112
|
-
getSize(): number;
|
|
113
|
-
/**
|
|
114
|
-
* Returns true if the batcher is empty
|
|
115
|
-
*/
|
|
116
|
-
getIsEmpty(): boolean;
|
|
117
|
-
/**
|
|
118
|
-
* Returns true if the batcher is running
|
|
119
|
-
*/
|
|
120
|
-
getIsRunning(): boolean;
|
|
141
|
+
start: () => void;
|
|
121
142
|
/**
|
|
122
|
-
* Returns a copy of all items
|
|
143
|
+
* Returns a copy of all items in the batcher
|
|
123
144
|
*/
|
|
124
|
-
peekAllItems()
|
|
145
|
+
peekAllItems: () => Array<TValue>;
|
|
125
146
|
/**
|
|
126
|
-
*
|
|
147
|
+
* Removes all items from the batcher
|
|
127
148
|
*/
|
|
128
|
-
|
|
149
|
+
clear: () => void;
|
|
129
150
|
/**
|
|
130
|
-
*
|
|
151
|
+
* Resets the batcher state to its default values
|
|
131
152
|
*/
|
|
132
|
-
|
|
153
|
+
reset: () => void;
|
|
133
154
|
}
|
|
134
155
|
/**
|
|
135
156
|
* Creates a batcher that processes items in batches
|
|
136
157
|
*
|
|
137
158
|
* @example
|
|
138
159
|
* ```ts
|
|
139
|
-
* const batchItems = batch<number>(
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
160
|
+
* const batchItems = batch<number>(
|
|
161
|
+
* (items) => console.log('Processing:', items),
|
|
162
|
+
* {
|
|
163
|
+
* maxSize: 3,
|
|
164
|
+
* onExecute: (batcher) => console.log('Batch executed')
|
|
165
|
+
* }
|
|
166
|
+
* );
|
|
143
167
|
*
|
|
144
168
|
* batchItems(1);
|
|
145
169
|
* batchItems(2);
|
|
@@ -147,3 +171,4 @@ export declare class Batcher<TValue> {
|
|
|
147
171
|
* ```
|
|
148
172
|
*/
|
|
149
173
|
export declare function batch<TValue>(fn: (items: Array<TValue>) => void, options: BatcherOptions<TValue>): (item: TValue) => void;
|
|
174
|
+
export {};
|