@tanstack/pacer 0.22.0 → 0.23.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/README.md +4 -4
- package/dist/async-batcher.d.ts +11 -13
- package/dist/async-batcher.js +185 -131
- package/dist/async-debouncer.d.ts +6 -8
- package/dist/async-debouncer.js +186 -142
- package/dist/async-queuer.d.ts +11 -13
- package/dist/async-queuer.js +383 -290
- package/dist/async-rate-limiter.d.ts +6 -8
- package/dist/async-rate-limiter.js +197 -143
- package/dist/async-retryer.d.ts +6 -8
- package/dist/async-retryer.js +209 -179
- package/dist/async-throttler.d.ts +6 -8
- package/dist/async-throttler.js +214 -157
- package/dist/batcher.d.ts +5 -7
- package/dist/batcher.js +107 -87
- package/dist/debouncer.d.ts +6 -8
- package/dist/debouncer.js +99 -86
- package/dist/event-client.d.ts +8 -10
- package/dist/event-client.js +1 -2
- package/dist/queuer.d.ts +7 -9
- package/dist/queuer.js +277 -212
- package/dist/rate-limiter.d.ts +6 -8
- package/dist/rate-limiter.js +127 -109
- package/dist/throttler.d.ts +6 -8
- package/dist/throttler.js +127 -90
- package/dist/types.d.ts +4 -6
- package/dist/utils.d.ts +3 -5
- package/dist/utils.js +1 -2
- package/package.json +21 -68
- package/dist/async-batcher.cjs +0 -337
- package/dist/async-batcher.cjs.map +0 -1
- package/dist/async-batcher.d.cts +0 -343
- package/dist/async-batcher.js.map +0 -1
- package/dist/async-debouncer.cjs +0 -327
- package/dist/async-debouncer.cjs.map +0 -1
- package/dist/async-debouncer.d.cts +0 -299
- package/dist/async-debouncer.js.map +0 -1
- package/dist/async-queuer.cjs +0 -516
- package/dist/async-queuer.cjs.map +0 -1
- package/dist/async-queuer.d.cts +0 -440
- package/dist/async-queuer.js.map +0 -1
- package/dist/async-rate-limiter.cjs +0 -370
- package/dist/async-rate-limiter.cjs.map +0 -1
- package/dist/async-rate-limiter.d.cts +0 -356
- package/dist/async-rate-limiter.js.map +0 -1
- package/dist/async-retryer.cjs +0 -365
- package/dist/async-retryer.cjs.map +0 -1
- package/dist/async-retryer.d.cts +0 -321
- package/dist/async-retryer.js.map +0 -1
- package/dist/async-throttler.cjs +0 -344
- package/dist/async-throttler.cjs.map +0 -1
- package/dist/async-throttler.d.cts +0 -319
- package/dist/async-throttler.js.map +0 -1
- package/dist/batcher.cjs +0 -200
- package/dist/batcher.cjs.map +0 -1
- package/dist/batcher.d.cts +0 -179
- package/dist/batcher.js.map +0 -1
- package/dist/debouncer.cjs +0 -203
- package/dist/debouncer.cjs.map +0 -1
- package/dist/debouncer.d.cts +0 -166
- package/dist/debouncer.js.map +0 -1
- package/dist/event-client.cjs +0 -64
- package/dist/event-client.cjs.map +0 -1
- package/dist/event-client.d.cts +0 -65
- package/dist/event-client.js.map +0 -1
- package/dist/index.cjs +0 -52
- package/dist/index.d.cts +0 -15
- package/dist/queuer.cjs +0 -406
- package/dist/queuer.cjs.map +0 -1
- package/dist/queuer.d.cts +0 -345
- package/dist/queuer.js.map +0 -1
- package/dist/rate-limiter.cjs +0 -263
- package/dist/rate-limiter.cjs.map +0 -1
- package/dist/rate-limiter.d.cts +0 -214
- package/dist/rate-limiter.js.map +0 -1
- package/dist/throttler.cjs +0 -215
- package/dist/throttler.cjs.map +0 -1
- package/dist/throttler.d.cts +0 -206
- package/dist/throttler.js.map +0 -1
- package/dist/types.cjs +0 -0
- package/dist/types.d.cts +0 -13
- package/dist/utils.cjs +0 -13
- package/dist/utils.cjs.map +0 -1
- package/dist/utils.d.cts +0 -7
- package/dist/utils.js.map +0 -1
- package/src/async-batcher.ts +0 -594
- package/src/async-debouncer.ts +0 -566
- package/src/async-queuer.ts +0 -988
- package/src/async-rate-limiter.ts +0 -648
- package/src/async-retryer.ts +0 -673
- package/src/async-throttler.ts +0 -634
- package/src/batcher.ts +0 -329
- package/src/debouncer.ts +0 -334
- package/src/event-client.ts +0 -129
- package/src/index.ts +0 -24
- package/src/queuer.ts +0 -751
- package/src/rate-limiter.ts +0 -429
- package/src/throttler.ts +0 -380
- package/src/types.ts +0 -12
- package/src/utils.ts +0 -12
package/dist/async-queuer.js
CHANGED
|
@@ -9,7 +9,7 @@ function getDefaultAsyncQueuerState() {
|
|
|
9
9
|
activeItems: [],
|
|
10
10
|
addItemCount: 0,
|
|
11
11
|
errorCount: 0,
|
|
12
|
-
|
|
12
|
+
executionCount: 0,
|
|
13
13
|
expirationCount: 0,
|
|
14
14
|
isEmpty: true,
|
|
15
15
|
isExecuting: false,
|
|
@@ -21,7 +21,7 @@ function getDefaultAsyncQueuerState() {
|
|
|
21
21
|
lastResult: null,
|
|
22
22
|
pendingTick: false,
|
|
23
23
|
rejectionCount: 0,
|
|
24
|
-
|
|
24
|
+
settleCount: 0,
|
|
25
25
|
size: 0,
|
|
26
26
|
status: "idle",
|
|
27
27
|
successCount: 0
|
|
@@ -111,287 +111,14 @@ const defaultOptions = {
|
|
|
111
111
|
* ```
|
|
112
112
|
*/
|
|
113
113
|
var AsyncQueuer = class {
|
|
114
|
-
|
|
114
|
+
fn;
|
|
115
|
+
store = new Store(getDefaultAsyncQueuerState());
|
|
116
|
+
key;
|
|
117
|
+
options;
|
|
118
|
+
asyncRetryers = /* @__PURE__ */ new Map();
|
|
119
|
+
#timeoutIds = /* @__PURE__ */ new Set();
|
|
115
120
|
constructor(fn, initialOptions = {}) {
|
|
116
121
|
this.fn = fn;
|
|
117
|
-
this.store = new Store(getDefaultAsyncQueuerState());
|
|
118
|
-
this.asyncRetryers = /* @__PURE__ */ new Map();
|
|
119
|
-
this.#timeoutIds = /* @__PURE__ */ new Set();
|
|
120
|
-
this.setOptions = (newOptions) => {
|
|
121
|
-
this.options = {
|
|
122
|
-
...this.options,
|
|
123
|
-
...newOptions
|
|
124
|
-
};
|
|
125
|
-
};
|
|
126
|
-
this.#setState = (newState) => {
|
|
127
|
-
this.store.setState((state) => {
|
|
128
|
-
const combinedState = {
|
|
129
|
-
...state,
|
|
130
|
-
...newState
|
|
131
|
-
};
|
|
132
|
-
const { activeItems, items, isRunning } = combinedState;
|
|
133
|
-
const size = items.length;
|
|
134
|
-
const isFull = size >= (this.options.maxSize ?? Infinity);
|
|
135
|
-
const isEmpty = size === 0;
|
|
136
|
-
const isIdle = isRunning && isEmpty && activeItems.length === 0;
|
|
137
|
-
const status = isIdle ? "idle" : isRunning ? "running" : "stopped";
|
|
138
|
-
return {
|
|
139
|
-
...combinedState,
|
|
140
|
-
isEmpty,
|
|
141
|
-
isFull,
|
|
142
|
-
isIdle,
|
|
143
|
-
size,
|
|
144
|
-
status
|
|
145
|
-
};
|
|
146
|
-
});
|
|
147
|
-
emitChange("AsyncQueuer", this);
|
|
148
|
-
};
|
|
149
|
-
this.#getWait = () => {
|
|
150
|
-
return parseFunctionOrValue(this.options.wait ?? 0, this);
|
|
151
|
-
};
|
|
152
|
-
this.#getConcurrency = () => {
|
|
153
|
-
return parseFunctionOrValue(this.options.concurrency ?? 1, this);
|
|
154
|
-
};
|
|
155
|
-
this.#tick = () => {
|
|
156
|
-
if (!this.store.state.isRunning) {
|
|
157
|
-
this.#setState({ pendingTick: false });
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
this.#setState({ pendingTick: true });
|
|
161
|
-
this.#checkExpiredItems();
|
|
162
|
-
let scheduledAsyncWork = false;
|
|
163
|
-
const activeItems = [...this.store.state.activeItems];
|
|
164
|
-
while (activeItems.length < this.#getConcurrency() && this.store.state.items.length > 0) {
|
|
165
|
-
const nextItem = this.peekNextItem();
|
|
166
|
-
if (nextItem === void 0) break;
|
|
167
|
-
activeItems.push(nextItem);
|
|
168
|
-
this.#setState({ activeItems: [...activeItems] });
|
|
169
|
-
scheduledAsyncWork = true;
|
|
170
|
-
(async () => {
|
|
171
|
-
try {
|
|
172
|
-
await this.execute();
|
|
173
|
-
} catch {}
|
|
174
|
-
const wait = this.#getWait();
|
|
175
|
-
if (wait > 0) {
|
|
176
|
-
const timeoutId = setTimeout(() => {
|
|
177
|
-
this.#timeoutIds.delete(timeoutId);
|
|
178
|
-
this.#tick();
|
|
179
|
-
}, wait);
|
|
180
|
-
this.#timeoutIds.add(timeoutId);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
this.#tick();
|
|
184
|
-
})();
|
|
185
|
-
}
|
|
186
|
-
if (!scheduledAsyncWork) this.#setState({ pendingTick: false });
|
|
187
|
-
};
|
|
188
|
-
this.addItem = (item, position = this.options.addItemsTo ?? "back", runOnItemsChange = true) => {
|
|
189
|
-
this.#setState({ addItemCount: this.store.state.addItemCount + 1 });
|
|
190
|
-
if (item === void 0) {
|
|
191
|
-
this.#setState({ rejectionCount: this.store.state.rejectionCount + 1 });
|
|
192
|
-
this.options.onReject?.(item, this);
|
|
193
|
-
return false;
|
|
194
|
-
}
|
|
195
|
-
if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {
|
|
196
|
-
this.#setState({ rejectionCount: this.store.state.rejectionCount + 1 });
|
|
197
|
-
this.options.onReject?.(item, this);
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
const priority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(item) : item?.priority;
|
|
201
|
-
const items = this.store.state.items;
|
|
202
|
-
const itemTimestamps = this.store.state.itemTimestamps;
|
|
203
|
-
if (priority !== void 0) {
|
|
204
|
-
const insertIndex = items.findIndex((existing) => {
|
|
205
|
-
return (this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(existing) : existing?.priority) < priority;
|
|
206
|
-
});
|
|
207
|
-
if (insertIndex === -1) {
|
|
208
|
-
items.push(item);
|
|
209
|
-
itemTimestamps.push(Date.now());
|
|
210
|
-
} else {
|
|
211
|
-
items.splice(insertIndex, 0, item);
|
|
212
|
-
itemTimestamps.splice(insertIndex, 0, Date.now());
|
|
213
|
-
}
|
|
214
|
-
} else if (position === "front") {
|
|
215
|
-
items.unshift(item);
|
|
216
|
-
itemTimestamps.unshift(Date.now());
|
|
217
|
-
} else {
|
|
218
|
-
items.push(item);
|
|
219
|
-
itemTimestamps.push(Date.now());
|
|
220
|
-
}
|
|
221
|
-
this.#setState({
|
|
222
|
-
items,
|
|
223
|
-
itemTimestamps
|
|
224
|
-
});
|
|
225
|
-
if (runOnItemsChange) this.options.onItemsChange?.(this);
|
|
226
|
-
if (this.store.state.isRunning && !this.store.state.pendingTick) this.#tick();
|
|
227
|
-
return true;
|
|
228
|
-
};
|
|
229
|
-
this.getNextItem = (position = this.options.getItemsFrom ?? "front") => {
|
|
230
|
-
const { items, itemTimestamps } = this.store.state;
|
|
231
|
-
let item;
|
|
232
|
-
if (this.options.getPriority !== defaultOptions.getPriority || position === "front") {
|
|
233
|
-
item = items[0];
|
|
234
|
-
if (item !== void 0) this.#setState({
|
|
235
|
-
items: items.slice(1),
|
|
236
|
-
itemTimestamps: itemTimestamps.slice(1)
|
|
237
|
-
});
|
|
238
|
-
} else {
|
|
239
|
-
item = items[items.length - 1];
|
|
240
|
-
if (item !== void 0) this.#setState({
|
|
241
|
-
items: items.slice(0, -1),
|
|
242
|
-
itemTimestamps: itemTimestamps.slice(0, -1)
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
if (item !== void 0) this.options.onItemsChange?.(this);
|
|
246
|
-
return item;
|
|
247
|
-
};
|
|
248
|
-
this.#getAllItems = () => {
|
|
249
|
-
const items = this.peekAllItems();
|
|
250
|
-
this.clear();
|
|
251
|
-
return items;
|
|
252
|
-
};
|
|
253
|
-
this.execute = async (position) => {
|
|
254
|
-
const item = this.getNextItem(position);
|
|
255
|
-
if (item !== void 0) {
|
|
256
|
-
const currentExecuteCount = this.store.state.executeCount + 1;
|
|
257
|
-
this.#setState({
|
|
258
|
-
executeCount: currentExecuteCount,
|
|
259
|
-
isExecuting: true
|
|
260
|
-
});
|
|
261
|
-
try {
|
|
262
|
-
const currentAsyncRetryer = new AsyncRetryer(this.fn, this.options.asyncRetryerOptions);
|
|
263
|
-
this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer);
|
|
264
|
-
const lastResult = await currentAsyncRetryer.execute(item);
|
|
265
|
-
this.#setState({
|
|
266
|
-
successCount: this.store.state.successCount + 1,
|
|
267
|
-
lastResult
|
|
268
|
-
});
|
|
269
|
-
this.options.onSuccess?.(lastResult, item, this);
|
|
270
|
-
} catch (error) {
|
|
271
|
-
this.#setState({ errorCount: this.store.state.errorCount + 1 });
|
|
272
|
-
this.options.onError?.(error, item, this);
|
|
273
|
-
if (this.options.throwOnError) throw error;
|
|
274
|
-
} finally {
|
|
275
|
-
this.asyncRetryers.delete(currentExecuteCount);
|
|
276
|
-
const remainingActiveItems = [...this.store.state.activeItems];
|
|
277
|
-
const activeItemIndex = remainingActiveItems.indexOf(item);
|
|
278
|
-
if (activeItemIndex !== -1) remainingActiveItems.splice(activeItemIndex, 1);
|
|
279
|
-
this.#setState({
|
|
280
|
-
activeItems: remainingActiveItems,
|
|
281
|
-
isExecuting: this.asyncRetryers.size > 0,
|
|
282
|
-
settledCount: this.store.state.settledCount + 1
|
|
283
|
-
});
|
|
284
|
-
this.options.onSettled?.(item, this);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return item;
|
|
288
|
-
};
|
|
289
|
-
this.flush = async (numberOfItems = this.store.state.items.length, position) => {
|
|
290
|
-
this.#clearTimeouts();
|
|
291
|
-
const results = await Promise.allSettled(Array.from({ length: numberOfItems }, () => this.execute(position)));
|
|
292
|
-
this.#setState({ pendingTick: false });
|
|
293
|
-
if (this.store.state.isRunning && this.store.state.items.length > 0) {
|
|
294
|
-
const wait = this.#getWait();
|
|
295
|
-
if (wait > 0) {
|
|
296
|
-
this.#setState({ pendingTick: true });
|
|
297
|
-
const timeoutId = setTimeout(() => {
|
|
298
|
-
this.#timeoutIds.delete(timeoutId);
|
|
299
|
-
this.#tick();
|
|
300
|
-
}, wait);
|
|
301
|
-
this.#timeoutIds.add(timeoutId);
|
|
302
|
-
} else this.#tick();
|
|
303
|
-
}
|
|
304
|
-
const failure = results.find((result) => result.status === "rejected");
|
|
305
|
-
if (failure) throw failure.reason;
|
|
306
|
-
};
|
|
307
|
-
this.flushAsBatch = async (batchFunction) => {
|
|
308
|
-
this.#clearTimeouts();
|
|
309
|
-
const items = this.#getAllItems();
|
|
310
|
-
try {
|
|
311
|
-
await batchFunction(items);
|
|
312
|
-
} finally {
|
|
313
|
-
this.#setState({ pendingTick: false });
|
|
314
|
-
if (this.store.state.isRunning && this.store.state.items.length > 0) this.#tick();
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
this.#checkExpiredItems = () => {
|
|
318
|
-
if ((this.options.expirationDuration ?? Infinity) === Infinity && this.options.getIsExpired === defaultOptions.getIsExpired) return;
|
|
319
|
-
const now = Date.now();
|
|
320
|
-
const expiredIndices = [];
|
|
321
|
-
for (let i = 0; i < this.store.state.items.length; i++) {
|
|
322
|
-
const timestamp = this.store.state.itemTimestamps[i];
|
|
323
|
-
if (timestamp === void 0) continue;
|
|
324
|
-
const item = this.store.state.items[i];
|
|
325
|
-
if (item === void 0) continue;
|
|
326
|
-
if (this.options.getIsExpired !== defaultOptions.getIsExpired ? this.options.getIsExpired(item, timestamp) : now - timestamp > (this.options.expirationDuration ?? Infinity)) expiredIndices.push(i);
|
|
327
|
-
}
|
|
328
|
-
for (let i = expiredIndices.length - 1; i >= 0; i--) {
|
|
329
|
-
const index = expiredIndices[i];
|
|
330
|
-
if (index === void 0) continue;
|
|
331
|
-
const expiredItem = this.store.state.items[index];
|
|
332
|
-
if (expiredItem === void 0) continue;
|
|
333
|
-
const newItems = [...this.store.state.items];
|
|
334
|
-
const newTimestamps = [...this.store.state.itemTimestamps];
|
|
335
|
-
newItems.splice(index, 1);
|
|
336
|
-
newTimestamps.splice(index, 1);
|
|
337
|
-
this.#setState({
|
|
338
|
-
items: newItems,
|
|
339
|
-
itemTimestamps: newTimestamps,
|
|
340
|
-
expirationCount: this.store.state.expirationCount + 1
|
|
341
|
-
});
|
|
342
|
-
this.options.onExpire?.(expiredItem, this);
|
|
343
|
-
}
|
|
344
|
-
if (expiredIndices.length > 0) this.options.onItemsChange?.(this);
|
|
345
|
-
};
|
|
346
|
-
this.peekNextItem = (position = "front") => {
|
|
347
|
-
if (position === "front") return this.store.state.items[0];
|
|
348
|
-
return this.store.state.items[this.store.state.items.length - 1];
|
|
349
|
-
};
|
|
350
|
-
this.peekAllItems = () => {
|
|
351
|
-
return [...this.peekActiveItems(), ...this.peekPendingItems()];
|
|
352
|
-
};
|
|
353
|
-
this.peekActiveItems = () => {
|
|
354
|
-
return [...this.store.state.activeItems];
|
|
355
|
-
};
|
|
356
|
-
this.peekPendingItems = () => {
|
|
357
|
-
return [...this.store.state.items];
|
|
358
|
-
};
|
|
359
|
-
this.start = () => {
|
|
360
|
-
this.#setState({ isRunning: true });
|
|
361
|
-
if (!this.store.state.pendingTick && this.store.state.items.length > 0) this.#tick();
|
|
362
|
-
};
|
|
363
|
-
this.stop = () => {
|
|
364
|
-
this.#clearTimeouts();
|
|
365
|
-
this.#setState({
|
|
366
|
-
isRunning: false,
|
|
367
|
-
pendingTick: false
|
|
368
|
-
});
|
|
369
|
-
};
|
|
370
|
-
this.#clearTimeouts = () => {
|
|
371
|
-
this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId));
|
|
372
|
-
this.#timeoutIds.clear();
|
|
373
|
-
};
|
|
374
|
-
this.clear = () => {
|
|
375
|
-
this.#setState({
|
|
376
|
-
items: [],
|
|
377
|
-
itemTimestamps: []
|
|
378
|
-
});
|
|
379
|
-
this.options.onItemsChange?.(this);
|
|
380
|
-
};
|
|
381
|
-
this.getAbortSignal = (executeCount) => {
|
|
382
|
-
const count = executeCount ?? this.store.state.executeCount;
|
|
383
|
-
return this.asyncRetryers.get(count)?.getAbortSignal() ?? null;
|
|
384
|
-
};
|
|
385
|
-
this.abort = () => {
|
|
386
|
-
this.asyncRetryers.forEach((retryer) => retryer.abort());
|
|
387
|
-
this.asyncRetryers.clear();
|
|
388
|
-
this.#setState({ isExecuting: false });
|
|
389
|
-
};
|
|
390
|
-
this.reset = () => {
|
|
391
|
-
this.#setState(getDefaultAsyncQueuerState());
|
|
392
|
-
this.options.onItemsChange?.(this);
|
|
393
|
-
this.asyncRetryers.forEach((retryer) => retryer.reset());
|
|
394
|
-
};
|
|
395
122
|
this.key = initialOptions.key;
|
|
396
123
|
this.options = {
|
|
397
124
|
...defaultOptions,
|
|
@@ -416,28 +143,395 @@ var AsyncQueuer = class {
|
|
|
416
143
|
this.setOptions(e.payload.options);
|
|
417
144
|
});
|
|
418
145
|
}
|
|
419
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Updates the queuer options. New options are merged with existing options.
|
|
148
|
+
*/
|
|
149
|
+
setOptions = (newOptions) => {
|
|
150
|
+
this.options = {
|
|
151
|
+
...this.options,
|
|
152
|
+
...newOptions
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
#setState = (newState) => {
|
|
156
|
+
this.store.setState((state) => {
|
|
157
|
+
const combinedState = {
|
|
158
|
+
...state,
|
|
159
|
+
...newState
|
|
160
|
+
};
|
|
161
|
+
const { activeItems, items, isRunning } = combinedState;
|
|
162
|
+
const size = items.length;
|
|
163
|
+
const isFull = size >= (this.options.maxSize ?? Infinity);
|
|
164
|
+
const isEmpty = size === 0;
|
|
165
|
+
const isIdle = isRunning && isEmpty && activeItems.length === 0;
|
|
166
|
+
const status = isIdle ? "idle" : isRunning ? "running" : "stopped";
|
|
167
|
+
return {
|
|
168
|
+
...combinedState,
|
|
169
|
+
isEmpty,
|
|
170
|
+
isFull,
|
|
171
|
+
isIdle,
|
|
172
|
+
size,
|
|
173
|
+
status
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
emitChange("AsyncQueuer", this);
|
|
177
|
+
};
|
|
420
178
|
/**
|
|
421
179
|
* Returns the current wait time (in milliseconds) between processing items.
|
|
422
180
|
* If a function is provided, it is called with the queuer instance.
|
|
423
181
|
*/
|
|
424
|
-
#getWait
|
|
182
|
+
#getWait = () => {
|
|
183
|
+
return parseFunctionOrValue(this.options.wait ?? 0, this);
|
|
184
|
+
};
|
|
425
185
|
/**
|
|
426
186
|
* Returns the current concurrency limit for processing items.
|
|
427
187
|
* If a function is provided, it is called with the queuer instance.
|
|
428
188
|
*/
|
|
429
|
-
#getConcurrency
|
|
189
|
+
#getConcurrency = () => {
|
|
190
|
+
return parseFunctionOrValue(this.options.concurrency ?? 1, this);
|
|
191
|
+
};
|
|
430
192
|
/**
|
|
431
193
|
* Processes items in the queue up to the concurrency limit. Internal use only.
|
|
432
194
|
*/
|
|
433
|
-
#tick
|
|
434
|
-
|
|
195
|
+
#tick = () => {
|
|
196
|
+
if (!this.store.state.isRunning) {
|
|
197
|
+
this.#setState({ pendingTick: false });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
this.#setState({ pendingTick: true });
|
|
201
|
+
this.#checkExpiredItems();
|
|
202
|
+
let scheduledAsyncWork = false;
|
|
203
|
+
const activeItems = [...this.store.state.activeItems];
|
|
204
|
+
while (activeItems.length < this.#getConcurrency() && this.store.state.items.length > 0) {
|
|
205
|
+
const nextItem = this.peekNextItem();
|
|
206
|
+
if (nextItem === void 0) break;
|
|
207
|
+
activeItems.push(nextItem);
|
|
208
|
+
this.#setState({ activeItems: [...activeItems] });
|
|
209
|
+
scheduledAsyncWork = true;
|
|
210
|
+
(async () => {
|
|
211
|
+
try {
|
|
212
|
+
await this.execute();
|
|
213
|
+
} catch {}
|
|
214
|
+
const wait = this.#getWait();
|
|
215
|
+
if (wait > 0) {
|
|
216
|
+
const timeoutId = setTimeout(() => {
|
|
217
|
+
this.#timeoutIds.delete(timeoutId);
|
|
218
|
+
this.#tick();
|
|
219
|
+
}, wait);
|
|
220
|
+
this.#timeoutIds.add(timeoutId);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
this.#tick();
|
|
224
|
+
})();
|
|
225
|
+
}
|
|
226
|
+
if (!scheduledAsyncWork) this.#setState({ pendingTick: false });
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
229
|
+
* Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.
|
|
230
|
+
* Items can be inserted based on priority or at the front/back depending on configuration.
|
|
231
|
+
* `undefined` cannot be queued (it is the internal "no item" sentinel) and is always rejected.
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```ts
|
|
235
|
+
* queuer.addItem({ value: 'task', priority: 10 });
|
|
236
|
+
* queuer.addItem('task2', 'front');
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
addItem = (item, position = this.options.addItemsTo ?? "back", runOnItemsChange = true) => {
|
|
240
|
+
this.#setState({ addItemCount: this.store.state.addItemCount + 1 });
|
|
241
|
+
if (item === void 0) {
|
|
242
|
+
this.#setState({ rejectionCount: this.store.state.rejectionCount + 1 });
|
|
243
|
+
this.options.onReject?.(item, this);
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {
|
|
247
|
+
this.#setState({ rejectionCount: this.store.state.rejectionCount + 1 });
|
|
248
|
+
this.options.onReject?.(item, this);
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
const priority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(item) : item?.priority;
|
|
252
|
+
const items = this.store.state.items;
|
|
253
|
+
const itemTimestamps = this.store.state.itemTimestamps;
|
|
254
|
+
if (priority !== void 0) {
|
|
255
|
+
const insertIndex = items.findIndex((existing) => {
|
|
256
|
+
return (this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(existing) : existing?.priority) < priority;
|
|
257
|
+
});
|
|
258
|
+
if (insertIndex === -1) {
|
|
259
|
+
items.push(item);
|
|
260
|
+
itemTimestamps.push(Date.now());
|
|
261
|
+
} else {
|
|
262
|
+
items.splice(insertIndex, 0, item);
|
|
263
|
+
itemTimestamps.splice(insertIndex, 0, Date.now());
|
|
264
|
+
}
|
|
265
|
+
} else if (position === "front") {
|
|
266
|
+
items.unshift(item);
|
|
267
|
+
itemTimestamps.unshift(Date.now());
|
|
268
|
+
} else {
|
|
269
|
+
items.push(item);
|
|
270
|
+
itemTimestamps.push(Date.now());
|
|
271
|
+
}
|
|
272
|
+
this.#setState({
|
|
273
|
+
items,
|
|
274
|
+
itemTimestamps
|
|
275
|
+
});
|
|
276
|
+
if (runOnItemsChange) this.options.onItemsChange?.(this);
|
|
277
|
+
if (this.store.state.isRunning && !this.store.state.pendingTick) this.#tick();
|
|
278
|
+
return true;
|
|
279
|
+
};
|
|
280
|
+
/**
|
|
281
|
+
* Removes and returns the next item from the queue without executing the task function.
|
|
282
|
+
* Use for manual queue management. Normally, use execute() to process items.
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```ts
|
|
286
|
+
* // FIFO
|
|
287
|
+
* queuer.getNextItem();
|
|
288
|
+
* // LIFO
|
|
289
|
+
* queuer.getNextItem('back');
|
|
290
|
+
* ```
|
|
291
|
+
*/
|
|
292
|
+
getNextItem = (position = this.options.getItemsFrom ?? "front") => {
|
|
293
|
+
const { items, itemTimestamps } = this.store.state;
|
|
294
|
+
let item;
|
|
295
|
+
if (this.options.getPriority !== defaultOptions.getPriority || position === "front") {
|
|
296
|
+
item = items[0];
|
|
297
|
+
if (item !== void 0) this.#setState({
|
|
298
|
+
items: items.slice(1),
|
|
299
|
+
itemTimestamps: itemTimestamps.slice(1)
|
|
300
|
+
});
|
|
301
|
+
} else {
|
|
302
|
+
item = items[items.length - 1];
|
|
303
|
+
if (item !== void 0) this.#setState({
|
|
304
|
+
items: items.slice(0, -1),
|
|
305
|
+
itemTimestamps: itemTimestamps.slice(0, -1)
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
if (item !== void 0) this.options.onItemsChange?.(this);
|
|
309
|
+
return item;
|
|
310
|
+
};
|
|
311
|
+
#getAllItems = () => {
|
|
312
|
+
const items = this.peekAllItems();
|
|
313
|
+
this.clear();
|
|
314
|
+
return items;
|
|
315
|
+
};
|
|
316
|
+
/**
|
|
317
|
+
* Removes and returns the next item from the queue and executes the task function with it.
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* ```ts
|
|
321
|
+
* queuer.execute();
|
|
322
|
+
* // LIFO
|
|
323
|
+
* queuer.execute('back');
|
|
324
|
+
* ```
|
|
325
|
+
*/
|
|
326
|
+
execute = async (position) => {
|
|
327
|
+
const item = this.getNextItem(position);
|
|
328
|
+
if (item !== void 0) {
|
|
329
|
+
const currentExecutionCount = this.store.state.executionCount + 1;
|
|
330
|
+
this.#setState({
|
|
331
|
+
executionCount: currentExecutionCount,
|
|
332
|
+
isExecuting: true
|
|
333
|
+
});
|
|
334
|
+
try {
|
|
335
|
+
const currentAsyncRetryer = new AsyncRetryer(this.fn, this.options.asyncRetryerOptions);
|
|
336
|
+
this.asyncRetryers.set(currentExecutionCount, currentAsyncRetryer);
|
|
337
|
+
const lastResult = await currentAsyncRetryer.execute(item);
|
|
338
|
+
this.#setState({
|
|
339
|
+
successCount: this.store.state.successCount + 1,
|
|
340
|
+
lastResult
|
|
341
|
+
});
|
|
342
|
+
this.options.onSuccess?.(lastResult, item, this);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
this.#setState({ errorCount: this.store.state.errorCount + 1 });
|
|
345
|
+
this.options.onError?.(error, item, this);
|
|
346
|
+
if (this.options.throwOnError) throw error;
|
|
347
|
+
} finally {
|
|
348
|
+
this.asyncRetryers.delete(currentExecutionCount);
|
|
349
|
+
const remainingActiveItems = [...this.store.state.activeItems];
|
|
350
|
+
const activeItemIndex = remainingActiveItems.indexOf(item);
|
|
351
|
+
if (activeItemIndex !== -1) remainingActiveItems.splice(activeItemIndex, 1);
|
|
352
|
+
this.#setState({
|
|
353
|
+
activeItems: remainingActiveItems,
|
|
354
|
+
isExecuting: this.asyncRetryers.size > 0,
|
|
355
|
+
settleCount: this.store.state.settleCount + 1
|
|
356
|
+
});
|
|
357
|
+
this.options.onSettled?.(item, this);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return item;
|
|
361
|
+
};
|
|
362
|
+
/**
|
|
363
|
+
* Processes a specified number of items to execute immediately with no wait time
|
|
364
|
+
* If no numberOfItems is provided, all items will be processed
|
|
365
|
+
*/
|
|
366
|
+
flush = async (numberOfItems = this.store.state.items.length, position) => {
|
|
367
|
+
this.#clearTimeouts();
|
|
368
|
+
const results = await Promise.allSettled(Array.from({ length: numberOfItems }, () => this.execute(position)));
|
|
369
|
+
this.#setState({ pendingTick: false });
|
|
370
|
+
if (this.store.state.isRunning && this.store.state.items.length > 0) {
|
|
371
|
+
const wait = this.#getWait();
|
|
372
|
+
if (wait > 0) {
|
|
373
|
+
this.#setState({ pendingTick: true });
|
|
374
|
+
const timeoutId = setTimeout(() => {
|
|
375
|
+
this.#timeoutIds.delete(timeoutId);
|
|
376
|
+
this.#tick();
|
|
377
|
+
}, wait);
|
|
378
|
+
this.#timeoutIds.add(timeoutId);
|
|
379
|
+
} else this.#tick();
|
|
380
|
+
}
|
|
381
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
382
|
+
if (failure) throw failure.reason;
|
|
383
|
+
};
|
|
384
|
+
/**
|
|
385
|
+
* Processes all items in the queue as a batch using the provided function as an argument
|
|
386
|
+
* The queue is cleared after processing
|
|
387
|
+
*/
|
|
388
|
+
flushAsBatch = async (batchFunction) => {
|
|
389
|
+
this.#clearTimeouts();
|
|
390
|
+
const items = this.#getAllItems();
|
|
391
|
+
try {
|
|
392
|
+
await batchFunction(items);
|
|
393
|
+
} finally {
|
|
394
|
+
this.#setState({ pendingTick: false });
|
|
395
|
+
if (this.store.state.isRunning && this.store.state.items.length > 0) this.#tick();
|
|
396
|
+
}
|
|
397
|
+
};
|
|
435
398
|
/**
|
|
436
399
|
* Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
|
|
437
400
|
* Internal use only.
|
|
438
401
|
*/
|
|
439
|
-
#checkExpiredItems
|
|
440
|
-
|
|
402
|
+
#checkExpiredItems = () => {
|
|
403
|
+
if ((this.options.expirationDuration ?? Infinity) === Infinity && this.options.getIsExpired === defaultOptions.getIsExpired) return;
|
|
404
|
+
const now = Date.now();
|
|
405
|
+
const expiredIndices = [];
|
|
406
|
+
for (let i = 0; i < this.store.state.items.length; i++) {
|
|
407
|
+
const timestamp = this.store.state.itemTimestamps[i];
|
|
408
|
+
if (timestamp === void 0) continue;
|
|
409
|
+
const item = this.store.state.items[i];
|
|
410
|
+
if (item === void 0) continue;
|
|
411
|
+
if (this.options.getIsExpired !== defaultOptions.getIsExpired ? this.options.getIsExpired(item, timestamp) : now - timestamp > (this.options.expirationDuration ?? Infinity)) expiredIndices.push(i);
|
|
412
|
+
}
|
|
413
|
+
for (let i = expiredIndices.length - 1; i >= 0; i--) {
|
|
414
|
+
const index = expiredIndices[i];
|
|
415
|
+
if (index === void 0) continue;
|
|
416
|
+
const expiredItem = this.store.state.items[index];
|
|
417
|
+
if (expiredItem === void 0) continue;
|
|
418
|
+
const newItems = [...this.store.state.items];
|
|
419
|
+
const newTimestamps = [...this.store.state.itemTimestamps];
|
|
420
|
+
newItems.splice(index, 1);
|
|
421
|
+
newTimestamps.splice(index, 1);
|
|
422
|
+
this.#setState({
|
|
423
|
+
items: newItems,
|
|
424
|
+
itemTimestamps: newTimestamps,
|
|
425
|
+
expirationCount: this.store.state.expirationCount + 1
|
|
426
|
+
});
|
|
427
|
+
this.options.onExpire?.(expiredItem, this);
|
|
428
|
+
}
|
|
429
|
+
if (expiredIndices.length > 0) this.options.onItemsChange?.(this);
|
|
430
|
+
};
|
|
431
|
+
/**
|
|
432
|
+
* Returns the next item in the queue without removing it.
|
|
433
|
+
*
|
|
434
|
+
* @example
|
|
435
|
+
* ```ts
|
|
436
|
+
* queuer.peekNextItem(); // front
|
|
437
|
+
* queuer.peekNextItem('back'); // back
|
|
438
|
+
* ```
|
|
439
|
+
*/
|
|
440
|
+
peekNextItem = (position = "front") => {
|
|
441
|
+
if (position === "front") return this.store.state.items[0];
|
|
442
|
+
return this.store.state.items[this.store.state.items.length - 1];
|
|
443
|
+
};
|
|
444
|
+
/**
|
|
445
|
+
* Returns a copy of all items in the queue, including active and pending items.
|
|
446
|
+
*/
|
|
447
|
+
peekAllItems = () => {
|
|
448
|
+
return [...this.peekActiveItems(), ...this.peekPendingItems()];
|
|
449
|
+
};
|
|
450
|
+
/**
|
|
451
|
+
* Returns the items currently being processed (active tasks).
|
|
452
|
+
*/
|
|
453
|
+
peekActiveItems = () => {
|
|
454
|
+
return [...this.store.state.activeItems];
|
|
455
|
+
};
|
|
456
|
+
/**
|
|
457
|
+
* Returns the items waiting to be processed (pending tasks).
|
|
458
|
+
*/
|
|
459
|
+
peekPendingItems = () => {
|
|
460
|
+
return [...this.store.state.items];
|
|
461
|
+
};
|
|
462
|
+
/**
|
|
463
|
+
* Starts processing items in the queue. If already running, does nothing.
|
|
464
|
+
*/
|
|
465
|
+
start = () => {
|
|
466
|
+
this.#setState({ isRunning: true });
|
|
467
|
+
if (!this.store.state.pendingTick && this.store.state.items.length > 0) this.#tick();
|
|
468
|
+
};
|
|
469
|
+
/**
|
|
470
|
+
* Stops processing items in the queue. Does not clear the queue.
|
|
471
|
+
*/
|
|
472
|
+
stop = () => {
|
|
473
|
+
this.#clearTimeouts();
|
|
474
|
+
this.#setState({
|
|
475
|
+
isRunning: false,
|
|
476
|
+
pendingTick: false
|
|
477
|
+
});
|
|
478
|
+
};
|
|
479
|
+
#clearTimeouts = () => {
|
|
480
|
+
this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId));
|
|
481
|
+
this.#timeoutIds.clear();
|
|
482
|
+
};
|
|
483
|
+
/**
|
|
484
|
+
* Removes all pending items from the queue.
|
|
485
|
+
* Does NOT affect active tasks.
|
|
486
|
+
*/
|
|
487
|
+
clear = () => {
|
|
488
|
+
this.#setState({
|
|
489
|
+
items: [],
|
|
490
|
+
itemTimestamps: []
|
|
491
|
+
});
|
|
492
|
+
this.options.onItemsChange?.(this);
|
|
493
|
+
};
|
|
494
|
+
/**
|
|
495
|
+
* Returns the AbortSignal for a specific execution.
|
|
496
|
+
* If no executionCount is provided, returns the signal for the most recent execution.
|
|
497
|
+
* Returns null if no execution is found or not currently executing.
|
|
498
|
+
*
|
|
499
|
+
* @param executionCount - Optional specific execution to get signal for
|
|
500
|
+
* @example
|
|
501
|
+
* ```typescript
|
|
502
|
+
* const queuer = new AsyncQueuer(
|
|
503
|
+
* async (item: string) => {
|
|
504
|
+
* const signal = queuer.getAbortSignal()
|
|
505
|
+
* if (signal) {
|
|
506
|
+
* const response = await fetch(`/api/process/${item}`, { signal })
|
|
507
|
+
* return response.json()
|
|
508
|
+
* }
|
|
509
|
+
* },
|
|
510
|
+
* { concurrency: 2 }
|
|
511
|
+
* )
|
|
512
|
+
* ```
|
|
513
|
+
*/
|
|
514
|
+
getAbortSignal = (executionCount) => {
|
|
515
|
+
const count = executionCount ?? this.store.state.executionCount;
|
|
516
|
+
return this.asyncRetryers.get(count)?.getAbortSignal() ?? null;
|
|
517
|
+
};
|
|
518
|
+
/**
|
|
519
|
+
* Aborts all ongoing executions with the internal abort controllers.
|
|
520
|
+
* Does NOT clear out the items.
|
|
521
|
+
*/
|
|
522
|
+
abort = () => {
|
|
523
|
+
this.asyncRetryers.forEach((retryer) => retryer.abort());
|
|
524
|
+
this.asyncRetryers.clear();
|
|
525
|
+
this.#setState({ isExecuting: false });
|
|
526
|
+
};
|
|
527
|
+
/**
|
|
528
|
+
* Resets the queuer state to its default values
|
|
529
|
+
*/
|
|
530
|
+
reset = () => {
|
|
531
|
+
this.#setState(getDefaultAsyncQueuerState());
|
|
532
|
+
this.options.onItemsChange?.(this);
|
|
533
|
+
this.asyncRetryers.forEach((retryer) => retryer.reset());
|
|
534
|
+
};
|
|
441
535
|
};
|
|
442
536
|
/**
|
|
443
537
|
* Creates a new AsyncQueuer instance and returns a bound addItem function for adding tasks.
|
|
@@ -509,5 +603,4 @@ function asyncQueue(fn, initialOptions) {
|
|
|
509
603
|
}
|
|
510
604
|
|
|
511
605
|
//#endregion
|
|
512
|
-
export { AsyncQueuer, asyncQueue, asyncQueuerOptions };
|
|
513
|
-
//# sourceMappingURL=async-queuer.js.map
|
|
606
|
+
export { AsyncQueuer, asyncQueue, asyncQueuerOptions };
|