flowx-control 1.0.4 → 1.0.6
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 -274
- 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,267 +73,37 @@ await limiter.execute(() => callExternalApi());
|
|
|
79
73
|
|
|
80
74
|
### 🛡️ Resilience
|
|
81
75
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const data = await retry(() => fetch('/api'), {
|
|
88
|
-
maxAttempts: 5,
|
|
89
|
-
delay: 1000,
|
|
90
|
-
backoff: 'exponential', // 'fixed' | 'linear' | 'exponential' | custom fn
|
|
91
|
-
jitter: true,
|
|
92
|
-
retryIf: (err) => err.status !== 404,
|
|
93
|
-
onRetry: (err, attempt) => console.log(`Attempt ${attempt}`),
|
|
94
|
-
signal: abortController.signal,
|
|
95
|
-
});
|
|
96
|
-
```
|
|
97
|
-
#### circuitBreaker — Stop cascading failures
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
import { createCircuitBreaker } from 'flowx-control/circuit-breaker';
|
|
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
|
|
101
80
|
|
|
102
|
-
const breaker = createCircuitBreaker(callApi, {
|
|
103
|
-
failureThreshold: 5,
|
|
104
|
-
resetTimeout: 30000,
|
|
105
|
-
halfOpenLimit: 1,
|
|
106
|
-
successThreshold: 2,
|
|
107
|
-
shouldTrip: (err) => err.status >= 500,
|
|
108
|
-
onStateChange: (from, to) => log(`${from} → ${to}`),
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
const result = await breaker.fire(args);
|
|
112
|
-
console.log(breaker.state); // 'closed' | 'open' | 'half-open'
|
|
113
|
-
console.log(breaker.failureCount);
|
|
114
|
-
breaker.reset();
|
|
115
|
-
```
|
|
116
|
-
#### fallback — Graceful degradation
|
|
117
|
-
|
|
118
|
-
```ts
|
|
119
|
-
import { withFallback, fallbackChain } from 'flowx-control/fallback';
|
|
120
|
-
|
|
121
|
-
const data = await withFallback(
|
|
122
|
-
() => fetchFromPrimary(),
|
|
123
|
-
'default-value',
|
|
124
|
-
{ onFallback: (err, idx) => console.warn(err) }
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
const result = await fallbackChain([
|
|
128
|
-
() => fetchFromPrimary(),
|
|
129
|
-
() => fetchFromCache(),
|
|
130
|
-
() => fetchFromFallback(),
|
|
131
|
-
]);
|
|
132
|
-
```
|
|
133
|
-
#### timeout — Never wait forever
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
import { withTimeout } from 'flowx-control/timeout';
|
|
137
|
-
|
|
138
|
-
const result = await withTimeout(() => fetch('/slow-api'), 5000, {
|
|
139
|
-
fallback: () => cachedData,
|
|
140
|
-
message: 'API took too long',
|
|
141
|
-
signal: controller.signal,
|
|
142
|
-
});
|
|
143
|
-
```
|
|
144
81
|
### 🚦 Concurrency
|
|
145
82
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
150
87
|
|
|
151
|
-
const bulkhead = createBulkhead({
|
|
152
|
-
maxConcurrent: 10,
|
|
153
|
-
maxQueue: 100,
|
|
154
|
-
queueTimeout: 5000,
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
const result = await bulkhead.execute(() => processRequest());
|
|
158
|
-
console.log(bulkhead.activeCount, bulkhead.queueSize);
|
|
159
|
-
```
|
|
160
|
-
#### queue — Priority async task queue
|
|
161
|
-
|
|
162
|
-
```ts
|
|
163
|
-
import { createQueue } from 'flowx-control/queue';
|
|
164
|
-
|
|
165
|
-
const queue = createQueue({ concurrency: 5, timeout: 10000 });
|
|
166
|
-
|
|
167
|
-
const result = await queue.add(() => processJob(), { priority: 1 });
|
|
168
|
-
const results = await queue.addAll(tasks.map(t => () => process(t)));
|
|
169
|
-
|
|
170
|
-
await queue.onIdle(); // wait until all done
|
|
171
|
-
queue.pause();
|
|
172
|
-
queue.resume();
|
|
173
|
-
```
|
|
174
|
-
#### semaphore — Counting resource lock
|
|
175
|
-
|
|
176
|
-
```ts
|
|
177
|
-
import { createSemaphore } from 'flowx-control/semaphore';
|
|
178
|
-
|
|
179
|
-
const sem = createSemaphore(3); // max 3 concurrent
|
|
180
|
-
const release = await sem.acquire();
|
|
181
|
-
try {
|
|
182
|
-
await doWork();
|
|
183
|
-
} finally {
|
|
184
|
-
release();
|
|
185
|
-
}
|
|
186
|
-
```
|
|
187
|
-
#### mutex — Mutual exclusion lock
|
|
188
|
-
|
|
189
|
-
```ts
|
|
190
|
-
import { createMutex } from 'flowx-control/mutex';
|
|
191
|
-
|
|
192
|
-
const mutex = createMutex();
|
|
193
|
-
const release = await mutex.acquire();
|
|
194
|
-
try {
|
|
195
|
-
await criticalSection();
|
|
196
|
-
} finally {
|
|
197
|
-
release();
|
|
198
|
-
}
|
|
199
|
-
```
|
|
200
88
|
### 🎛️ Flow Control
|
|
201
89
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const limiter = createRateLimiter({
|
|
208
|
-
limit: 100,
|
|
209
|
-
interval: 60_000,
|
|
210
|
-
strategy: 'queue', // 'queue' | 'reject'
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
await limiter.execute(() => callApi());
|
|
214
|
-
console.log(limiter.remaining);
|
|
215
|
-
limiter.reset();
|
|
216
|
-
```
|
|
217
|
-
#### throttle — Rate-limit function calls
|
|
218
|
-
|
|
219
|
-
```ts
|
|
220
|
-
import { throttle } from 'flowx-control/throttle';
|
|
221
|
-
|
|
222
|
-
const save = throttle(saveToDb, 1000, {
|
|
223
|
-
leading: true,
|
|
224
|
-
trailing: true,
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
await save(data);
|
|
228
|
-
save.cancel();
|
|
229
|
-
```
|
|
230
|
-
#### debounce — Delay until activity pauses
|
|
231
|
-
|
|
232
|
-
```ts
|
|
233
|
-
import { debounce } from 'flowx-control/debounce';
|
|
234
|
-
|
|
235
|
-
const search = debounce(searchApi, 300, {
|
|
236
|
-
leading: false,
|
|
237
|
-
trailing: true,
|
|
238
|
-
maxWait: 1000,
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
await search(query);
|
|
242
|
-
await search.flush();
|
|
243
|
-
search.cancel();
|
|
244
|
-
```
|
|
245
|
-
#### batch — Process collections in chunks
|
|
246
|
-
|
|
247
|
-
```ts
|
|
248
|
-
import { batch } from 'flowx-control/batch';
|
|
249
|
-
|
|
250
|
-
const result = await batch(urls, async (url, i) => {
|
|
251
|
-
return fetch(url).then(r => r.json());
|
|
252
|
-
}, {
|
|
253
|
-
batchSize: 10,
|
|
254
|
-
concurrency: 3,
|
|
255
|
-
onProgress: (done, total) => console.log(`${done}/${total}`),
|
|
256
|
-
signal: controller.signal,
|
|
257
|
-
});
|
|
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
|
|
258
95
|
|
|
259
|
-
console.log(result.succeeded, result.failed, result.errors);
|
|
260
|
-
```
|
|
261
|
-
#### pipeline — Compose async operations
|
|
262
|
-
|
|
263
|
-
```ts
|
|
264
|
-
import { pipeline, pipe } from 'flowx-control/pipeline';
|
|
265
|
-
|
|
266
|
-
const transform = pipe(
|
|
267
|
-
(input: string) => input.trim(),
|
|
268
|
-
(str) => str.toUpperCase(),
|
|
269
|
-
async (str) => await translate(str),
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
const result = await transform(' hello world ');
|
|
273
|
-
```
|
|
274
96
|
### 🛠️ Utilities
|
|
275
97
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const { result, stop } = poll(() => checkJobStatus(jobId), {
|
|
282
|
-
interval: 2000,
|
|
283
|
-
until: (status) => status === 'complete',
|
|
284
|
-
maxAttempts: 30,
|
|
285
|
-
backoff: 'exponential',
|
|
286
|
-
signal: controller.signal,
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
const finalStatus = await result;
|
|
290
|
-
```
|
|
291
|
-
#### hedge — Hedged/speculative requests
|
|
292
|
-
|
|
293
|
-
```ts
|
|
294
|
-
import { hedge } from 'flowx-control/hedge';
|
|
295
|
-
|
|
296
|
-
// If primary doesn't respond in 200ms, fire a parallel request
|
|
297
|
-
const data = await hedge(() => fetch('/api'), {
|
|
298
|
-
delay: 200,
|
|
299
|
-
maxHedges: 2,
|
|
300
|
-
});
|
|
301
|
-
```
|
|
302
|
-
#### memo — Async memoization with TTL
|
|
303
|
-
|
|
304
|
-
```ts
|
|
305
|
-
import { memo } from 'flowx-control/memo';
|
|
306
|
-
|
|
307
|
-
const cachedFetch = memo(fetchUserById, {
|
|
308
|
-
ttl: 60_000,
|
|
309
|
-
maxSize: 1000,
|
|
310
|
-
key: (id) => `user:${id}`,
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
const user = await cachedFetch(123);
|
|
314
|
-
cachedFetch.clear();
|
|
315
|
-
```
|
|
316
|
-
#### deferred — Externally resolvable promise
|
|
317
|
-
|
|
318
|
-
```ts
|
|
319
|
-
import { deferred } from 'flowx-control/deferred';
|
|
320
|
-
|
|
321
|
-
const d = deferred<string>();
|
|
322
|
-
setTimeout(() => d.resolve('hello'), 1000);
|
|
323
|
-
const value = await d.promise; // 'hello'
|
|
324
|
-
```
|
|
325
|
-
---
|
|
326
|
-
|
|
327
|
-
## Deep Imports (Tree-shaking)
|
|
328
|
-
|
|
329
|
-
Import only what you need — zero unused code in your bundle:
|
|
330
|
-
|
|
331
|
-
```ts
|
|
332
|
-
// Only pulls in ~2KB instead of the full 28KB
|
|
333
|
-
import { retry } from 'flowx-control/retry';
|
|
334
|
-
import { createQueue } from 'flowx-control/queue';
|
|
335
|
-
```
|
|
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
|
|
336
102
|
|
|
337
103
|
---
|
|
338
104
|
|
|
339
105
|
## Error Hierarchy
|
|
340
106
|
|
|
341
|
-
All errors extend `FlowXError` with a machine-readable `code`:
|
|
342
|
-
|
|
343
107
|
| Error | Code | Thrown by |
|
|
344
108
|
|-------|------|----------|
|
|
345
109
|
| `TimeoutError` | `ERR_TIMEOUT` | `withTimeout` |
|
|
@@ -348,18 +112,6 @@ All errors extend `FlowXError` with a machine-readable `code`:
|
|
|
348
112
|
| `AbortError` | `ERR_ABORTED` | `poll`, `batch`, `timeout` |
|
|
349
113
|
| `RateLimitError` | `ERR_RATE_LIMIT` | `rateLimit` |
|
|
350
114
|
|
|
351
|
-
```ts
|
|
352
|
-
import { TimeoutError, FlowXError } from 'flowx-control';
|
|
353
|
-
|
|
354
|
-
try {
|
|
355
|
-
await withTimeout(fn, 1000);
|
|
356
|
-
} catch (err) {
|
|
357
|
-
if (err instanceof TimeoutError) {
|
|
358
|
-
console.log(err.code); // 'ERR_TIMEOUT'
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
```
|
|
362
|
-
|
|
363
115
|
---
|
|
364
116
|
|
|
365
117
|
## Compatibility
|
|
@@ -378,11 +130,9 @@ try {
|
|
|
378
130
|
|
|
379
131
|
```bash
|
|
380
132
|
git clone https://github.com/Avinashvelu03/flowx-control.git
|
|
381
|
-
cd flowx-control
|
|
382
|
-
npm
|
|
383
|
-
npm
|
|
384
|
-
npm run lint # ESLint
|
|
385
|
-
npm run build # Build ESM + CJS + DTS
|
|
133
|
+
cd flowx-control && npm install
|
|
134
|
+
npm test
|
|
135
|
+
npm run build
|
|
386
136
|
```
|
|
387
137
|
|
|
388
138
|
---
|
|
@@ -390,4 +140,34 @@ npm run build # Build ESM + CJS + DTS
|
|
|
390
140
|
## License
|
|
391
141
|
|
|
392
142
|
MIT © [Avinash](https://github.com/Avinashvelu03)
|
|
393
|
-
|
|
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.6",
|
|
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",
|