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 +368 -0
- package/dist/index.cjs +450 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +282 -0
- package/dist/index.d.ts +282 -0
- package/dist/index.js +440 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
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
|
+
[](https://www.npmjs.com/package/async-flex-loop)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](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)
|