@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.
@@ -1,64 +1,486 @@
1
1
  import * as evtmitter from 'evtmitter';
2
2
  import { ResultValidErrors, Result } from 't-result';
3
+ import { DurationObj } from './time.cjs';
3
4
 
5
+ /**
6
+ * Configuration for rate limiting task execution
7
+ */
8
+ type RateLimit = {
9
+ /** Maximum number of tasks to execute within the interval */
10
+ maxTasks: number;
11
+ /** Time interval in milliseconds or as a duration object */
12
+ interval: DurationObj | number;
13
+ };
14
+ /**
15
+ * Configuration options for AsyncQueue initialization
16
+ */
4
17
  type AsyncQueueOptions = {
18
+ /** Maximum number of tasks to run concurrently (default: 1) */
5
19
  concurrency?: number;
20
+ /** AbortSignal to cancel the entire queue */
6
21
  signal?: AbortSignal;
22
+ /** Default timeout for all tasks in milliseconds */
7
23
  timeout?: number;
24
+ /** Stop processing new tasks when any task fails (default: false) */
25
+ stopOnError?: boolean;
26
+ /** Reject all pending tasks when stopping on error (default: false) */
27
+ rejectPendingOnError?: boolean;
28
+ /** Start processing tasks immediately when added (default: true) */
29
+ autoStart?: boolean;
30
+ /** Rate limit configuration to limit tasks per time interval */
31
+ rateLimit?: RateLimit;
8
32
  };
33
+ /**
34
+ * Options for adding individual tasks to the queue
35
+ */
9
36
  type AddOptions<I, T, E extends ResultValidErrors> = {
37
+ /** AbortSignal to cancel this specific task */
10
38
  signal?: AbortSignal;
39
+ /** Timeout for this specific task in milliseconds */
11
40
  timeout?: number;
41
+ /** Metadata to associate with this task */
12
42
  meta?: I;
43
+ /** Callback invoked when task completes successfully */
13
44
  onComplete?: (value: T) => void;
45
+ /** Callback invoked when task fails */
14
46
  onError?: (error: E | Error) => void;
15
47
  };
48
+ /**
49
+ * Runtime context passed to task functions
50
+ */
16
51
  type RunCtx<I> = {
52
+ /** Combined AbortSignal from task, queue, and timeout signals */
17
53
  signal?: AbortSignal;
54
+ /** Metadata associated with this task */
18
55
  meta?: I;
19
56
  };
57
+ /**
58
+ * A powerful async task queue with advanced error handling and flow control
59
+ *
60
+ * @template T - The type of value returned by successful tasks
61
+ * @template E - The type of errors that tasks can produce (defaults to Error)
62
+ * @template I - The type of metadata associated with tasks (defaults to unknown)
63
+ *
64
+ * @example Basic Usage
65
+ * ```typescript
66
+ * const queue = createAsyncQueue<string>({ concurrency: 2 });
67
+ *
68
+ * const processedItems: string[] = [];
69
+ *
70
+ * queue.resultifyAdd(async () => {
71
+ * await delay(100);
72
+ * return 'task completed';
73
+ * }).then(result => {
74
+ * if (result.ok) processedItems.push(result.value);
75
+ * });
76
+ *
77
+ * await queue.onIdle();
78
+ * console.log('Processed:', processedItems);
79
+ * ```
80
+ *
81
+ * @example Error Recovery
82
+ * ```typescript
83
+ * const queue = createAsyncQueue<string>({
84
+ * stopOnError: true,
85
+ * rejectPendingOnError: false
86
+ * });
87
+ *
88
+ * // Add batch of tasks
89
+ * const items = ['item1', 'item2', 'bad-item', 'item3'];
90
+ * items.forEach(item => {
91
+ * queue.resultifyAdd(async () => {
92
+ * if (item === 'bad-item') throw new Error('Processing failed');
93
+ * return item.toUpperCase();
94
+ * });
95
+ * });
96
+ *
97
+ * await queue.onIdle();
98
+ *
99
+ * if (queue.isStopped) {
100
+ * console.log(`Stopped at ${queue.failed} failures, ${queue.size} remaining`);
101
+ * // Reset and continue with remaining tasks
102
+ * queue.reset();
103
+ * await queue.onIdle();
104
+ * }
105
+ * ```
106
+ *
107
+ * @example Lazy Start
108
+ * ```typescript
109
+ * const queue = createAsyncQueue<string>({ autoStart: false });
110
+ *
111
+ * // Prepare all tasks without starting
112
+ * queue.resultifyAdd(() => processTask1());
113
+ * queue.resultifyAdd(() => processTask2());
114
+ * queue.resultifyAdd(() => processTask3());
115
+ *
116
+ * // Start processing when ready
117
+ * queue.start();
118
+ * await queue.onIdle();
119
+ * ```
120
+ */
20
121
  declare class AsyncQueue<T, E extends ResultValidErrors = Error, I = unknown> {
21
122
  #private;
123
+ /**
124
+ * Event emitter for tracking task lifecycle
125
+ *
126
+ * @example Listening to Events
127
+ * ```typescript
128
+ * const queue = createAsyncQueue<string>();
129
+ *
130
+ * queue.events.on('start', (event) => {
131
+ * console.log('Task started:', event.payload.meta);
132
+ * });
133
+ *
134
+ * queue.events.on('complete', (event) => {
135
+ * console.log('Task completed:', event.payload.value);
136
+ * });
137
+ *
138
+ * queue.events.on('error', (event) => {
139
+ * console.error('Task failed:', event.payload.error);
140
+ * });
141
+ * ```
142
+ */
22
143
  events: evtmitter.Emitter<{
23
- error: {
144
+ /** Emitted when a task starts executing */
145
+ start: {
24
146
  meta: I;
25
- error: E | Error;
26
147
  };
148
+ /** Emitted when a task completes successfully */
27
149
  complete: {
28
150
  meta: I;
29
151
  value: T;
30
152
  };
31
- start: {
153
+ /** Emitted when a task fails */
154
+ error: {
32
155
  meta: I;
156
+ error: E | Error;
33
157
  };
34
158
  }>;
159
+ /** Array of all task failures with metadata for debugging and analysis */
35
160
  failures: Array<{
36
161
  meta: I;
37
162
  error: E | Error;
38
163
  }>;
164
+ /** Array of all task completions with metadata for debugging and analysis */
39
165
  completions: Array<{
40
166
  meta: I;
41
167
  value: T;
42
168
  }>;
43
- constructor({ concurrency, signal, timeout: taskTimeout, }?: AsyncQueueOptions);
169
+ constructor({ concurrency, signal, timeout: taskTimeout, stopOnError, rejectPendingOnError, autoStart, rateLimit, }?: AsyncQueueOptions);
170
+ /**
171
+ * Add a task that returns a Result to the queue
172
+ *
173
+ * Use this method when your task function already returns a Result type.
174
+ * For functions that throw errors or return plain values, use `resultifyAdd` instead.
175
+ *
176
+ * @param fn - Task function that returns a Result
177
+ * @param options - Optional configuration for this task
178
+ * @returns Promise that resolves with the task result
179
+ *
180
+ * @example
181
+ * ```typescript
182
+ * const queue = createAsyncQueue<string>();
183
+ *
184
+ * const result = await queue.add(async () => {
185
+ * try {
186
+ * const data = await fetchData();
187
+ * return Result.ok(data);
188
+ * } catch (error) {
189
+ * return Result.err(error);
190
+ * }
191
+ * });
192
+ *
193
+ * if (result.ok) {
194
+ * console.log('Success:', result.value);
195
+ * } else {
196
+ * console.log('Error:', result.error);
197
+ * }
198
+ * ```
199
+ */
44
200
  add(fn: (ctx: RunCtx<I>) => Promise<Result<T, E>> | Result<T, E>, options?: AddOptions<I, T, E>): Promise<Result<T, E | Error>>;
201
+ /**
202
+ * Add a task that returns a plain value or throws errors to the queue
203
+ *
204
+ * This is the most commonly used method. It automatically wraps your function
205
+ * to handle errors and convert them to Result types.
206
+ *
207
+ * @param fn - Task function that returns a value or throws
208
+ * @param options - Optional configuration for this task
209
+ * @returns Promise that resolves with the task result wrapped in Result
210
+ *
211
+ * @example Basic Usage
212
+ * ```typescript
213
+ * const queue = createAsyncQueue<string>();
214
+ *
215
+ * queue.resultifyAdd(async () => {
216
+ * const response = await fetch('/api/data');
217
+ * return response.json();
218
+ * }).then(result => {
219
+ * if (result.ok) {
220
+ * console.log('Data:', result.value);
221
+ * } else {
222
+ * console.error('Failed:', result.error);
223
+ * }
224
+ * });
225
+ * ```
226
+ *
227
+ * @example With Callbacks
228
+ * ```typescript
229
+ * queue.resultifyAdd(
230
+ * async () => processData(),
231
+ * {
232
+ * onComplete: (data) => console.log('Processed:', data),
233
+ * onError: (error) => console.error('Failed:', error),
234
+ * timeout: 5000
235
+ * }
236
+ * );
237
+ * ```
238
+ */
45
239
  resultifyAdd(fn: (ctx: RunCtx<I>) => Promise<T> | T, options?: AddOptions<I, T, E>): Promise<Result<T, E | Error>>;
240
+ /**
241
+ * Wait for the queue to become idle (no pending tasks, no queued tasks, and no rate-limit timers)
242
+ *
243
+ * This method resolves when:
244
+ * - All tasks have completed (success or failure)
245
+ * - The queue is stopped due to error (stopOnError), even with remaining tasks
246
+ * - There are no queued tasks, no running tasks, and no pending rate-limit timers
247
+ *
248
+ * @returns Promise that resolves when the queue is idle
249
+ *
250
+ * @example
251
+ * ```typescript
252
+ * const queue = createAsyncQueue<string>();
253
+ *
254
+ * // Add multiple tasks
255
+ * for (let i = 0; i < 10; i++) {
256
+ * queue.resultifyAdd(async () => `task ${i}`);
257
+ * }
258
+ *
259
+ * // Wait for all tasks to complete
260
+ * await queue.onIdle();
261
+ *
262
+ * console.log(`Completed: ${queue.completed}, Failed: ${queue.failed}`);
263
+ * ```
264
+ */
46
265
  onIdle(): Promise<void>;
266
+ /**
267
+ * Wait until the queued task count is below a limit
268
+ *
269
+ * Resolves immediately if `size < limit` at the moment of calling. This only
270
+ * considers queued (not yet started) tasks; running tasks are tracked by
271
+ * `pending`.
272
+ *
273
+ * @param limit Threshold that `size` must be below to resolve
274
+ */
275
+ onSizeLessThan(limit: number): Promise<void>;
276
+ /**
277
+ * Clear all queued tasks (does not affect currently running tasks)
278
+ *
279
+ * This removes all tasks waiting in the queue but allows currently
280
+ * executing tasks to complete normally.
281
+ *
282
+ * @example
283
+ * ```typescript
284
+ * const queue = createAsyncQueue({ concurrency: 1 });
285
+ *
286
+ * // Add multiple tasks
287
+ * queue.resultifyAdd(async () => longRunningTask()); // Will start immediately
288
+ * queue.resultifyAdd(async () => task2()); // Queued
289
+ * queue.resultifyAdd(async () => task3()); // Queued
290
+ *
291
+ * // Clear remaining queued tasks
292
+ * queue.clear();
293
+ *
294
+ * // Only the first task will complete
295
+ * await queue.onIdle();
296
+ * ```
297
+ */
47
298
  clear(): void;
299
+ /** Number of tasks that have completed successfully */
48
300
  get completed(): number;
301
+ /** Number of tasks that have failed */
49
302
  get failed(): number;
303
+ /** Number of tasks currently being processed */
50
304
  get pending(): number;
305
+ /** Number of tasks waiting in the queue to be processed */
51
306
  get size(): number;
307
+ /**
308
+ * Manually start processing tasks (only needed if autoStart: false)
309
+ *
310
+ * @example
311
+ * ```typescript
312
+ * const queue = createAsyncQueue({ autoStart: false });
313
+ *
314
+ * // Add tasks without starting processing
315
+ * queue.resultifyAdd(async () => 'task1');
316
+ * queue.resultifyAdd(async () => 'task2');
317
+ *
318
+ * // Start processing when ready
319
+ * queue.start();
320
+ * await queue.onIdle();
321
+ * ```
322
+ */
323
+ start(): void;
324
+ /**
325
+ * Pause processing new tasks (currently running tasks continue)
326
+ *
327
+ * @example
328
+ * ```typescript
329
+ * const queue = createAsyncQueue();
330
+ *
331
+ * // Start some tasks
332
+ * queue.resultifyAdd(async () => longRunningTask1());
333
+ * queue.resultifyAdd(async () => longRunningTask2());
334
+ *
335
+ * // Pause before more tasks are picked up
336
+ * queue.pause();
337
+ *
338
+ * // Later, resume processing
339
+ * queue.resume();
340
+ * ```
341
+ */
342
+ pause(): void;
343
+ /**
344
+ * Resume processing tasks after pause
345
+ */
346
+ resume(): void;
347
+ /**
348
+ * Reset the queue after being stopped, allowing new tasks to be processed
349
+ *
350
+ * This clears the stopped state and error reason, and resumes processing
351
+ * any remaining queued tasks if autoStart was enabled.
352
+ *
353
+ * @example
354
+ * ```typescript
355
+ * const queue = createAsyncQueue({ stopOnError: true });
356
+ *
357
+ * // Add tasks that will cause the queue to stop
358
+ * queue.resultifyAdd(async () => { throw new Error('fail'); });
359
+ * queue.resultifyAdd(async () => 'remaining task');
360
+ *
361
+ * await queue.onIdle();
362
+ *
363
+ * if (queue.isStopped) {
364
+ * console.log(`Queue stopped, ${queue.size} tasks remaining`);
365
+ *
366
+ * // Reset and process remaining tasks
367
+ * queue.reset();
368
+ * await queue.onIdle();
369
+ * }
370
+ * ```
371
+ */
372
+ reset(): void;
373
+ /** Whether the queue is stopped due to an error */
374
+ get isStopped(): boolean;
375
+ /** Whether the queue is currently paused */
376
+ get isPaused(): boolean;
377
+ /** Whether the queue has been started (relevant for autoStart: false) */
378
+ get isStarted(): boolean;
379
+ /** The error that caused the queue to stop (if any) */
380
+ get stoppedReason(): Error | undefined;
52
381
  }
382
+ /**
383
+ * AddOptions variant that requires metadata to be provided
384
+ */
53
385
  type AddOptionsWithId<I, T, E extends ResultValidErrors> = Omit<AddOptions<I, T, E>, 'meta'> & {
54
386
  meta: I;
55
387
  };
388
+ /**
389
+ * AsyncQueue variant that requires metadata for all tasks
390
+ *
391
+ * This class enforces that every task must include metadata, which is useful
392
+ * when you need to track or identify tasks consistently.
393
+ *
394
+ * @template T - The type of value returned by successful tasks
395
+ * @template I - The type of metadata (required for all tasks)
396
+ * @template E - The type of errors that tasks can produce
397
+ *
398
+ * @example
399
+ * ```typescript
400
+ * interface TaskMeta {
401
+ * id: string;
402
+ * priority: number;
403
+ * }
404
+ *
405
+ * const queue = createAsyncQueueWithMeta<string, TaskMeta>({ concurrency: 2 });
406
+ *
407
+ * queue.resultifyAdd(
408
+ * async () => processImportantTask(),
409
+ * { meta: { id: 'task-1', priority: 1 } }
410
+ * );
411
+ *
412
+ * // Listen to events with metadata
413
+ * queue.events.on('complete', (event) => {
414
+ * console.log(`Task ${event.payload.meta.id} completed`);
415
+ * });
416
+ * ```
417
+ */
56
418
  declare class AsyncQueueWithMeta<T, I, E extends ResultValidErrors = Error> extends AsyncQueue<T, E, I> {
57
419
  constructor(options?: AsyncQueueOptions);
58
420
  add(fn: (ctx: RunCtx<I>) => Promise<Result<T, E>> | Result<T, E>, options: AddOptionsWithId<I, T, E>): Promise<Result<T, E | Error>>;
59
421
  resultifyAdd(fn: (ctx: RunCtx<I>) => Promise<T> | T, options: AddOptionsWithId<I, T, E>): Promise<Result<T, E | Error>>;
60
422
  }
423
+ /**
424
+ * Create a new AsyncQueue instance
425
+ *
426
+ * @template T - The type of value returned by successful tasks
427
+ * @template E - The type of errors that tasks can produce (defaults to Error)
428
+ * @param options - Configuration options for the queue
429
+ * @returns A new AsyncQueue instance
430
+ *
431
+ * @example Basic Queue
432
+ * ```typescript
433
+ * const queue = createAsyncQueue<string>({ concurrency: 3 });
434
+ * ```
435
+ *
436
+ * @example Error Handling Queue
437
+ * ```typescript
438
+ * const queue = createAsyncQueue<string>({
439
+ * concurrency: 2,
440
+ * stopOnError: true,
441
+ * rejectPendingOnError: true
442
+ * });
443
+ * ```
444
+ *
445
+ * @example Lazy Start Queue
446
+ * ```typescript
447
+ * const queue = createAsyncQueue<string>({
448
+ * autoStart: false,
449
+ * concurrency: 1
450
+ * });
451
+ * ```
452
+ */
61
453
  declare function createAsyncQueue<T, E extends ResultValidErrors = Error>(options?: AsyncQueueOptions): AsyncQueue<T, E>;
454
+
455
+ /**
456
+ * Create a new AsyncQueueWithMeta instance that requires metadata for all tasks
457
+ *
458
+ * @template T - The type of value returned by successful tasks
459
+ * @template I - The type of metadata (required for all tasks)
460
+ * @template E - The type of errors that tasks can produce (defaults to Error)
461
+ * @param options - Configuration options for the queue
462
+ * @returns A new AsyncQueueWithMeta instance
463
+ *
464
+ * @example
465
+ * ```typescript
466
+ * interface TaskInfo {
467
+ * taskId: string;
468
+ * userId: string;
469
+ * }
470
+ *
471
+ * const queue = createAsyncQueueWithMeta<ProcessResult, TaskInfo>({
472
+ * concurrency: 5
473
+ * });
474
+ *
475
+ * queue.resultifyAdd(
476
+ * async (ctx) => {
477
+ * console.log(`Processing task ${ctx.meta.taskId} for user ${ctx.meta.userId}`);
478
+ * return await processUserTask(ctx.meta.userId);
479
+ * },
480
+ * { meta: { taskId: '123', userId: 'user456' } }
481
+ * );
482
+ * ```
483
+ */
62
484
  declare function createAsyncQueueWithMeta<T, I, E extends ResultValidErrors = Error>(options?: AsyncQueueOptions): AsyncQueueWithMeta<T, I, E>;
63
485
 
64
- export { createAsyncQueue, createAsyncQueueWithMeta };
486
+ export { AsyncQueue, AsyncQueueWithMeta, createAsyncQueue, createAsyncQueueWithMeta };