async-flex-loop 1.0.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 ADDED
@@ -0,0 +1,368 @@
1
+ # async-flex-loop
2
+
3
+ > Flexible async array processing with dynamic concurrency control, retry logic, timeouts, and lifecycle callbacks.
4
+
5
+ [![npm version](https://img.shields.io/badge/npm-1.0.0-CB3837?logo=npm)](https://www.npmjs.com/package/async-flex-loop)
6
+ [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
7
+ [![types](https://img.shields.io/badge/types-TypeScript-3178C6?logo=typescript)](https://www.npmjs.com/package/async-flex-loop)
8
+
9
+ ---
10
+
11
+ ## Features
12
+
13
+ - 🚦 **Concurrency control** β€” limit how many tasks run simultaneously
14
+ - πŸ”„ **Retry with backoff** β€” automatic retries with exponential backoff
15
+ - ⏱️ **Per-task timeout** β€” abort tasks that take too long
16
+ - ⏸️ **Pause / Resume** β€” full lifecycle control at any time
17
+ - πŸ“¬ **Dynamic push** β€” add items while the queue is running
18
+ - πŸ“Š **Result collection** β€” ordered results like `Promise.allSettled()`
19
+ - 🎯 **Lifecycle callbacks** β€” `onError`, `onTaskComplete`, `onProgress`, `onIdle`
20
+ - πŸ”‘ **Callback context** β€” callbacks receive the queue instance as `this`
21
+ - πŸ“¦ **Dual format** β€” ships as both ESM and CJS
22
+
23
+ ---
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ npm install async-flex-loop
29
+ # or
30
+ yarn add async-flex-loop
31
+ # or
32
+ bun add async-flex-loop
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Quick Start
38
+
39
+ ```typescript
40
+ import { AsyncFlexLoop } from "async-flex-loop";
41
+
42
+ const urls = ["https://api.example.com/1", "https://api.example.com/2", "https://api.example.com/3"];
43
+
44
+ const queue = new AsyncFlexLoop(
45
+ urls,
46
+ async (url) => {
47
+ const res = await fetch(url);
48
+ return res.json();
49
+ },
50
+ { concurrency: 2 },
51
+ );
52
+
53
+ await queue.onIdle();
54
+ const results = await queue.getResults();
55
+ console.log(results); // [data1, data2, data3] β€” in original order
56
+ ```
57
+
58
+ ---
59
+
60
+ ## API Reference
61
+
62
+ ### Constructor
63
+
64
+ ```typescript
65
+ new AsyncFlexLoop<InputType, ResponseType>(
66
+ initialItems: InputType[],
67
+ handler: (item: InputType, index: number) => Promise<ResponseType>,
68
+ options?: AsyncFlexLoopOptions
69
+ )
70
+ ```
71
+
72
+ | Parameter | Type | Description |
73
+ | -------------- | ----------------------------- | ----------------------------------- |
74
+ | `initialItems` | `InputType[]` | Items to process |
75
+ | `handler` | `(item, index) => Promise<R>` | Async function called for each item |
76
+ | `options` | `AsyncFlexLoopOptions` | Optional configuration (see below) |
77
+
78
+ ---
79
+
80
+ ### Options
81
+
82
+ | Option | Type | Default | Description |
83
+ | ---------------- | ---------- | ----------- | --------------------------------------------------------------- |
84
+ | `concurrency` | `number` | `Infinity` | Maximum number of concurrent tasks |
85
+ | `autoStart` | `boolean` | `true` | Start processing immediately on construction |
86
+ | `retry` | `number` | `0` | Number of retry attempts on failure |
87
+ | `retryDelay` | `number` | `0` | Base delay between retries (ms) |
88
+ | `retryBackoff` | `number` | `1` | Exponential backoff multiplier (`retryDelay * backoff^attempt`) |
89
+ | `delayAfterTask` | `number` | `0` | Delay after each task completes (ms) |
90
+ | `timeout` | `number` | `undefined` | Per-task timeout in ms β€” timed-out tasks are never retried |
91
+ | `yieldLoop` | `boolean` | `true` | Yield to event loop between tasks to avoid blocking |
92
+ | `throwOnError` | `boolean` | `true` | Reject `onIdle()` and stop queue on unrecoverable error |
93
+ | `onError` | `function` | `undefined` | Called when a task fails (after all retries) |
94
+ | `onTaskComplete` | `function` | `undefined` | Called after every task (success or failure) |
95
+ | `onProgress` | `function` | `undefined` | Called after each result is recorded |
96
+ | `onIdle` | `function` | `undefined` | Called when the queue becomes idle |
97
+
98
+ ---
99
+
100
+ ### Methods
101
+
102
+ #### Queue Control
103
+
104
+ | Method | Description |
105
+ | ---------- | ------------------------------------------------------------- |
106
+ | `start()` | Start processing. No-op if already running; resumes if paused |
107
+ | `pause()` | Pause after current in-flight tasks finish |
108
+ | `resume()` | Resume from a paused state |
109
+ | `clear()` | Remove all pending (unstarted) items from the queue |
110
+
111
+ #### Adding Items
112
+
113
+ ```typescript
114
+ const indices = queue.push(item1, item2, item3);
115
+ // Returns the assigned index for each item
116
+ ```
117
+
118
+ - Auto-resumes from `Idle` state when new items are pushed
119
+ - Respects `Paused` state β€” items are queued but not processed
120
+
121
+ #### Waiting & Results
122
+
123
+ | Method | Returns | Description |
124
+ | ---------------------------- | ---------------------------------------- | ----------------------------------------------------------- |
125
+ | `onIdle()` | `Promise<void>` | Resolves when queue is empty and all tasks have completed |
126
+ | `getResults()` | `Promise<(ResponseType \| undefined)[]>` | Ordered results; `undefined` for failed tasks |
127
+ | `getRawResults()` | `Promise<TaskResult[]>` | Full results with error details (like `Promise.allSettled`) |
128
+ | `getCompletedResults()` | `ResponseType[]` | Successful values only (does not wait for idle) |
129
+ | `getCompletedResultsAsync()` | `Promise<ResponseType[]>` | Successful values only (waits for idle) |
130
+
131
+ #### Introspection
132
+
133
+ | Method | Returns | Description |
134
+ | --------------------- | ------------------------ | ------------------------------------- |
135
+ | `getState()` | `QueueState` | Current state enum value |
136
+ | `isRunning()` | `boolean` | `true` if state is `Processing` |
137
+ | `getPendingCount()` | `number` | Items remaining in the queue |
138
+ | `getProcessedCount()` | `number` | Items completed (success or failure) |
139
+ | `getNextItem()` | `InputType \| undefined` | Peek at the next item to be processed |
140
+ | `getStats()` | `QueueStats` | Processing statistics |
141
+
142
+ ---
143
+
144
+ ### Queue States
145
+
146
+ ```typescript
147
+ import { QueueState } from "async-flex-loop";
148
+ ```
149
+
150
+ | State | Description |
151
+ | ------------ | ---------------------------------------------- |
152
+ | `Pending` | Created but not yet started |
153
+ | `Processing` | Actively processing items |
154
+ | `Paused` | Paused β€” in-flight tasks finish, new ones wait |
155
+ | `Idle` | Queue empty and all tasks completed |
156
+
157
+ ```
158
+ Pending β†’ Processing β†’ Idle
159
+ ↕
160
+ Paused
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Examples
166
+
167
+ ### Concurrency Control
168
+
169
+ ```typescript
170
+ const queue = new AsyncFlexLoop(
171
+ Array.from({ length: 100 }, (_, i) => i),
172
+ async (item) => {
173
+ await fetch(`https://api.example.com/items/${item}`);
174
+ return item;
175
+ },
176
+ { concurrency: 5 }, // max 5 requests at a time
177
+ );
178
+
179
+ await queue.onIdle();
180
+ ```
181
+
182
+ ### Retry with Exponential Backoff
183
+
184
+ ```typescript
185
+ const queue = new AsyncFlexLoop(
186
+ ["task1", "task2", "task3"],
187
+ async (task) => {
188
+ const res = await fetch(`https://flaky-api.com/${task}`);
189
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
190
+ return res.json();
191
+ },
192
+ {
193
+ retry: 3, // up to 3 retries
194
+ retryDelay: 500, // start with 500ms delay
195
+ retryBackoff: 2, // double delay each attempt: 500 β†’ 1000 β†’ 2000ms
196
+ },
197
+ );
198
+ ```
199
+
200
+ ### Per-Task Timeout
201
+
202
+ ```typescript
203
+ import { TimeoutError } from "async-flex-loop";
204
+
205
+ const queue = new AsyncFlexLoop(items, async (item) => slowOperation(item), {
206
+ timeout: 3000, // 3s per task
207
+ throwOnError: false, // continue on timeout instead of stopping
208
+ onError(error, item, index) {
209
+ if (error instanceof TimeoutError) {
210
+ console.warn(`Item ${index} timed out`);
211
+ }
212
+ },
213
+ });
214
+ ```
215
+
216
+ ### Pause & Resume
217
+
218
+ ```typescript
219
+ const queue = new AsyncFlexLoop(largeDataset, processItem, { concurrency: 3 });
220
+
221
+ // Pause after 2 seconds
222
+ setTimeout(() => queue.pause(), 2000);
223
+
224
+ // Resume after another 2 seconds
225
+ setTimeout(() => queue.resume(), 4000);
226
+
227
+ await queue.onIdle();
228
+ ```
229
+
230
+ ### Dynamic Push
231
+
232
+ ```typescript
233
+ const queue = new AsyncFlexLoop([], processItem, { autoStart: false });
234
+
235
+ // Add items later
236
+ queue.push("a", "b", "c");
237
+ queue.start();
238
+
239
+ // Add more while running
240
+ setTimeout(() => queue.push("d", "e"), 500);
241
+
242
+ await queue.onIdle();
243
+ ```
244
+
245
+ ### Callbacks with Queue Access
246
+
247
+ All callbacks receive the `AsyncFlexLoop` instance as `this`, giving access to queue methods:
248
+
249
+ ```typescript
250
+ const queue = new AsyncFlexLoop(items, processItem, {
251
+ throwOnError: false,
252
+ onError(error, item, index) {
253
+ console.error(`[${index}] Failed: ${error.message}`);
254
+ console.log(`${this.getPendingCount()} items remaining`);
255
+
256
+ // Pause if too many errors
257
+ const stats = this.getStats();
258
+ if (stats.totalFailed > 5) {
259
+ this.pause();
260
+ }
261
+ },
262
+
263
+ onIdle() {
264
+ const stats = this.getStats();
265
+ console.log(`Done! Success rate: ${(stats.successRate * 100).toFixed(1)}%`);
266
+ },
267
+ });
268
+ ```
269
+
270
+ > ⚠️ Use regular `function` syntax (not arrow functions) to access `this`.
271
+
272
+ ### Collect Results
273
+
274
+ ```typescript
275
+ const queue = new AsyncFlexLoop([1, 2, 3, 4, 5], async (n) => n * 2, {
276
+ throwOnError: false,
277
+ });
278
+
279
+ // Wait and get all results (undefined for failures)
280
+ const results = await queue.getResults();
281
+ // [2, 4, 6, 8, 10]
282
+
283
+ // Get raw results with status (like Promise.allSettled)
284
+ const raw = await queue.getRawResults();
285
+ // [
286
+ // { status: "fulfilled", value: 2, index: 0 },
287
+ // { status: "rejected", reason: Error, item: 2, index: 1 },
288
+ // ...
289
+ // ]
290
+
291
+ // Get only successful values (no waiting)
292
+ const successes = queue.getCompletedResults();
293
+ ```
294
+
295
+ ### Statistics
296
+
297
+ ```typescript
298
+ await queue.onIdle();
299
+
300
+ const stats = queue.getStats();
301
+ console.log(stats);
302
+ // {
303
+ // totalProcessed: 100,
304
+ // totalSuccess: 97,
305
+ // totalFailed: 3,
306
+ // avgProcessingTime: 142, // ms
307
+ // successRate: 0.97,
308
+ // }
309
+ ```
310
+
311
+ ---
312
+
313
+ ## Error Types
314
+
315
+ ```typescript
316
+ import { TimeoutError, MaxRetryError, QueueAbortError } from "async-flex-loop";
317
+ ```
318
+
319
+ | Error | When thrown |
320
+ | ----------------- | ------------------------------------------------------------------------------------ |
321
+ | `TimeoutError` | Task exceeds `timeout` ms. Has `.item`, `.index`, `.timeoutMs` |
322
+ | `MaxRetryError` | Task fails after all retries. Has `.item`, `.index`, `.retryCount`, `.originalError` |
323
+ | `QueueAbortError` | Queue is manually aborted |
324
+
325
+ ---
326
+
327
+ ## Advanced Utilities
328
+
329
+ These are exported for advanced use cases:
330
+
331
+ ```typescript
332
+ import { delay, yieldToEventLoop, calculateRetryDelay, withTimeout } from "async-flex-loop";
333
+
334
+ // Sleep for N milliseconds
335
+ await delay(500);
336
+
337
+ // Yield control back to the event loop
338
+ await yieldToEventLoop();
339
+
340
+ // Calculate backoff delay for a given attempt
341
+ const ms = calculateRetryDelay(baseDelay, backoff, attemptNumber);
342
+
343
+ // Wrap a promise with a timeout
344
+ const result = await withTimeout(myPromise, 3000, item, index);
345
+ ```
346
+
347
+ ---
348
+
349
+ ## TypeScript
350
+
351
+ `async-flex-loop` is written in TypeScript and ships with full type declarations.
352
+
353
+ ```typescript
354
+ import type { AsyncFlexLoopOptions, TaskResult, QueueItem, QueueStats } from "async-flex-loop";
355
+
356
+ // Fully typed
357
+ const queue = new AsyncFlexLoop<string, { id: number }>(
358
+ ["a", "b", "c"],
359
+ async (item): Promise<{ id: number }> => ({ id: item.charCodeAt(0) }),
360
+ { concurrency: 2 },
361
+ );
362
+ ```
363
+
364
+ ---
365
+
366
+ ## License
367
+
368
+ MIT Β© [TanMacDuc](mailto:tanducmac@gmail.com)