@ls-stack/utils 3.38.0 → 3.40.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/asyncQueue.cjs +425 -13
- package/dist/asyncQueue.d.cts +427 -5
- package/dist/asyncQueue.d.ts +427 -5
- package/dist/asyncQueue.js +414 -13
- package/dist/consoleFmt.cjs +1 -0
- package/dist/consoleFmt.d.cts +6 -0
- package/dist/consoleFmt.d.ts +6 -0
- package/dist/consoleFmt.js +1 -0
- package/docs/asyncQueue/-internal-.md +88 -606
- package/docs/asyncQueue/README.md +1414 -4
- package/docs/consoleFmt.md +30 -6
- package/package.json +2 -2
package/dist/asyncQueue.cjs
CHANGED
|
@@ -38,8 +38,24 @@ function defer() {
|
|
|
38
38
|
return { resolve, reject, promise };
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// src/time.ts
|
|
42
|
+
var MINUTE_AS_MS = 60 * 1e3;
|
|
43
|
+
var HOUR_AS_MS = 60 * MINUTE_AS_MS;
|
|
44
|
+
var DAY_AS_MS = 24 * HOUR_AS_MS;
|
|
45
|
+
var WEEK_AS_MS = 7 * DAY_AS_MS;
|
|
46
|
+
var MONTH_AS_MS = 30 * DAY_AS_MS;
|
|
47
|
+
var YEAR_AS_MS = 365 * DAY_AS_MS;
|
|
48
|
+
var HOUR_AS_SECS = 60 * 60;
|
|
49
|
+
var DAY_AS_SECS = 24 * HOUR_AS_SECS;
|
|
50
|
+
var WEEK_AS_SECS = 7 * DAY_AS_SECS;
|
|
51
|
+
var MONTH_AS_SECS = 30 * DAY_AS_SECS;
|
|
52
|
+
var YEAR_AS_SECS = 365 * DAY_AS_SECS;
|
|
53
|
+
function durationObjToMs(durationObj) {
|
|
54
|
+
return (durationObj.hours ?? 0) * HOUR_AS_MS + (durationObj.minutes ?? 0) * MINUTE_AS_MS + (durationObj.seconds ?? 0) * 1e3 + (durationObj.ms ?? 0) + (durationObj.days ?? 0) * DAY_AS_MS;
|
|
55
|
+
}
|
|
56
|
+
|
|
41
57
|
// src/asyncQueue.ts
|
|
42
|
-
var AsyncQueue = class {
|
|
58
|
+
var AsyncQueue = class _AsyncQueue {
|
|
43
59
|
#queue = [];
|
|
44
60
|
#pending = 0;
|
|
45
61
|
#size = 0;
|
|
@@ -47,19 +63,61 @@ var AsyncQueue = class {
|
|
|
47
63
|
#completed = 0;
|
|
48
64
|
#failed = 0;
|
|
49
65
|
#idleResolvers = [];
|
|
66
|
+
#sizeLessThanWaiters = [];
|
|
67
|
+
/**
|
|
68
|
+
* Event emitter for tracking task lifecycle
|
|
69
|
+
*
|
|
70
|
+
* @example Listening to Events
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const queue = createAsyncQueue<string>();
|
|
73
|
+
*
|
|
74
|
+
* queue.events.on('start', (event) => {
|
|
75
|
+
* console.log('Task started:', event.payload.meta);
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* queue.events.on('complete', (event) => {
|
|
79
|
+
* console.log('Task completed:', event.payload.value);
|
|
80
|
+
* });
|
|
81
|
+
*
|
|
82
|
+
* queue.events.on('error', (event) => {
|
|
83
|
+
* console.error('Task failed:', event.payload.error);
|
|
84
|
+
* });
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
50
87
|
events = (0, import_evtmitter.evtmitter)();
|
|
51
88
|
#signal;
|
|
52
89
|
#taskTimeout;
|
|
90
|
+
#stopped = false;
|
|
91
|
+
#paused = false;
|
|
92
|
+
#started = false;
|
|
93
|
+
#stopOnError = false;
|
|
94
|
+
#rejectPendingOnError = false;
|
|
95
|
+
#autoStart = true;
|
|
96
|
+
#stoppedReason;
|
|
97
|
+
#rateLimit;
|
|
98
|
+
#taskExecutionTimes = [];
|
|
99
|
+
#rateLimitTimeouts = /* @__PURE__ */ new Set();
|
|
100
|
+
/** Array of all task failures with metadata for debugging and analysis */
|
|
53
101
|
failures = [];
|
|
102
|
+
/** Array of all task completions with metadata for debugging and analysis */
|
|
54
103
|
completions = [];
|
|
55
104
|
constructor({
|
|
56
105
|
concurrency = 1,
|
|
57
106
|
signal,
|
|
58
|
-
timeout: taskTimeout
|
|
107
|
+
timeout: taskTimeout,
|
|
108
|
+
stopOnError = false,
|
|
109
|
+
rejectPendingOnError = false,
|
|
110
|
+
autoStart = true,
|
|
111
|
+
rateLimit
|
|
59
112
|
} = {}) {
|
|
60
113
|
this.#concurrency = concurrency;
|
|
61
114
|
this.#signal = signal;
|
|
62
115
|
this.#taskTimeout = taskTimeout;
|
|
116
|
+
this.#stopOnError = stopOnError;
|
|
117
|
+
this.#rejectPendingOnError = rejectPendingOnError;
|
|
118
|
+
this.#autoStart = autoStart;
|
|
119
|
+
this.#started = autoStart;
|
|
120
|
+
this.#rateLimit = rateLimit;
|
|
63
121
|
this.events.on("error", (e) => {
|
|
64
122
|
this.failures.push(e.payload);
|
|
65
123
|
});
|
|
@@ -67,14 +125,113 @@ var AsyncQueue = class {
|
|
|
67
125
|
this.completions.push(e.payload);
|
|
68
126
|
});
|
|
69
127
|
}
|
|
128
|
+
#getRateLimitIntervalMs() {
|
|
129
|
+
if (!this.#rateLimit) return 0;
|
|
130
|
+
return typeof this.#rateLimit.interval === "number" ? this.#rateLimit.interval : durationObjToMs(this.#rateLimit.interval);
|
|
131
|
+
}
|
|
132
|
+
#cleanupExpiredExecutionTimes(now) {
|
|
133
|
+
if (!this.#rateLimit) return;
|
|
134
|
+
const intervalMs = this.#getRateLimitIntervalMs();
|
|
135
|
+
const cutoff = now - intervalMs;
|
|
136
|
+
this.#taskExecutionTimes = this.#taskExecutionTimes.filter(
|
|
137
|
+
(time) => time > cutoff
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
#isRateLimited() {
|
|
141
|
+
if (!this.#rateLimit) return false;
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
this.#cleanupExpiredExecutionTimes(now);
|
|
144
|
+
return this.#taskExecutionTimes.length >= this.#rateLimit.maxTasks;
|
|
145
|
+
}
|
|
146
|
+
#getRateLimitDelay() {
|
|
147
|
+
if (!this.#rateLimit || this.#taskExecutionTimes.length === 0) return 0;
|
|
148
|
+
const oldestExecution = this.#taskExecutionTimes[0];
|
|
149
|
+
if (oldestExecution === void 0) return 0;
|
|
150
|
+
const intervalMs = this.#getRateLimitIntervalMs();
|
|
151
|
+
const timeUntilSlotOpens = oldestExecution + intervalMs - Date.now();
|
|
152
|
+
return Math.max(0, timeUntilSlotOpens);
|
|
153
|
+
}
|
|
154
|
+
#recordTaskExecution() {
|
|
155
|
+
if (!this.#rateLimit) return;
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
this.#taskExecutionTimes.push(now);
|
|
158
|
+
this.#cleanupExpiredExecutionTimes(now);
|
|
159
|
+
}
|
|
70
160
|
#enqueue(task) {
|
|
71
161
|
this.#queue.push(task);
|
|
72
162
|
this.#size++;
|
|
73
163
|
}
|
|
164
|
+
static #createTimeoutSignal(ms) {
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
const id = setTimeout(() => {
|
|
167
|
+
controller.abort(
|
|
168
|
+
new DOMException(
|
|
169
|
+
"The operation was aborted due to timeout",
|
|
170
|
+
"TimeoutError"
|
|
171
|
+
)
|
|
172
|
+
);
|
|
173
|
+
}, ms);
|
|
174
|
+
controller.signal.addEventListener(
|
|
175
|
+
"abort",
|
|
176
|
+
() => {
|
|
177
|
+
clearTimeout(id);
|
|
178
|
+
},
|
|
179
|
+
{ once: true }
|
|
180
|
+
);
|
|
181
|
+
return controller.signal;
|
|
182
|
+
}
|
|
183
|
+
// removed: onEmpty-related waiters
|
|
184
|
+
#resolveSizeLessThanWaiters() {
|
|
185
|
+
if (this.#sizeLessThanWaiters.length === 0) return;
|
|
186
|
+
const remaining = [];
|
|
187
|
+
for (const waiter of this.#sizeLessThanWaiters) {
|
|
188
|
+
if (this.#size < waiter.limit) {
|
|
189
|
+
waiter.resolve();
|
|
190
|
+
} else {
|
|
191
|
+
remaining.push(waiter);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
this.#sizeLessThanWaiters = remaining;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Add a task that returns a Result to the queue
|
|
198
|
+
*
|
|
199
|
+
* Use this method when your task function already returns a Result type.
|
|
200
|
+
* For functions that throw errors or return plain values, use `resultifyAdd` instead.
|
|
201
|
+
*
|
|
202
|
+
* @param fn - Task function that returns a Result
|
|
203
|
+
* @param options - Optional configuration for this task
|
|
204
|
+
* @returns Promise that resolves with the task result
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* ```typescript
|
|
208
|
+
* const queue = createAsyncQueue<string>();
|
|
209
|
+
*
|
|
210
|
+
* const result = await queue.add(async () => {
|
|
211
|
+
* try {
|
|
212
|
+
* const data = await fetchData();
|
|
213
|
+
* return Result.ok(data);
|
|
214
|
+
* } catch (error) {
|
|
215
|
+
* return Result.err(error);
|
|
216
|
+
* }
|
|
217
|
+
* });
|
|
218
|
+
*
|
|
219
|
+
* if (result.ok) {
|
|
220
|
+
* console.log('Success:', result.value);
|
|
221
|
+
* } else {
|
|
222
|
+
* console.log('Error:', result.error);
|
|
223
|
+
* }
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
74
226
|
async add(fn, options) {
|
|
75
227
|
if (this.#signal?.aborted) {
|
|
76
228
|
return import_t_result.Result.err(
|
|
77
|
-
this.#signal.reason instanceof Error ? this.#signal.reason : new DOMException("
|
|
229
|
+
this.#signal.reason instanceof Error ? this.#signal.reason : new DOMException("This operation was aborted", "AbortError")
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (this.#stopped) {
|
|
233
|
+
return import_t_result.Result.err(
|
|
234
|
+
this.#stoppedReason ?? new Error("Queue has been stopped")
|
|
78
235
|
);
|
|
79
236
|
}
|
|
80
237
|
const deferred = defer();
|
|
@@ -90,7 +247,9 @@ var AsyncQueue = class {
|
|
|
90
247
|
timeout: taskTimeout
|
|
91
248
|
};
|
|
92
249
|
this.#enqueue(task);
|
|
93
|
-
this.#
|
|
250
|
+
if (this.#autoStart && this.#started) {
|
|
251
|
+
this.#processQueue();
|
|
252
|
+
}
|
|
94
253
|
const r = await deferred.promise;
|
|
95
254
|
if (options?.onComplete) {
|
|
96
255
|
r.onOk(options.onComplete);
|
|
@@ -100,6 +259,44 @@ var AsyncQueue = class {
|
|
|
100
259
|
}
|
|
101
260
|
return r;
|
|
102
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Add a task that returns a plain value or throws errors to the queue
|
|
264
|
+
*
|
|
265
|
+
* This is the most commonly used method. It automatically wraps your function
|
|
266
|
+
* to handle errors and convert them to Result types.
|
|
267
|
+
*
|
|
268
|
+
* @param fn - Task function that returns a value or throws
|
|
269
|
+
* @param options - Optional configuration for this task
|
|
270
|
+
* @returns Promise that resolves with the task result wrapped in Result
|
|
271
|
+
*
|
|
272
|
+
* @example Basic Usage
|
|
273
|
+
* ```typescript
|
|
274
|
+
* const queue = createAsyncQueue<string>();
|
|
275
|
+
*
|
|
276
|
+
* queue.resultifyAdd(async () => {
|
|
277
|
+
* const response = await fetch('/api/data');
|
|
278
|
+
* return response.json();
|
|
279
|
+
* }).then(result => {
|
|
280
|
+
* if (result.ok) {
|
|
281
|
+
* console.log('Data:', result.value);
|
|
282
|
+
* } else {
|
|
283
|
+
* console.error('Failed:', result.error);
|
|
284
|
+
* }
|
|
285
|
+
* });
|
|
286
|
+
* ```
|
|
287
|
+
*
|
|
288
|
+
* @example With Callbacks
|
|
289
|
+
* ```typescript
|
|
290
|
+
* queue.resultifyAdd(
|
|
291
|
+
* async () => processData(),
|
|
292
|
+
* {
|
|
293
|
+
* onComplete: (data) => console.log('Processed:', data),
|
|
294
|
+
* onError: (error) => console.error('Failed:', error),
|
|
295
|
+
* timeout: 5000
|
|
296
|
+
* }
|
|
297
|
+
* );
|
|
298
|
+
* ```
|
|
299
|
+
*/
|
|
103
300
|
resultifyAdd(fn, options) {
|
|
104
301
|
return this.add(
|
|
105
302
|
(ctx) => (0, import_t_result.resultify)(async () => {
|
|
@@ -113,15 +310,31 @@ var AsyncQueue = class {
|
|
|
113
310
|
this.clear();
|
|
114
311
|
return;
|
|
115
312
|
}
|
|
313
|
+
if (this.#stopped || this.#paused || !this.#started) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
116
316
|
if (this.#pending >= this.#concurrency || this.#queue.length === 0) {
|
|
117
317
|
return;
|
|
118
318
|
}
|
|
319
|
+
if (this.#isRateLimited()) {
|
|
320
|
+
const delay = this.#getRateLimitDelay();
|
|
321
|
+
if (delay > 0) {
|
|
322
|
+
const timeoutId = setTimeout(() => {
|
|
323
|
+
this.#rateLimitTimeouts.delete(timeoutId);
|
|
324
|
+
this.#processQueue();
|
|
325
|
+
}, delay);
|
|
326
|
+
this.#rateLimitTimeouts.add(timeoutId);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
119
330
|
const task = this.#queue.shift();
|
|
120
331
|
if (!task) {
|
|
121
332
|
return;
|
|
122
333
|
}
|
|
123
334
|
this.#pending++;
|
|
124
335
|
this.#size--;
|
|
336
|
+
this.#resolveSizeLessThanWaiters();
|
|
337
|
+
this.#recordTaskExecution();
|
|
125
338
|
const signals = [];
|
|
126
339
|
if (task.signal) {
|
|
127
340
|
signals.push(task.signal);
|
|
@@ -129,8 +342,8 @@ var AsyncQueue = class {
|
|
|
129
342
|
if (this.#signal) {
|
|
130
343
|
signals.push(this.#signal);
|
|
131
344
|
}
|
|
132
|
-
if (task.timeout) {
|
|
133
|
-
signals.push(
|
|
345
|
+
if (task.timeout !== void 0) {
|
|
346
|
+
signals.push(_AsyncQueue.#createTimeoutSignal(task.timeout));
|
|
134
347
|
}
|
|
135
348
|
const signal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
|
|
136
349
|
let abortListener;
|
|
@@ -141,10 +354,11 @@ var AsyncQueue = class {
|
|
|
141
354
|
}
|
|
142
355
|
const signalAbortPromise = new Promise((_, reject) => {
|
|
143
356
|
if (signal) {
|
|
144
|
-
const error = signal.reason instanceof Error ? signal.reason : new DOMException("This operation was aborted", "AbortError");
|
|
145
357
|
abortListener = () => {
|
|
358
|
+
const reason = signal.reason;
|
|
359
|
+
const err = reason instanceof Error ? reason : new DOMException("This operation was aborted", "AbortError");
|
|
146
360
|
setTimeout(() => {
|
|
147
|
-
reject(
|
|
361
|
+
reject(err);
|
|
148
362
|
}, 0);
|
|
149
363
|
};
|
|
150
364
|
signal.addEventListener("abort", abortListener, { once: true });
|
|
@@ -155,12 +369,13 @@ var AsyncQueue = class {
|
|
|
155
369
|
const result = await Promise.race([taskRunPromise, signalAbortPromise]);
|
|
156
370
|
if ((0, import_t_result.isResult)(result)) {
|
|
157
371
|
task.resolve(result);
|
|
158
|
-
if (result.
|
|
372
|
+
if (!result.ok) {
|
|
159
373
|
this.#failed++;
|
|
160
374
|
this.events.emit("error", {
|
|
161
375
|
meta: task.meta,
|
|
162
376
|
error: result.error
|
|
163
377
|
});
|
|
378
|
+
this.#stopOnErrorAndRejectPending((0, import_t_result.unknownToError)(result.error));
|
|
164
379
|
} else {
|
|
165
380
|
this.#completed++;
|
|
166
381
|
this.events.emit("complete", {
|
|
@@ -176,21 +391,24 @@ var AsyncQueue = class {
|
|
|
176
391
|
meta: task.meta,
|
|
177
392
|
error
|
|
178
393
|
});
|
|
394
|
+
this.#stopOnErrorAndRejectPending(error);
|
|
179
395
|
}
|
|
180
396
|
} catch (error) {
|
|
181
|
-
|
|
397
|
+
const processedError = (0, import_t_result.unknownToError)(error);
|
|
398
|
+
task.resolve(import_t_result.Result.err(processedError));
|
|
182
399
|
this.#failed++;
|
|
183
400
|
this.events.emit("error", {
|
|
184
401
|
meta: task.meta,
|
|
185
|
-
error:
|
|
402
|
+
error: processedError
|
|
186
403
|
});
|
|
404
|
+
this.#stopOnErrorAndRejectPending(processedError);
|
|
187
405
|
} finally {
|
|
188
406
|
if (signal && abortListener) {
|
|
189
407
|
signal.removeEventListener("abort", abortListener);
|
|
190
408
|
}
|
|
191
409
|
this.#pending--;
|
|
192
410
|
this.#processQueue();
|
|
193
|
-
if (this.#pending === 0 && this.#size === 0) {
|
|
411
|
+
if (this.#pending === 0 && this.#size === 0 && this.#rateLimitTimeouts.size === 0) {
|
|
194
412
|
this.#resolveIdleWaiters();
|
|
195
413
|
}
|
|
196
414
|
}
|
|
@@ -203,33 +421,227 @@ var AsyncQueue = class {
|
|
|
203
421
|
}
|
|
204
422
|
}
|
|
205
423
|
}
|
|
424
|
+
#stopOnErrorAndRejectPending(error) {
|
|
425
|
+
if (!this.#stopOnError) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
this.#stopped = true;
|
|
429
|
+
this.#stoppedReason = error;
|
|
430
|
+
if (this.#rejectPendingOnError) {
|
|
431
|
+
while (this.#queue.length > 0) {
|
|
432
|
+
const task = this.#queue.shift();
|
|
433
|
+
if (task) {
|
|
434
|
+
task.resolve(import_t_result.Result.err(error));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
this.#size = 0;
|
|
438
|
+
this.#resolveSizeLessThanWaiters();
|
|
439
|
+
}
|
|
440
|
+
this.#resolveIdleWaiters();
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Wait for the queue to become idle (no pending tasks, no queued tasks, and no rate-limit timers)
|
|
444
|
+
*
|
|
445
|
+
* This method resolves when:
|
|
446
|
+
* - All tasks have completed (success or failure)
|
|
447
|
+
* - The queue is stopped due to error (stopOnError), even with remaining tasks
|
|
448
|
+
* - There are no queued tasks, no running tasks, and no pending rate-limit timers
|
|
449
|
+
*
|
|
450
|
+
* @returns Promise that resolves when the queue is idle
|
|
451
|
+
*
|
|
452
|
+
* @example
|
|
453
|
+
* ```typescript
|
|
454
|
+
* const queue = createAsyncQueue<string>();
|
|
455
|
+
*
|
|
456
|
+
* // Add multiple tasks
|
|
457
|
+
* for (let i = 0; i < 10; i++) {
|
|
458
|
+
* queue.resultifyAdd(async () => `task ${i}`);
|
|
459
|
+
* }
|
|
460
|
+
*
|
|
461
|
+
* // Wait for all tasks to complete
|
|
462
|
+
* await queue.onIdle();
|
|
463
|
+
*
|
|
464
|
+
* console.log(`Completed: ${queue.completed}, Failed: ${queue.failed}`);
|
|
465
|
+
* ```
|
|
466
|
+
*/
|
|
206
467
|
async onIdle() {
|
|
207
|
-
if (this.#pending === 0 && this.#size === 0) {
|
|
468
|
+
if (this.#stopped || this.#pending === 0 && this.#size === 0 && this.#rateLimitTimeouts.size === 0) {
|
|
208
469
|
return Promise.resolve();
|
|
209
470
|
}
|
|
210
471
|
return new Promise((resolve) => {
|
|
211
472
|
this.#idleResolvers.push(resolve);
|
|
212
473
|
});
|
|
213
474
|
}
|
|
475
|
+
// removed: onEmpty()
|
|
476
|
+
/**
|
|
477
|
+
* Wait until the queued task count is below a limit
|
|
478
|
+
*
|
|
479
|
+
* Resolves immediately if `size < limit` at the moment of calling. This only
|
|
480
|
+
* considers queued (not yet started) tasks; running tasks are tracked by
|
|
481
|
+
* `pending`.
|
|
482
|
+
*
|
|
483
|
+
* @param limit Threshold that `size` must be below to resolve
|
|
484
|
+
*/
|
|
485
|
+
onSizeLessThan(limit) {
|
|
486
|
+
if (this.#size < limit) {
|
|
487
|
+
return Promise.resolve();
|
|
488
|
+
}
|
|
489
|
+
return new Promise((resolve) => {
|
|
490
|
+
this.#sizeLessThanWaiters.push({ limit, resolve });
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Clear all queued tasks (does not affect currently running tasks)
|
|
495
|
+
*
|
|
496
|
+
* This removes all tasks waiting in the queue but allows currently
|
|
497
|
+
* executing tasks to complete normally.
|
|
498
|
+
*
|
|
499
|
+
* @example
|
|
500
|
+
* ```typescript
|
|
501
|
+
* const queue = createAsyncQueue({ concurrency: 1 });
|
|
502
|
+
*
|
|
503
|
+
* // Add multiple tasks
|
|
504
|
+
* queue.resultifyAdd(async () => longRunningTask()); // Will start immediately
|
|
505
|
+
* queue.resultifyAdd(async () => task2()); // Queued
|
|
506
|
+
* queue.resultifyAdd(async () => task3()); // Queued
|
|
507
|
+
*
|
|
508
|
+
* // Clear remaining queued tasks
|
|
509
|
+
* queue.clear();
|
|
510
|
+
*
|
|
511
|
+
* // Only the first task will complete
|
|
512
|
+
* await queue.onIdle();
|
|
513
|
+
* ```
|
|
514
|
+
*/
|
|
214
515
|
clear() {
|
|
215
516
|
this.#queue = [];
|
|
216
517
|
this.#size = 0;
|
|
518
|
+
for (const timeoutId of this.#rateLimitTimeouts) {
|
|
519
|
+
clearTimeout(timeoutId);
|
|
520
|
+
}
|
|
521
|
+
this.#rateLimitTimeouts.clear();
|
|
217
522
|
if (this.#pending === 0) {
|
|
218
523
|
this.#resolveIdleWaiters();
|
|
219
524
|
}
|
|
525
|
+
this.#resolveSizeLessThanWaiters();
|
|
220
526
|
}
|
|
527
|
+
/** Number of tasks that have completed successfully */
|
|
221
528
|
get completed() {
|
|
222
529
|
return this.#completed;
|
|
223
530
|
}
|
|
531
|
+
/** Number of tasks that have failed */
|
|
224
532
|
get failed() {
|
|
225
533
|
return this.#failed;
|
|
226
534
|
}
|
|
535
|
+
/** Number of tasks currently being processed */
|
|
227
536
|
get pending() {
|
|
228
537
|
return this.#pending;
|
|
229
538
|
}
|
|
539
|
+
/** Number of tasks waiting in the queue to be processed */
|
|
230
540
|
get size() {
|
|
231
541
|
return this.#size;
|
|
232
542
|
}
|
|
543
|
+
/**
|
|
544
|
+
* Manually start processing tasks (only needed if autoStart: false)
|
|
545
|
+
*
|
|
546
|
+
* @example
|
|
547
|
+
* ```typescript
|
|
548
|
+
* const queue = createAsyncQueue({ autoStart: false });
|
|
549
|
+
*
|
|
550
|
+
* // Add tasks without starting processing
|
|
551
|
+
* queue.resultifyAdd(async () => 'task1');
|
|
552
|
+
* queue.resultifyAdd(async () => 'task2');
|
|
553
|
+
*
|
|
554
|
+
* // Start processing when ready
|
|
555
|
+
* queue.start();
|
|
556
|
+
* await queue.onIdle();
|
|
557
|
+
* ```
|
|
558
|
+
*/
|
|
559
|
+
start() {
|
|
560
|
+
if (this.#stopped) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
this.#started = true;
|
|
564
|
+
this.#processQueue();
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Pause processing new tasks (currently running tasks continue)
|
|
568
|
+
*
|
|
569
|
+
* @example
|
|
570
|
+
* ```typescript
|
|
571
|
+
* const queue = createAsyncQueue();
|
|
572
|
+
*
|
|
573
|
+
* // Start some tasks
|
|
574
|
+
* queue.resultifyAdd(async () => longRunningTask1());
|
|
575
|
+
* queue.resultifyAdd(async () => longRunningTask2());
|
|
576
|
+
*
|
|
577
|
+
* // Pause before more tasks are picked up
|
|
578
|
+
* queue.pause();
|
|
579
|
+
*
|
|
580
|
+
* // Later, resume processing
|
|
581
|
+
* queue.resume();
|
|
582
|
+
* ```
|
|
583
|
+
*/
|
|
584
|
+
pause() {
|
|
585
|
+
this.#paused = true;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Resume processing tasks after pause
|
|
589
|
+
*/
|
|
590
|
+
resume() {
|
|
591
|
+
this.#paused = false;
|
|
592
|
+
if (this.#started && !this.#stopped) {
|
|
593
|
+
this.#processQueue();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Reset the queue after being stopped, allowing new tasks to be processed
|
|
598
|
+
*
|
|
599
|
+
* This clears the stopped state and error reason, and resumes processing
|
|
600
|
+
* any remaining queued tasks if autoStart was enabled.
|
|
601
|
+
*
|
|
602
|
+
* @example
|
|
603
|
+
* ```typescript
|
|
604
|
+
* const queue = createAsyncQueue({ stopOnError: true });
|
|
605
|
+
*
|
|
606
|
+
* // Add tasks that will cause the queue to stop
|
|
607
|
+
* queue.resultifyAdd(async () => { throw new Error('fail'); });
|
|
608
|
+
* queue.resultifyAdd(async () => 'remaining task');
|
|
609
|
+
*
|
|
610
|
+
* await queue.onIdle();
|
|
611
|
+
*
|
|
612
|
+
* if (queue.isStopped) {
|
|
613
|
+
* console.log(`Queue stopped, ${queue.size} tasks remaining`);
|
|
614
|
+
*
|
|
615
|
+
* // Reset and process remaining tasks
|
|
616
|
+
* queue.reset();
|
|
617
|
+
* await queue.onIdle();
|
|
618
|
+
* }
|
|
619
|
+
* ```
|
|
620
|
+
*/
|
|
621
|
+
reset() {
|
|
622
|
+
this.#stopped = false;
|
|
623
|
+
this.#stoppedReason = void 0;
|
|
624
|
+
if (this.#autoStart) {
|
|
625
|
+
this.#started = true;
|
|
626
|
+
this.#processQueue();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/** Whether the queue is stopped due to an error */
|
|
630
|
+
get isStopped() {
|
|
631
|
+
return this.#stopped;
|
|
632
|
+
}
|
|
633
|
+
/** Whether the queue is currently paused */
|
|
634
|
+
get isPaused() {
|
|
635
|
+
return this.#paused;
|
|
636
|
+
}
|
|
637
|
+
/** Whether the queue has been started (relevant for autoStart: false) */
|
|
638
|
+
get isStarted() {
|
|
639
|
+
return this.#started;
|
|
640
|
+
}
|
|
641
|
+
/** The error that caused the queue to stop (if any) */
|
|
642
|
+
get stoppedReason() {
|
|
643
|
+
return this.#stoppedReason;
|
|
644
|
+
}
|
|
233
645
|
};
|
|
234
646
|
var AsyncQueueWithMeta = class extends AsyncQueue {
|
|
235
647
|
constructor(options) {
|