flowx-control 1.0.3 → 1.0.5
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/CHANGELOG.md +22 -0
- package/README.md +54 -324
- package/package.json +3 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to **flowx-control** will be documented here.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Planned
|
|
10
|
+
- Actor model support
|
|
11
|
+
- Distributed rate limiting
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## [1.0.0] - 2026-03-31
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Initial release
|
|
19
|
+
- Retry with backoff, Circuit Breaker, Bulkhead
|
|
20
|
+
- Rate Limiter, Priority Queue, Semaphore, Mutex
|
|
21
|
+
- Debounce, Throttle, Timeout, Hedge, Poll, Batch, Pipeline
|
|
22
|
+
- 100% test coverage, tree-shakable
|
package/README.md
CHANGED
|
@@ -32,13 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
34
|
npm install flowx-control
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
35
|
yarn add flowx-control
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
36
|
pnpm add flowx-control
|
|
43
37
|
```
|
|
44
38
|
|
|
@@ -56,19 +50,19 @@ const data = await retry(() => fetch('/api/data'), {
|
|
|
56
50
|
backoff: 'exponential',
|
|
57
51
|
});
|
|
58
52
|
|
|
59
|
-
// Circuit breaker
|
|
53
|
+
// Circuit breaker
|
|
60
54
|
const breaker = createCircuitBreaker(fetchUser, {
|
|
61
55
|
failureThreshold: 5,
|
|
62
56
|
resetTimeout: 30_000,
|
|
63
57
|
});
|
|
64
58
|
const user = await breaker.fire(userId);
|
|
65
59
|
|
|
66
|
-
// Timeout
|
|
60
|
+
// Timeout
|
|
67
61
|
const result = await withTimeout(() => fetch('/slow'), 5000, {
|
|
68
62
|
fallback: () => cachedResponse,
|
|
69
63
|
});
|
|
70
64
|
|
|
71
|
-
// Rate limiter
|
|
65
|
+
// Rate limiter
|
|
72
66
|
const limiter = createRateLimiter({ limit: 10, interval: 1000 });
|
|
73
67
|
await limiter.execute(() => callExternalApi());
|
|
74
68
|
```
|
|
@@ -79,318 +73,37 @@ await limiter.execute(() => callExternalApi());
|
|
|
79
73
|
|
|
80
74
|
### 🛡️ Resilience
|
|
81
75
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
import { retry } from 'flowx-control/retry';
|
|
87
|
-
|
|
88
|
-
const data = await retry(() => fetch('/api'), {
|
|
89
|
-
maxAttempts: 5,
|
|
90
|
-
delay: 1000,
|
|
91
|
-
backoff: 'exponential', // 'fixed' | 'linear' | 'exponential' | custom fn
|
|
92
|
-
jitter: true,
|
|
93
|
-
retryIf: (err) => err.status !== 404,
|
|
94
|
-
onRetry: (err, attempt) => console.log(`Attempt ${attempt}`),
|
|
95
|
-
signal: abortController.signal,
|
|
96
|
-
});
|
|
97
|
-
```
|
|
98
|
-
</details>
|
|
99
|
-
|
|
100
|
-
<details>
|
|
101
|
-
<summary><strong>circuitBreaker</strong> — Stop cascading failures</summary>
|
|
102
|
-
|
|
103
|
-
```ts
|
|
104
|
-
import { createCircuitBreaker } from 'flowx-control/circuit-breaker';
|
|
105
|
-
|
|
106
|
-
const breaker = createCircuitBreaker(callApi, {
|
|
107
|
-
failureThreshold: 5,
|
|
108
|
-
resetTimeout: 30000,
|
|
109
|
-
halfOpenLimit: 1,
|
|
110
|
-
successThreshold: 2,
|
|
111
|
-
shouldTrip: (err) => err.status >= 500,
|
|
112
|
-
onStateChange: (from, to) => log(`${from} → ${to}`),
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const result = await breaker.fire(args);
|
|
116
|
-
console.log(breaker.state); // 'closed' | 'open' | 'half-open'
|
|
117
|
-
console.log(breaker.failureCount);
|
|
118
|
-
breaker.reset();
|
|
119
|
-
```
|
|
120
|
-
</details>
|
|
121
|
-
|
|
122
|
-
<details>
|
|
123
|
-
<summary><strong>fallback</strong> — Graceful degradation</summary>
|
|
124
|
-
|
|
125
|
-
```ts
|
|
126
|
-
import { withFallback, fallbackChain } from 'flowx-control/fallback';
|
|
127
|
-
|
|
128
|
-
const data = await withFallback(
|
|
129
|
-
() => fetchFromPrimary(),
|
|
130
|
-
'default-value',
|
|
131
|
-
{ onFallback: (err, idx) => console.warn(err) }
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
const result = await fallbackChain([
|
|
135
|
-
() => fetchFromPrimary(),
|
|
136
|
-
() => fetchFromCache(),
|
|
137
|
-
() => fetchFromFallback(),
|
|
138
|
-
]);
|
|
139
|
-
```
|
|
140
|
-
</details>
|
|
141
|
-
|
|
142
|
-
<details>
|
|
143
|
-
<summary><strong>timeout</strong> — Never wait forever</summary>
|
|
144
|
-
|
|
145
|
-
```ts
|
|
146
|
-
import { withTimeout } from 'flowx-control/timeout';
|
|
147
|
-
|
|
148
|
-
const result = await withTimeout(() => fetch('/slow-api'), 5000, {
|
|
149
|
-
fallback: () => cachedData,
|
|
150
|
-
message: 'API took too long',
|
|
151
|
-
signal: controller.signal,
|
|
152
|
-
});
|
|
153
|
-
```
|
|
154
|
-
</details>
|
|
76
|
+
- **retry** — Exponential backoff, jitter, abort signal, custom retry predicates
|
|
77
|
+
- **circuitBreaker** — Closed/Open/Half-open state machine, trip hooks
|
|
78
|
+
- **fallback** — Graceful degradation with fallback chains
|
|
79
|
+
- **timeout** — Hard deadline + optional fallback value
|
|
155
80
|
|
|
156
81
|
### 🚦 Concurrency
|
|
157
82
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
import { createBulkhead } from 'flowx-control/bulkhead';
|
|
163
|
-
|
|
164
|
-
const bulkhead = createBulkhead({
|
|
165
|
-
maxConcurrent: 10,
|
|
166
|
-
maxQueue: 100,
|
|
167
|
-
queueTimeout: 5000,
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const result = await bulkhead.execute(() => processRequest());
|
|
171
|
-
console.log(bulkhead.activeCount, bulkhead.queueSize);
|
|
172
|
-
```
|
|
173
|
-
</details>
|
|
174
|
-
|
|
175
|
-
<details>
|
|
176
|
-
<summary><strong>queue</strong> — Priority async task queue</summary>
|
|
177
|
-
|
|
178
|
-
```ts
|
|
179
|
-
import { createQueue } from 'flowx-control/queue';
|
|
180
|
-
|
|
181
|
-
const queue = createQueue({ concurrency: 5, timeout: 10000 });
|
|
182
|
-
|
|
183
|
-
const result = await queue.add(() => processJob(), { priority: 1 });
|
|
184
|
-
const results = await queue.addAll(tasks.map(t => () => process(t)));
|
|
185
|
-
|
|
186
|
-
await queue.onIdle(); // wait until all done
|
|
187
|
-
queue.pause();
|
|
188
|
-
queue.resume();
|
|
189
|
-
```
|
|
190
|
-
</details>
|
|
191
|
-
|
|
192
|
-
<details>
|
|
193
|
-
<summary><strong>semaphore</strong> — Counting resource lock</summary>
|
|
194
|
-
|
|
195
|
-
```ts
|
|
196
|
-
import { createSemaphore } from 'flowx-control/semaphore';
|
|
197
|
-
|
|
198
|
-
const sem = createSemaphore(3); // max 3 concurrent
|
|
199
|
-
const release = await sem.acquire();
|
|
200
|
-
try {
|
|
201
|
-
await doWork();
|
|
202
|
-
} finally {
|
|
203
|
-
release();
|
|
204
|
-
}
|
|
205
|
-
```
|
|
206
|
-
</details>
|
|
207
|
-
|
|
208
|
-
<details>
|
|
209
|
-
<summary><strong>mutex</strong> — Mutual exclusion lock</summary>
|
|
210
|
-
|
|
211
|
-
```ts
|
|
212
|
-
import { createMutex } from 'flowx-control/mutex';
|
|
213
|
-
|
|
214
|
-
const mutex = createMutex();
|
|
215
|
-
const release = await mutex.acquire();
|
|
216
|
-
try {
|
|
217
|
-
await criticalSection();
|
|
218
|
-
} finally {
|
|
219
|
-
release();
|
|
220
|
-
}
|
|
221
|
-
```
|
|
222
|
-
</details>
|
|
83
|
+
- **bulkhead** — Max concurrent + max queue isolation
|
|
84
|
+
- **queue** — Priority async task queue with concurrency
|
|
85
|
+
- **semaphore** — Counting resource lock (acquire/release)
|
|
86
|
+
- **mutex** — Mutual exclusion for critical sections
|
|
223
87
|
|
|
224
88
|
### 🎛️ Flow Control
|
|
225
89
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const limiter = createRateLimiter({
|
|
233
|
-
limit: 100,
|
|
234
|
-
interval: 60_000,
|
|
235
|
-
strategy: 'queue', // 'queue' | 'reject'
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
await limiter.execute(() => callApi());
|
|
239
|
-
console.log(limiter.remaining);
|
|
240
|
-
limiter.reset();
|
|
241
|
-
```
|
|
242
|
-
</details>
|
|
243
|
-
|
|
244
|
-
<details>
|
|
245
|
-
<summary><strong>throttle</strong> — Rate-limit function calls</summary>
|
|
246
|
-
|
|
247
|
-
```ts
|
|
248
|
-
import { throttle } from 'flowx-control/throttle';
|
|
249
|
-
|
|
250
|
-
const save = throttle(saveToDb, 1000, {
|
|
251
|
-
leading: true,
|
|
252
|
-
trailing: true,
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
await save(data);
|
|
256
|
-
save.cancel();
|
|
257
|
-
```
|
|
258
|
-
</details>
|
|
259
|
-
|
|
260
|
-
<details>
|
|
261
|
-
<summary><strong>debounce</strong> — Delay until activity pauses</summary>
|
|
262
|
-
|
|
263
|
-
```ts
|
|
264
|
-
import { debounce } from 'flowx-control/debounce';
|
|
265
|
-
|
|
266
|
-
const search = debounce(searchApi, 300, {
|
|
267
|
-
leading: false,
|
|
268
|
-
trailing: true,
|
|
269
|
-
maxWait: 1000,
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
await search(query);
|
|
273
|
-
await search.flush();
|
|
274
|
-
search.cancel();
|
|
275
|
-
```
|
|
276
|
-
</details>
|
|
277
|
-
|
|
278
|
-
<details>
|
|
279
|
-
<summary><strong>batch</strong> — Process collections in chunks</summary>
|
|
280
|
-
|
|
281
|
-
```ts
|
|
282
|
-
import { batch } from 'flowx-control/batch';
|
|
283
|
-
|
|
284
|
-
const result = await batch(urls, async (url, i) => {
|
|
285
|
-
return fetch(url).then(r => r.json());
|
|
286
|
-
}, {
|
|
287
|
-
batchSize: 10,
|
|
288
|
-
concurrency: 3,
|
|
289
|
-
onProgress: (done, total) => console.log(`${done}/${total}`),
|
|
290
|
-
signal: controller.signal,
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
console.log(result.succeeded, result.failed, result.errors);
|
|
294
|
-
```
|
|
295
|
-
</details>
|
|
296
|
-
|
|
297
|
-
<details>
|
|
298
|
-
<summary><strong>pipeline</strong> — Compose async operations</summary>
|
|
299
|
-
|
|
300
|
-
```ts
|
|
301
|
-
import { pipeline, pipe } from 'flowx-control/pipeline';
|
|
302
|
-
|
|
303
|
-
const transform = pipe(
|
|
304
|
-
(input: string) => input.trim(),
|
|
305
|
-
(str) => str.toUpperCase(),
|
|
306
|
-
async (str) => await translate(str),
|
|
307
|
-
);
|
|
308
|
-
|
|
309
|
-
const result = await transform(' hello world ');
|
|
310
|
-
```
|
|
311
|
-
</details>
|
|
90
|
+
- **rateLimit** — Token bucket with queue/reject strategies
|
|
91
|
+
- **throttle** — Leading/trailing edge, cancellable
|
|
92
|
+
- **debounce** — maxWait support, flush/cancel
|
|
93
|
+
- **batch** — Process collections in chunks with progress
|
|
94
|
+
- **pipeline** — Compose sync/async operations
|
|
312
95
|
|
|
313
96
|
### 🛠️ Utilities
|
|
314
97
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
import { poll } from 'flowx-control/poll';
|
|
320
|
-
|
|
321
|
-
const { result, stop } = poll(() => checkJobStatus(jobId), {
|
|
322
|
-
interval: 2000,
|
|
323
|
-
until: (status) => status === 'complete',
|
|
324
|
-
maxAttempts: 30,
|
|
325
|
-
backoff: 'exponential',
|
|
326
|
-
signal: controller.signal,
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
const finalStatus = await result;
|
|
330
|
-
```
|
|
331
|
-
</details>
|
|
332
|
-
|
|
333
|
-
<details>
|
|
334
|
-
<summary><strong>hedge</strong> — Hedged/speculative requests</summary>
|
|
335
|
-
|
|
336
|
-
```ts
|
|
337
|
-
import { hedge } from 'flowx-control/hedge';
|
|
338
|
-
|
|
339
|
-
// If primary doesn't respond in 200ms, fire a parallel request
|
|
340
|
-
const data = await hedge(() => fetch('/api'), {
|
|
341
|
-
delay: 200,
|
|
342
|
-
maxHedges: 2,
|
|
343
|
-
});
|
|
344
|
-
```
|
|
345
|
-
</details>
|
|
346
|
-
|
|
347
|
-
<details>
|
|
348
|
-
<summary><strong>memo</strong> — Async memoization with TTL</summary>
|
|
349
|
-
|
|
350
|
-
```ts
|
|
351
|
-
import { memo } from 'flowx-control/memo';
|
|
352
|
-
|
|
353
|
-
const cachedFetch = memo(fetchUserById, {
|
|
354
|
-
ttl: 60_000,
|
|
355
|
-
maxSize: 1000,
|
|
356
|
-
key: (id) => `user:${id}`,
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
const user = await cachedFetch(123);
|
|
360
|
-
cachedFetch.clear();
|
|
361
|
-
```
|
|
362
|
-
</details>
|
|
363
|
-
|
|
364
|
-
<details>
|
|
365
|
-
<summary><strong>deferred</strong> — Externally resolvable promise</summary>
|
|
366
|
-
|
|
367
|
-
```ts
|
|
368
|
-
import { deferred } from 'flowx-control/deferred';
|
|
369
|
-
|
|
370
|
-
const d = deferred<string>();
|
|
371
|
-
setTimeout(() => d.resolve('hello'), 1000);
|
|
372
|
-
const value = await d.promise; // 'hello'
|
|
373
|
-
```
|
|
374
|
-
</details>
|
|
375
|
-
|
|
376
|
-
---
|
|
377
|
-
|
|
378
|
-
## Deep Imports (Tree-shaking)
|
|
379
|
-
|
|
380
|
-
Import only what you need — zero unused code in your bundle:
|
|
381
|
-
|
|
382
|
-
```ts
|
|
383
|
-
// Only pulls in ~2KB instead of the full 28KB
|
|
384
|
-
import { retry } from 'flowx-control/retry';
|
|
385
|
-
import { createQueue } from 'flowx-control/queue';
|
|
386
|
-
```
|
|
98
|
+
- **poll** — Repeated polling with backoff until condition
|
|
99
|
+
- **hedge** — Speculative parallel requests
|
|
100
|
+
- **memo** — Async memoization with TTL + max size
|
|
101
|
+
- **deferred** — Externally resolvable promise
|
|
387
102
|
|
|
388
103
|
---
|
|
389
104
|
|
|
390
105
|
## Error Hierarchy
|
|
391
106
|
|
|
392
|
-
All errors extend `FlowXError` with a machine-readable `code`:
|
|
393
|
-
|
|
394
107
|
| Error | Code | Thrown by |
|
|
395
108
|
|-------|------|----------|
|
|
396
109
|
| `TimeoutError` | `ERR_TIMEOUT` | `withTimeout` |
|
|
@@ -399,18 +112,6 @@ All errors extend `FlowXError` with a machine-readable `code`:
|
|
|
399
112
|
| `AbortError` | `ERR_ABORTED` | `poll`, `batch`, `timeout` |
|
|
400
113
|
| `RateLimitError` | `ERR_RATE_LIMIT` | `rateLimit` |
|
|
401
114
|
|
|
402
|
-
```ts
|
|
403
|
-
import { TimeoutError, FlowXError } from 'flowx-control';
|
|
404
|
-
|
|
405
|
-
try {
|
|
406
|
-
await withTimeout(fn, 1000);
|
|
407
|
-
} catch (err) {
|
|
408
|
-
if (err instanceof TimeoutError) {
|
|
409
|
-
console.log(err.code); // 'ERR_TIMEOUT'
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
```
|
|
413
|
-
|
|
414
115
|
---
|
|
415
116
|
|
|
416
117
|
## Compatibility
|
|
@@ -429,11 +130,9 @@ try {
|
|
|
429
130
|
|
|
430
131
|
```bash
|
|
431
132
|
git clone https://github.com/Avinashvelu03/flowx-control.git
|
|
432
|
-
cd flowx-control
|
|
433
|
-
npm
|
|
434
|
-
npm
|
|
435
|
-
npm run lint # ESLint
|
|
436
|
-
npm run build # Build ESM + CJS + DTS
|
|
133
|
+
cd flowx-control && npm install
|
|
134
|
+
npm test
|
|
135
|
+
npm run build
|
|
437
136
|
```
|
|
438
137
|
|
|
439
138
|
---
|
|
@@ -441,3 +140,34 @@ npm run build # Build ESM + CJS + DTS
|
|
|
441
140
|
## License
|
|
442
141
|
|
|
443
142
|
MIT © [Avinash](https://github.com/Avinashvelu03)
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## ⚡ Fuel the Flow
|
|
147
|
+
|
|
148
|
+
<div align="center">
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
· · · · · · · · · · · · · · · · · · · · · · · ·
|
|
152
|
+
· ·
|
|
153
|
+
· FlowX handles your retries, ·
|
|
154
|
+
· your circuit breakers, ·
|
|
155
|
+
· your race conditions, ·
|
|
156
|
+
· and your 3 AM production fires. ·
|
|
157
|
+
· ·
|
|
158
|
+
· If it earned your trust — fuel it. ·
|
|
159
|
+
· ·
|
|
160
|
+
· · · · · · · · · · · · · · · · · · · · · · · ·
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
[](https://ko-fi.com/avinashvelu)
|
|
164
|
+
[](https://github.com/sponsors/Avinashvelu03)
|
|
165
|
+
|
|
166
|
+
**No budget? No problem:**
|
|
167
|
+
- ⭐ [Star FlowX](https://github.com/Avinashvelu03/flowx-control) — boosts discovery
|
|
168
|
+
- 🐛 [Open an issue](https://github.com/Avinashvelu03/flowx-control/issues) — shape the roadmap
|
|
169
|
+
- 🗣️ Tell a dev who ships async code
|
|
170
|
+
|
|
171
|
+
*Built solo, shipped free — by [Avinash Velu](https://github.com/Avinashvelu03)*
|
|
172
|
+
|
|
173
|
+
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowx-control",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Production-grade, zero-dependency TypeScript resilience & async flow control library. 100% test coverage. Retry with backoff, Circuit Breaker, Bulkhead, Rate Limiter, Priority Queue, Semaphore, Mutex, Debounce, Throttle, Timeout, Hedge, Poll, Batch, Pipeline — all tree-shakable & composable.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -108,8 +108,9 @@
|
|
|
108
108
|
"test": "jest --coverage",
|
|
109
109
|
"test:ci": "jest --coverage",
|
|
110
110
|
"lint": "eslint src/ --ext .ts",
|
|
111
|
+
"typecheck": "tsc --noEmit",
|
|
111
112
|
"format": "prettier --write \"src/**/*.ts\" \"__tests__/**/*.ts\"",
|
|
112
|
-
"
|
|
113
|
+
"prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build"
|
|
113
114
|
},
|
|
114
115
|
"keywords": [
|
|
115
116
|
"async",
|