@async-kit/flowx 0.1.2
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 +9 -0
- package/README.md +354 -0
- package/dist/index.d.mts +124 -0
- package/dist/index.d.ts +124 -0
- package/dist/index.js +208 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +200 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +49 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="https://capsule-render.vercel.app/api?type=rect&color=gradient&customColorList=2&height=120§ion=header&text=flowx&fontSize=60&fontColor=fff&animation=fadeIn&desc=%40async-kit%2Fflowx&descAlignY=75&descAlign=50" width="100%"/>
|
|
4
|
+
|
|
5
|
+
<br/>
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@async-kit/flowx)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](../../LICENSE)
|
|
10
|
+
[](https://bundlephobia.com/package/@async-kit/flowx)
|
|
11
|
+
[](https://nodejs.org/)
|
|
12
|
+
[](#compatibility)
|
|
13
|
+
|
|
14
|
+
**Type-safe async pipeline builder — composable steps, fallbacks, per-step timeouts, and concurrency control.**
|
|
15
|
+
|
|
16
|
+
*Chain transforms, fan-out concurrently, reduce sequentially — all type-safe.*
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @async-kit/flowx
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { pipeline, parallel, sequence } from '@async-kit/flowx';
|
|
32
|
+
|
|
33
|
+
// Composable async pipeline
|
|
34
|
+
const result = await pipeline<string>()
|
|
35
|
+
.pipe(s => s.trim())
|
|
36
|
+
.pipe({ name: 'uppercase', fn: s => s.toUpperCase() })
|
|
37
|
+
.tap(s => console.log('after uppercase:', s))
|
|
38
|
+
.run(' hello world ');
|
|
39
|
+
// → 'HELLO WORLD'
|
|
40
|
+
|
|
41
|
+
// Concurrent tasks
|
|
42
|
+
const [users, products] = await parallel([
|
|
43
|
+
() => fetchUsers(),
|
|
44
|
+
() => fetchProducts(),
|
|
45
|
+
], { concurrency: 2 });
|
|
46
|
+
|
|
47
|
+
// Async reduce
|
|
48
|
+
const total = await sequence([1, 2, 3], 0, (acc, n) => acc + n);
|
|
49
|
+
// → 6
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## API
|
|
53
|
+
|
|
54
|
+
### `pipeline<T>()`
|
|
55
|
+
|
|
56
|
+
Creates a new empty pipeline starting with type `T`. Returns a `Pipeline<T, T>` builder.
|
|
57
|
+
|
|
58
|
+
### `Pipeline<TIn, TOut>`
|
|
59
|
+
|
|
60
|
+
Each method returns a **new Pipeline** — pipelines are immutable and reusable.
|
|
61
|
+
|
|
62
|
+
| Method | Returns | Description |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `.pipe(step)` | `Pipeline<TIn, TNext>` | Add a transform step |
|
|
65
|
+
| `.pipeWithFallback(step, fallback)` | `Pipeline<TIn, TNext>` | Step with an inline error fallback |
|
|
66
|
+
| `.tap(fn)` | `Pipeline<TIn, TOut>` | Side-effect step; passes value through unchanged |
|
|
67
|
+
| `.run(input, opts?)` | `Promise<TOut>` | Execute the pipeline |
|
|
68
|
+
| `.stepCount` | `number` | Number of non-fallback steps |
|
|
69
|
+
| `.toArray()` | `StepMeta[]` | Metadata array for debugging/tooling |
|
|
70
|
+
|
|
71
|
+
#### Step formats
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
// Plain function
|
|
75
|
+
.pipe(value => value.trim())
|
|
76
|
+
|
|
77
|
+
// Named step with optional per-step timeout
|
|
78
|
+
.pipe({ name: 'normalize', fn: value => value.trim(), timeoutMs: 1000 })
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### `FlowxOptions`
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
interface FlowxOptions {
|
|
85
|
+
signal?: AbortSignal; // Checked between steps
|
|
86
|
+
onStepComplete?: (index, name, result) => void;
|
|
87
|
+
stepTimeoutMs?: number; // Default per-step timeout (overridable per step)
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Step Context
|
|
92
|
+
|
|
93
|
+
Every step receives a `StepContext` as its second argument:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
interface StepContext {
|
|
97
|
+
signal: AbortSignal;
|
|
98
|
+
stepIndex: number; // 0-based
|
|
99
|
+
stepName: string | undefined;
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Fallbacks
|
|
104
|
+
|
|
105
|
+
`.pipeWithFallback` catches step errors and runs a recovery function instead of propagating:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
const result = await pipeline<string>()
|
|
109
|
+
.pipeWithFallback(
|
|
110
|
+
{ name: 'parse', fn: s => JSON.parse(s) },
|
|
111
|
+
(err, input) => ({ raw: input, error: err.cause })
|
|
112
|
+
)
|
|
113
|
+
.run('invalid json');
|
|
114
|
+
// → { raw: 'invalid json', error: SyntaxError }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## `parallel(tasks, options?)`
|
|
118
|
+
|
|
119
|
+
Run tasks concurrently, resolve in declaration order. Rejects on first failure.
|
|
120
|
+
|
|
121
|
+
| Option | Type | Default | Description |
|
|
122
|
+
|---|---|---|---|
|
|
123
|
+
| `concurrency` | `number` | unbounded | Max simultaneous tasks |
|
|
124
|
+
| `signal` | `AbortSignal` | — | Cancel pending tasks |
|
|
125
|
+
|
|
126
|
+
## `parallelSettled(tasks, options?)`
|
|
127
|
+
|
|
128
|
+
Same as `parallel` but returns `PromiseSettledResult<T>[]` — never rejects.
|
|
129
|
+
|
|
130
|
+
## `sequence(items, initial, fn, options?)`
|
|
131
|
+
|
|
132
|
+
Async left-fold — like `Array.reduce` but supports async reducers and `AbortSignal`.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
const total = await sequence(orders, 0, async (acc, order) => {
|
|
136
|
+
const price = await fetchPrice(order.id);
|
|
137
|
+
return acc + price;
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Error Types
|
|
142
|
+
|
|
143
|
+
| Class | When |
|
|
144
|
+
|---|---|
|
|
145
|
+
| `PipelineStepError` | A step threw — has `.stepIndex`, `.stepName`, `.cause`, `.inputValue` |
|
|
146
|
+
| `PipelineTimeoutError` | Per-step timeout exceeded — has `.stepIndex`, `.stepName`, `.timeoutMs` |
|
|
147
|
+
|
|
148
|
+
## Examples
|
|
149
|
+
|
|
150
|
+
### Type-safe order processing pipeline
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import { pipeline } from '@async-kit/flowx';
|
|
154
|
+
|
|
155
|
+
interface RawOrder { id: string; amount: string; userId: string }
|
|
156
|
+
interface ParsedOrder { id: string; amount: number; userId: string }
|
|
157
|
+
interface EnrichedOrder extends ParsedOrder { user: User; currency: string }
|
|
158
|
+
interface Report extends EnrichedOrder { formattedAmount: string }
|
|
159
|
+
|
|
160
|
+
const orderPipeline = pipeline<RawOrder>()
|
|
161
|
+
// Step 0 — parse
|
|
162
|
+
.pipe((raw): ParsedOrder => ({ ...raw, amount: parseFloat(raw.amount) }))
|
|
163
|
+
// Step 1 — validate
|
|
164
|
+
.pipe(async (order) => {
|
|
165
|
+
if (order.amount <= 0) throw new Error(`Invalid amount: ${order.amount}`);
|
|
166
|
+
return order;
|
|
167
|
+
})
|
|
168
|
+
// Step 2 — enrich (concurrent sub-calls)
|
|
169
|
+
.pipe(async (order): Promise<EnrichedOrder> => {
|
|
170
|
+
const [user, currency] = await Promise.all([
|
|
171
|
+
userApi.get(order.userId),
|
|
172
|
+
fxApi.getDefault(order.userId),
|
|
173
|
+
]);
|
|
174
|
+
return { ...order, user, currency };
|
|
175
|
+
})
|
|
176
|
+
// Step 3 — format
|
|
177
|
+
.pipe((order): Report => ({
|
|
178
|
+
...order,
|
|
179
|
+
formattedAmount: new Intl.NumberFormat('en-US', {
|
|
180
|
+
style: 'currency',
|
|
181
|
+
currency: order.currency,
|
|
182
|
+
}).format(order.amount),
|
|
183
|
+
}));
|
|
184
|
+
|
|
185
|
+
// Reusable — run with any input
|
|
186
|
+
const report = await orderPipeline.run(rawOrder, {
|
|
187
|
+
onStepComplete: (i, name) => metrics.track(`order.pipeline.step_${i}_${name}`),
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Fallback to cache when primary fetch fails
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import { pipeline } from '@async-kit/flowx';
|
|
195
|
+
|
|
196
|
+
const enrichedPipeline = pipeline<string>() // starts with user ID
|
|
197
|
+
.pipeWithFallback(
|
|
198
|
+
{ name: 'fetchProfile', fn: id => profileApi.get(id), timeoutMs: 2_000 },
|
|
199
|
+
(_err, id) => cache.get(`profile:${id}`) ?? { id, name: 'Unknown' }
|
|
200
|
+
)
|
|
201
|
+
.pipe({ name: 'fetchOrders', fn: profile => orderApi.list(profile.id), timeoutMs: 3_000 })
|
|
202
|
+
.pipeWithFallback(
|
|
203
|
+
{ name: 'fetchInventory', fn: orders => inventoryApi.check(orders) },
|
|
204
|
+
(_err, orders) => orders.map(o => ({ ...o, inStock: null }))
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const data = await enrichedPipeline.run(userId);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Fan-out with bounded concurrency
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import { parallel, parallelSettled } from '@async-kit/flowx';
|
|
214
|
+
|
|
215
|
+
// All succeed — throws if any fail (like Promise.all)
|
|
216
|
+
const [profile, orders, prefs] = await parallel([
|
|
217
|
+
() => userApi.getProfile(userId),
|
|
218
|
+
() => orderApi.getOrders(userId),
|
|
219
|
+
() => prefsApi.get(userId),
|
|
220
|
+
], { concurrency: 2 }); // max 2 in-flight
|
|
221
|
+
|
|
222
|
+
// Tolerate partial failures
|
|
223
|
+
const results = await parallelSettled([
|
|
224
|
+
() => analyticsApi.getStats(userId),
|
|
225
|
+
() => notificationsApi.getUnread(userId),
|
|
226
|
+
() => recsApi.get(userId),
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
for (const r of results) {
|
|
230
|
+
if (r.status === 'fulfilled') console.log(r.value);
|
|
231
|
+
else console.warn('Optional data unavailable:', r.reason);
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Async reduce with sequence — build a report
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { sequence } from '@async-kit/flowx';
|
|
239
|
+
|
|
240
|
+
const report = await sequence(
|
|
241
|
+
reportSections,
|
|
242
|
+
{ sections: [], totalMs: 0 },
|
|
243
|
+
async (acc, section) => {
|
|
244
|
+
const t0 = Date.now();
|
|
245
|
+
const rendered = await renderSection(section);
|
|
246
|
+
return {
|
|
247
|
+
sections: [...acc.sections, rendered],
|
|
248
|
+
totalMs: acc.totalMs + (Date.now() - t0),
|
|
249
|
+
};
|
|
250
|
+
},
|
|
251
|
+
{ signal: abortController.signal }
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
console.log(`Built ${report.sections.length} sections in ${report.totalMs}ms`);
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Cancellable pipeline with step-level observability
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
import { pipeline } from '@async-kit/flowx';
|
|
261
|
+
|
|
262
|
+
const controller = new AbortController();
|
|
263
|
+
|
|
264
|
+
// Cancel from the UI
|
|
265
|
+
document.getElementById('cancel')!.onclick = () => controller.abort();
|
|
266
|
+
|
|
267
|
+
const result = await pipeline<Blob>()
|
|
268
|
+
.pipe({ name: 'upload', fn: blob => uploadService.store(blob), timeoutMs: 10_000 })
|
|
269
|
+
.pipe({ name: 'transcode', fn: ref => transcodeService.run(ref), timeoutMs: 60_000 })
|
|
270
|
+
.pipe({ name: 'thumbnail', fn: ref => thumbnailService.generate(ref), timeoutMs: 5_000 })
|
|
271
|
+
.tap(ref => console.log('Asset ready:', ref))
|
|
272
|
+
.run(file, {
|
|
273
|
+
signal: controller.signal,
|
|
274
|
+
onStepComplete: (i, name, result) => {
|
|
275
|
+
console.log(`Step ${i} (${name}) done:`, result);
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Reusable pipeline — define once, run many
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
import { pipeline } from '@async-kit/flowx';
|
|
284
|
+
|
|
285
|
+
const csvPipeline = pipeline<string>()
|
|
286
|
+
.pipe(raw => raw.trim().split('\n'))
|
|
287
|
+
.pipe(lines => lines.slice(1)) // skip header
|
|
288
|
+
.pipe(lines => lines.map(l => l.split(',')))
|
|
289
|
+
.pipe(rows => rows.map(([id, name, score]) => ({
|
|
290
|
+
id: Number(id),
|
|
291
|
+
name: name.trim(),
|
|
292
|
+
score: parseFloat(score),
|
|
293
|
+
})))
|
|
294
|
+
.pipe(rows => rows.filter(r => r.score >= 0.5)); // threshold
|
|
295
|
+
|
|
296
|
+
// Run against multiple files concurrently
|
|
297
|
+
const [a, b, c] = await Promise.all([
|
|
298
|
+
csvPipeline.run(await readFile('a.csv', 'utf8')),
|
|
299
|
+
csvPipeline.run(await readFile('b.csv', 'utf8')),
|
|
300
|
+
csvPipeline.run(await readFile('c.csv', 'utf8')),
|
|
301
|
+
]);
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Tap for structured logging
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
import { pipeline } from '@async-kit/flowx';
|
|
308
|
+
|
|
309
|
+
const pipe = pipeline<Order>()
|
|
310
|
+
.pipe(validate)
|
|
311
|
+
.tap(order => logger.info('validated', { orderId: order.id }))
|
|
312
|
+
.pipe(enrich)
|
|
313
|
+
.tap(order => logger.info('enriched', { user: order.user.name }))
|
|
314
|
+
.pipe(persist)
|
|
315
|
+
.tap(saved => logger.info('persisted', { savedId: saved.id }));
|
|
316
|
+
|
|
317
|
+
await pipe.run(incomingOrder);
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Types
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
import type {
|
|
324
|
+
StepFn,
|
|
325
|
+
StepContext,
|
|
326
|
+
NamedStep,
|
|
327
|
+
FlowxOptions,
|
|
328
|
+
ParallelOptions,
|
|
329
|
+
SequenceOptions,
|
|
330
|
+
} from '@async-kit/flowx';
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Compatibility
|
|
334
|
+
|
|
335
|
+
| Environment | Support | Notes |
|
|
336
|
+
|---|---|---|
|
|
337
|
+
| **Node.js** | ≥ 18 | Recommended ≥ 24 for best performance |
|
|
338
|
+
| **Deno** | ✅ | Via npm specifier (`npm:@async-kit/flowx`) |
|
|
339
|
+
| **Bun** | ✅ | Full support |
|
|
340
|
+
| **Chrome** | ≥ 80 | ESM via bundler or native import |
|
|
341
|
+
| **Firefox** | ≥ 75 | ESM via bundler or native import |
|
|
342
|
+
| **Safari** | ≥ 13.1 | ESM via bundler or native import |
|
|
343
|
+
| **Edge** | ≥ 80 | ESM via bundler or native import |
|
|
344
|
+
| **React Native** | ✅ | Via Metro bundler |
|
|
345
|
+
| **Cloudflare Workers** | ✅ | ESM, `AbortSignal` natively supported |
|
|
346
|
+
| **Vercel Edge Runtime** | ✅ | ESM, no `process` / `fs` dependencies |
|
|
347
|
+
|
|
348
|
+
**No Node.js built-ins are used.** The package relies only on standard JavaScript (`Promise`, `setTimeout`, `clearTimeout`, `AbortSignal`, `DOMException`) — all available in any modern runtime or browser.
|
|
349
|
+
|
|
350
|
+
> `PipelineTimeoutError` uses `setTimeout` which is available everywhere. `AbortSignal` is standard since Node.js ≥ 15 and all modern browsers.
|
|
351
|
+
|
|
352
|
+
## License
|
|
353
|
+
|
|
354
|
+
MIT © async-kit contributors · Part of the [async-kit](../../README.md) ecosystem
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/** A pipeline step function: transforms `In` → `Out`, optionally async. */
|
|
2
|
+
type StepFn<In, Out> = (input: In, context: StepContext) => Promise<Out> | Out;
|
|
3
|
+
/** Context injected into every pipeline step. */
|
|
4
|
+
interface StepContext {
|
|
5
|
+
/** AbortSignal — passed into every step for fine-grained cancellation. */
|
|
6
|
+
signal: AbortSignal;
|
|
7
|
+
/** 0-based index of the current step. */
|
|
8
|
+
stepIndex: number;
|
|
9
|
+
/** Name of the step, if provided. */
|
|
10
|
+
stepName: string | undefined;
|
|
11
|
+
}
|
|
12
|
+
/** A named step with an optional per-step timeout override. */
|
|
13
|
+
interface NamedStep<In, Out> {
|
|
14
|
+
name: string;
|
|
15
|
+
fn: StepFn<In, Out>;
|
|
16
|
+
/** Per-step timeout in ms. Overrides `FlowxOptions.stepTimeoutMs`. */
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
/** Options passed to `Pipeline.run()`. */
|
|
20
|
+
interface FlowxOptions {
|
|
21
|
+
/** AbortSignal — checked between steps and passed into each step's context. */
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
/** Called after each step completes successfully. */
|
|
24
|
+
onStepComplete?: (stepIndex: number, stepName: string | undefined, result: unknown) => void;
|
|
25
|
+
/** Default timeout per step in ms. Can be overridden per step in `NamedStep`. */
|
|
26
|
+
stepTimeoutMs?: number;
|
|
27
|
+
}
|
|
28
|
+
/** Options for `parallel()` and `parallelSettled()`. */
|
|
29
|
+
interface ParallelOptions {
|
|
30
|
+
/** Limit simultaneous tasks. Undefined = unbounded. */
|
|
31
|
+
concurrency?: number;
|
|
32
|
+
/** AbortSignal — cancels pending tasks when aborted. */
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
/** Called as each task settles. */
|
|
35
|
+
onSettle?: (index: number, result: PromiseSettledResult<unknown>) => void;
|
|
36
|
+
}
|
|
37
|
+
/** Options for `sequence()`. */
|
|
38
|
+
interface SequenceOptions {
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare class PipelineStepError extends Error {
|
|
43
|
+
readonly stepIndex: number;
|
|
44
|
+
readonly stepName: string | undefined;
|
|
45
|
+
readonly cause: unknown;
|
|
46
|
+
readonly inputValue: unknown;
|
|
47
|
+
constructor(stepIndex: number, stepName: string | undefined, cause: unknown, inputValue: unknown);
|
|
48
|
+
}
|
|
49
|
+
declare class PipelineTimeoutError extends Error {
|
|
50
|
+
readonly stepIndex: number;
|
|
51
|
+
readonly stepName: string | undefined;
|
|
52
|
+
readonly timeoutMs: number;
|
|
53
|
+
constructor(stepIndex: number, stepName: string | undefined, timeoutMs: number);
|
|
54
|
+
}
|
|
55
|
+
interface StepRecord {
|
|
56
|
+
fn: StepFn<unknown, unknown>;
|
|
57
|
+
name: string | undefined;
|
|
58
|
+
timeoutMs: number | undefined;
|
|
59
|
+
isFallback: false;
|
|
60
|
+
}
|
|
61
|
+
interface FallbackRecord {
|
|
62
|
+
fn: (error: PipelineStepError, input: unknown) => unknown | Promise<unknown>;
|
|
63
|
+
isFallback: true;
|
|
64
|
+
}
|
|
65
|
+
type AnyRecord = StepRecord | FallbackRecord;
|
|
66
|
+
/**
|
|
67
|
+
* Flowx — composable, type-safe async pipeline builder.
|
|
68
|
+
*
|
|
69
|
+
* Each step receives the output of the previous step as its first argument,
|
|
70
|
+
* plus a `StepContext` carrying the abort signal, step index, and step name.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* const result = await pipeline<string>()
|
|
74
|
+
* .pipe(s => s.trim())
|
|
75
|
+
* .pipe({ name: 'uppercase', fn: s => s.toUpperCase() })
|
|
76
|
+
* .tap(s => console.log('after uppercase:', s))
|
|
77
|
+
* .run(' hello ');
|
|
78
|
+
*/
|
|
79
|
+
declare class Pipeline<TIn, TOut> {
|
|
80
|
+
private readonly records;
|
|
81
|
+
constructor(records?: AnyRecord[]);
|
|
82
|
+
/**
|
|
83
|
+
* Add a transform step. Accepts either a plain function or a `NamedStep`
|
|
84
|
+
* object with an optional per-step `timeoutMs`.
|
|
85
|
+
*/
|
|
86
|
+
pipe<TNext>(step: StepFn<TOut, TNext> | NamedStep<TOut, TNext>): Pipeline<TIn, TNext>;
|
|
87
|
+
/**
|
|
88
|
+
* Add a step with an inline fallback. If the step throws, `fallback` is
|
|
89
|
+
* called with the error and the original input to that step.
|
|
90
|
+
*/
|
|
91
|
+
pipeWithFallback<TNext>(step: StepFn<TOut, TNext> | NamedStep<TOut, TNext>, fallback: (error: PipelineStepError, input: TOut) => TNext | Promise<TNext>): Pipeline<TIn, TNext>;
|
|
92
|
+
/**
|
|
93
|
+
* Add a side-effect step. Runs `fn` for its effect only; passes the current
|
|
94
|
+
* value through unchanged. Errors in `fn` propagate normally.
|
|
95
|
+
*/
|
|
96
|
+
tap(fn: (value: TOut, context: StepContext) => void | Promise<void>): Pipeline<TIn, TOut>;
|
|
97
|
+
run(input: TIn, options?: FlowxOptions): Promise<TOut>;
|
|
98
|
+
get stepCount(): number;
|
|
99
|
+
/** Returns step metadata for debugging/tooling. */
|
|
100
|
+
toArray(): Array<{
|
|
101
|
+
index: number;
|
|
102
|
+
name: string | undefined;
|
|
103
|
+
timeoutMs: number | undefined;
|
|
104
|
+
}>;
|
|
105
|
+
}
|
|
106
|
+
/** Create a new empty pipeline starting with type `T`. */
|
|
107
|
+
declare function pipeline<T>(): Pipeline<T, T>;
|
|
108
|
+
/**
|
|
109
|
+
* Run tasks concurrently, return results in declaration order.
|
|
110
|
+
* Rejects if any task throws (like `Promise.all`).
|
|
111
|
+
*/
|
|
112
|
+
declare function parallel<T>(tasks: Array<() => Promise<T>>, options?: ParallelOptions): Promise<T[]>;
|
|
113
|
+
/**
|
|
114
|
+
* Run tasks concurrently and return all results regardless of success/failure.
|
|
115
|
+
* Equivalent to `Promise.allSettled` but with optional concurrency control.
|
|
116
|
+
*/
|
|
117
|
+
declare function parallelSettled<T>(tasks: Array<() => Promise<T>>, options?: Pick<ParallelOptions, 'concurrency' | 'signal'>): Promise<PromiseSettledResult<T>[]>;
|
|
118
|
+
/**
|
|
119
|
+
* Async left-fold over an array. Like `Array.reduce` but supports async reducers
|
|
120
|
+
* and can be cancelled via `AbortSignal`.
|
|
121
|
+
*/
|
|
122
|
+
declare function sequence<T, A>(items: T[], initial: A, fn: (acc: A, item: T, index: number) => Promise<A> | A, options?: SequenceOptions): Promise<A>;
|
|
123
|
+
|
|
124
|
+
export { type FlowxOptions, type NamedStep, type ParallelOptions, Pipeline, PipelineStepError, PipelineTimeoutError, type SequenceOptions, type StepContext, type StepFn, parallel, parallelSettled, pipeline, sequence };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/** A pipeline step function: transforms `In` → `Out`, optionally async. */
|
|
2
|
+
type StepFn<In, Out> = (input: In, context: StepContext) => Promise<Out> | Out;
|
|
3
|
+
/** Context injected into every pipeline step. */
|
|
4
|
+
interface StepContext {
|
|
5
|
+
/** AbortSignal — passed into every step for fine-grained cancellation. */
|
|
6
|
+
signal: AbortSignal;
|
|
7
|
+
/** 0-based index of the current step. */
|
|
8
|
+
stepIndex: number;
|
|
9
|
+
/** Name of the step, if provided. */
|
|
10
|
+
stepName: string | undefined;
|
|
11
|
+
}
|
|
12
|
+
/** A named step with an optional per-step timeout override. */
|
|
13
|
+
interface NamedStep<In, Out> {
|
|
14
|
+
name: string;
|
|
15
|
+
fn: StepFn<In, Out>;
|
|
16
|
+
/** Per-step timeout in ms. Overrides `FlowxOptions.stepTimeoutMs`. */
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
/** Options passed to `Pipeline.run()`. */
|
|
20
|
+
interface FlowxOptions {
|
|
21
|
+
/** AbortSignal — checked between steps and passed into each step's context. */
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
/** Called after each step completes successfully. */
|
|
24
|
+
onStepComplete?: (stepIndex: number, stepName: string | undefined, result: unknown) => void;
|
|
25
|
+
/** Default timeout per step in ms. Can be overridden per step in `NamedStep`. */
|
|
26
|
+
stepTimeoutMs?: number;
|
|
27
|
+
}
|
|
28
|
+
/** Options for `parallel()` and `parallelSettled()`. */
|
|
29
|
+
interface ParallelOptions {
|
|
30
|
+
/** Limit simultaneous tasks. Undefined = unbounded. */
|
|
31
|
+
concurrency?: number;
|
|
32
|
+
/** AbortSignal — cancels pending tasks when aborted. */
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
/** Called as each task settles. */
|
|
35
|
+
onSettle?: (index: number, result: PromiseSettledResult<unknown>) => void;
|
|
36
|
+
}
|
|
37
|
+
/** Options for `sequence()`. */
|
|
38
|
+
interface SequenceOptions {
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare class PipelineStepError extends Error {
|
|
43
|
+
readonly stepIndex: number;
|
|
44
|
+
readonly stepName: string | undefined;
|
|
45
|
+
readonly cause: unknown;
|
|
46
|
+
readonly inputValue: unknown;
|
|
47
|
+
constructor(stepIndex: number, stepName: string | undefined, cause: unknown, inputValue: unknown);
|
|
48
|
+
}
|
|
49
|
+
declare class PipelineTimeoutError extends Error {
|
|
50
|
+
readonly stepIndex: number;
|
|
51
|
+
readonly stepName: string | undefined;
|
|
52
|
+
readonly timeoutMs: number;
|
|
53
|
+
constructor(stepIndex: number, stepName: string | undefined, timeoutMs: number);
|
|
54
|
+
}
|
|
55
|
+
interface StepRecord {
|
|
56
|
+
fn: StepFn<unknown, unknown>;
|
|
57
|
+
name: string | undefined;
|
|
58
|
+
timeoutMs: number | undefined;
|
|
59
|
+
isFallback: false;
|
|
60
|
+
}
|
|
61
|
+
interface FallbackRecord {
|
|
62
|
+
fn: (error: PipelineStepError, input: unknown) => unknown | Promise<unknown>;
|
|
63
|
+
isFallback: true;
|
|
64
|
+
}
|
|
65
|
+
type AnyRecord = StepRecord | FallbackRecord;
|
|
66
|
+
/**
|
|
67
|
+
* Flowx — composable, type-safe async pipeline builder.
|
|
68
|
+
*
|
|
69
|
+
* Each step receives the output of the previous step as its first argument,
|
|
70
|
+
* plus a `StepContext` carrying the abort signal, step index, and step name.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* const result = await pipeline<string>()
|
|
74
|
+
* .pipe(s => s.trim())
|
|
75
|
+
* .pipe({ name: 'uppercase', fn: s => s.toUpperCase() })
|
|
76
|
+
* .tap(s => console.log('after uppercase:', s))
|
|
77
|
+
* .run(' hello ');
|
|
78
|
+
*/
|
|
79
|
+
declare class Pipeline<TIn, TOut> {
|
|
80
|
+
private readonly records;
|
|
81
|
+
constructor(records?: AnyRecord[]);
|
|
82
|
+
/**
|
|
83
|
+
* Add a transform step. Accepts either a plain function or a `NamedStep`
|
|
84
|
+
* object with an optional per-step `timeoutMs`.
|
|
85
|
+
*/
|
|
86
|
+
pipe<TNext>(step: StepFn<TOut, TNext> | NamedStep<TOut, TNext>): Pipeline<TIn, TNext>;
|
|
87
|
+
/**
|
|
88
|
+
* Add a step with an inline fallback. If the step throws, `fallback` is
|
|
89
|
+
* called with the error and the original input to that step.
|
|
90
|
+
*/
|
|
91
|
+
pipeWithFallback<TNext>(step: StepFn<TOut, TNext> | NamedStep<TOut, TNext>, fallback: (error: PipelineStepError, input: TOut) => TNext | Promise<TNext>): Pipeline<TIn, TNext>;
|
|
92
|
+
/**
|
|
93
|
+
* Add a side-effect step. Runs `fn` for its effect only; passes the current
|
|
94
|
+
* value through unchanged. Errors in `fn` propagate normally.
|
|
95
|
+
*/
|
|
96
|
+
tap(fn: (value: TOut, context: StepContext) => void | Promise<void>): Pipeline<TIn, TOut>;
|
|
97
|
+
run(input: TIn, options?: FlowxOptions): Promise<TOut>;
|
|
98
|
+
get stepCount(): number;
|
|
99
|
+
/** Returns step metadata for debugging/tooling. */
|
|
100
|
+
toArray(): Array<{
|
|
101
|
+
index: number;
|
|
102
|
+
name: string | undefined;
|
|
103
|
+
timeoutMs: number | undefined;
|
|
104
|
+
}>;
|
|
105
|
+
}
|
|
106
|
+
/** Create a new empty pipeline starting with type `T`. */
|
|
107
|
+
declare function pipeline<T>(): Pipeline<T, T>;
|
|
108
|
+
/**
|
|
109
|
+
* Run tasks concurrently, return results in declaration order.
|
|
110
|
+
* Rejects if any task throws (like `Promise.all`).
|
|
111
|
+
*/
|
|
112
|
+
declare function parallel<T>(tasks: Array<() => Promise<T>>, options?: ParallelOptions): Promise<T[]>;
|
|
113
|
+
/**
|
|
114
|
+
* Run tasks concurrently and return all results regardless of success/failure.
|
|
115
|
+
* Equivalent to `Promise.allSettled` but with optional concurrency control.
|
|
116
|
+
*/
|
|
117
|
+
declare function parallelSettled<T>(tasks: Array<() => Promise<T>>, options?: Pick<ParallelOptions, 'concurrency' | 'signal'>): Promise<PromiseSettledResult<T>[]>;
|
|
118
|
+
/**
|
|
119
|
+
* Async left-fold over an array. Like `Array.reduce` but supports async reducers
|
|
120
|
+
* and can be cancelled via `AbortSignal`.
|
|
121
|
+
*/
|
|
122
|
+
declare function sequence<T, A>(items: T[], initial: A, fn: (acc: A, item: T, index: number) => Promise<A> | A, options?: SequenceOptions): Promise<A>;
|
|
123
|
+
|
|
124
|
+
export { type FlowxOptions, type NamedStep, type ParallelOptions, Pipeline, PipelineStepError, PipelineTimeoutError, type SequenceOptions, type StepContext, type StepFn, parallel, parallelSettled, pipeline, sequence };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/flowx.ts
|
|
4
|
+
var PipelineStepError = class extends Error {
|
|
5
|
+
constructor(stepIndex, stepName, cause, inputValue) {
|
|
6
|
+
super(
|
|
7
|
+
`Pipeline failed at step ${stepIndex}${stepName ? ` (${stepName})` : ""}: ${String(cause)}`
|
|
8
|
+
);
|
|
9
|
+
this.stepIndex = stepIndex;
|
|
10
|
+
this.stepName = stepName;
|
|
11
|
+
this.cause = cause;
|
|
12
|
+
this.inputValue = inputValue;
|
|
13
|
+
this.name = "PipelineStepError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var PipelineTimeoutError = class extends Error {
|
|
17
|
+
constructor(stepIndex, stepName, timeoutMs) {
|
|
18
|
+
super(`Step ${stepIndex}${stepName ? ` (${stepName})` : ""} timed out after ${timeoutMs}ms`);
|
|
19
|
+
this.stepIndex = stepIndex;
|
|
20
|
+
this.stepName = stepName;
|
|
21
|
+
this.timeoutMs = timeoutMs;
|
|
22
|
+
this.name = "PipelineTimeoutError";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var neverAborted = new AbortController().signal;
|
|
26
|
+
function raceStepTimeout(promise, ms, index, name) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const timer = setTimeout(() => reject(new PipelineTimeoutError(index, name, ms)), ms);
|
|
29
|
+
promise.then(
|
|
30
|
+
(v) => {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
resolve(v);
|
|
33
|
+
},
|
|
34
|
+
(e) => {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
reject(e);
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function normalizeStep(step) {
|
|
42
|
+
if (typeof step === "function") {
|
|
43
|
+
return { fn: step, name: void 0, timeoutMs: void 0 };
|
|
44
|
+
}
|
|
45
|
+
return { fn: step.fn, name: step.name, timeoutMs: step.timeoutMs };
|
|
46
|
+
}
|
|
47
|
+
var Pipeline = class _Pipeline {
|
|
48
|
+
records;
|
|
49
|
+
constructor(records = []) {
|
|
50
|
+
this.records = records;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Add a transform step. Accepts either a plain function or a `NamedStep`
|
|
54
|
+
* object with an optional per-step `timeoutMs`.
|
|
55
|
+
*/
|
|
56
|
+
pipe(step) {
|
|
57
|
+
const { fn, name, timeoutMs } = normalizeStep(step);
|
|
58
|
+
const record = {
|
|
59
|
+
fn,
|
|
60
|
+
name,
|
|
61
|
+
timeoutMs,
|
|
62
|
+
isFallback: false
|
|
63
|
+
};
|
|
64
|
+
return new _Pipeline([...this.records, record]);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Add a step with an inline fallback. If the step throws, `fallback` is
|
|
68
|
+
* called with the error and the original input to that step.
|
|
69
|
+
*/
|
|
70
|
+
pipeWithFallback(step, fallback) {
|
|
71
|
+
const { fn, name, timeoutMs } = normalizeStep(step);
|
|
72
|
+
const stepRecord = {
|
|
73
|
+
fn,
|
|
74
|
+
name,
|
|
75
|
+
timeoutMs,
|
|
76
|
+
isFallback: false
|
|
77
|
+
};
|
|
78
|
+
const fallbackRecord = {
|
|
79
|
+
fn: fallback,
|
|
80
|
+
isFallback: true
|
|
81
|
+
};
|
|
82
|
+
return new _Pipeline([...this.records, stepRecord, fallbackRecord]);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Add a side-effect step. Runs `fn` for its effect only; passes the current
|
|
86
|
+
* value through unchanged. Errors in `fn` propagate normally.
|
|
87
|
+
*/
|
|
88
|
+
tap(fn) {
|
|
89
|
+
const tapStep = async (value, ctx) => {
|
|
90
|
+
await fn(value, ctx);
|
|
91
|
+
return value;
|
|
92
|
+
};
|
|
93
|
+
return this.pipe(tapStep);
|
|
94
|
+
}
|
|
95
|
+
async run(input, options = {}) {
|
|
96
|
+
const signal = options.signal ?? neverAborted;
|
|
97
|
+
const { onStepComplete, stepTimeoutMs } = options;
|
|
98
|
+
let current = input;
|
|
99
|
+
let stepIndex = 0;
|
|
100
|
+
for (let i = 0; i < this.records.length; i++) {
|
|
101
|
+
const record = this.records[i];
|
|
102
|
+
if (record.isFallback) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (signal.aborted) {
|
|
106
|
+
throw new DOMException("Pipeline aborted", "AbortError");
|
|
107
|
+
}
|
|
108
|
+
const inputValue = current;
|
|
109
|
+
const ctx = { signal, stepIndex, stepName: record.name };
|
|
110
|
+
const effectiveTimeout = record.timeoutMs ?? stepTimeoutMs;
|
|
111
|
+
try {
|
|
112
|
+
const rawResult = record.fn(current, ctx);
|
|
113
|
+
let stepPromise = rawResult instanceof Promise ? rawResult : Promise.resolve(rawResult);
|
|
114
|
+
if (effectiveTimeout != null) {
|
|
115
|
+
stepPromise = raceStepTimeout(stepPromise, effectiveTimeout, stepIndex, record.name);
|
|
116
|
+
}
|
|
117
|
+
current = await stepPromise;
|
|
118
|
+
onStepComplete?.(stepIndex, record.name, current);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
const nextRecord = this.records[i + 1];
|
|
121
|
+
if (nextRecord?.isFallback) {
|
|
122
|
+
const pipelineErr = new PipelineStepError(stepIndex, record.name, err, inputValue);
|
|
123
|
+
current = await nextRecord.fn(pipelineErr, inputValue);
|
|
124
|
+
i++;
|
|
125
|
+
onStepComplete?.(stepIndex, record.name, current);
|
|
126
|
+
} else {
|
|
127
|
+
throw new PipelineStepError(stepIndex, record.name, err, inputValue);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
stepIndex++;
|
|
131
|
+
}
|
|
132
|
+
return current;
|
|
133
|
+
}
|
|
134
|
+
get stepCount() {
|
|
135
|
+
return this.records.filter((r) => !r.isFallback).length;
|
|
136
|
+
}
|
|
137
|
+
/** Returns step metadata for debugging/tooling. */
|
|
138
|
+
toArray() {
|
|
139
|
+
let idx = 0;
|
|
140
|
+
const result = [];
|
|
141
|
+
for (const r of this.records) {
|
|
142
|
+
if (!r.isFallback) {
|
|
143
|
+
result.push({ index: idx++, name: r.name, timeoutMs: r.timeoutMs });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
function pipeline() {
|
|
150
|
+
return new Pipeline();
|
|
151
|
+
}
|
|
152
|
+
async function parallel(tasks, options = {}) {
|
|
153
|
+
const { concurrency, signal } = options;
|
|
154
|
+
if (concurrency == null || concurrency >= tasks.length) {
|
|
155
|
+
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
156
|
+
return Promise.all(tasks.map((t) => t()));
|
|
157
|
+
}
|
|
158
|
+
const results = new Array(tasks.length);
|
|
159
|
+
let nextIndex = 0;
|
|
160
|
+
let hasError = false;
|
|
161
|
+
let firstError;
|
|
162
|
+
const run = async () => {
|
|
163
|
+
while (nextIndex < tasks.length) {
|
|
164
|
+
if (signal?.aborted || hasError) break;
|
|
165
|
+
const idx = nextIndex++;
|
|
166
|
+
try {
|
|
167
|
+
results[idx] = await tasks[idx]();
|
|
168
|
+
} catch (err) {
|
|
169
|
+
hasError = true;
|
|
170
|
+
firstError = err;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, run);
|
|
176
|
+
await Promise.all(workers);
|
|
177
|
+
if (hasError) throw firstError;
|
|
178
|
+
return results;
|
|
179
|
+
}
|
|
180
|
+
async function parallelSettled(tasks, options = {}) {
|
|
181
|
+
const { concurrency, signal } = options;
|
|
182
|
+
const wrapped = tasks.map(
|
|
183
|
+
(t) => () => t().then(
|
|
184
|
+
(value) => ({ status: "fulfilled", value }),
|
|
185
|
+
(reason) => ({ status: "rejected", reason })
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
return parallel(wrapped, { concurrency, signal });
|
|
189
|
+
}
|
|
190
|
+
async function sequence(items, initial, fn, options = {}) {
|
|
191
|
+
const { signal } = options;
|
|
192
|
+
let acc = initial;
|
|
193
|
+
for (let i = 0; i < items.length; i++) {
|
|
194
|
+
if (signal?.aborted) throw new DOMException("Sequence aborted", "AbortError");
|
|
195
|
+
acc = await fn(acc, items[i], i);
|
|
196
|
+
}
|
|
197
|
+
return acc;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
exports.Pipeline = Pipeline;
|
|
201
|
+
exports.PipelineStepError = PipelineStepError;
|
|
202
|
+
exports.PipelineTimeoutError = PipelineTimeoutError;
|
|
203
|
+
exports.parallel = parallel;
|
|
204
|
+
exports.parallelSettled = parallelSettled;
|
|
205
|
+
exports.pipeline = pipeline;
|
|
206
|
+
exports.sequence = sequence;
|
|
207
|
+
//# sourceMappingURL=index.js.map
|
|
208
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/flowx.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["export type { StepFn, StepContext, NamedStep, FlowxOptions, ParallelOptions, SequenceOptions } from './types.js';\nimport type { StepFn, StepContext, NamedStep, FlowxOptions, ParallelOptions, SequenceOptions } from './types.js';\n\n// ─── Errors ──────────────────────────────────────────────────────────────────\n\nexport class PipelineStepError extends Error {\n constructor(\n public readonly stepIndex: number,\n public readonly stepName: string | undefined,\n public override readonly cause: unknown,\n public readonly inputValue: unknown\n ) {\n super(\n `Pipeline failed at step ${stepIndex}${stepName ? ` (${stepName})` : ''}: ${String(cause)}`\n );\n this.name = 'PipelineStepError';\n }\n}\n\nexport class PipelineTimeoutError extends Error {\n constructor(\n public readonly stepIndex: number,\n public readonly stepName: string | undefined,\n public readonly timeoutMs: number\n ) {\n super(`Step ${stepIndex}${stepName ? ` (${stepName})` : ''} timed out after ${timeoutMs}ms`);\n this.name = 'PipelineTimeoutError';\n }\n}\n\n// ─── Internal Step Record ────────────────────────────────────────────────────\n\ninterface StepRecord {\n fn: StepFn<unknown, unknown>;\n name: string | undefined;\n timeoutMs: number | undefined;\n isFallback: false;\n}\n\ninterface FallbackRecord {\n fn: (error: PipelineStepError, input: unknown) => unknown | Promise<unknown>;\n isFallback: true;\n}\n\ntype AnyRecord = StepRecord | FallbackRecord;\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nconst neverAborted = new AbortController().signal;\n\nfunction raceStepTimeout<T>(\n promise: Promise<T>,\n ms: number,\n index: number,\n name: string | undefined\n): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => reject(new PipelineTimeoutError(index, name, ms)), ms);\n promise.then(\n (v) => { clearTimeout(timer); resolve(v); },\n (e) => { clearTimeout(timer); reject(e); }\n );\n });\n}\n\nfunction normalizeStep<In, Out>(\n step: StepFn<In, Out> | NamedStep<In, Out>\n): { fn: StepFn<In, Out>; name: string | undefined; timeoutMs: number | undefined } {\n if (typeof step === 'function') {\n return { fn: step, name: undefined, timeoutMs: undefined };\n }\n return { fn: step.fn, name: step.name, timeoutMs: step.timeoutMs };\n}\n\n// ─── Pipeline ────────────────────────────────────────────────────────────────\n\n/**\n * Flowx — composable, type-safe async pipeline builder.\n *\n * Each step receives the output of the previous step as its first argument,\n * plus a `StepContext` carrying the abort signal, step index, and step name.\n *\n * @example\n * const result = await pipeline<string>()\n * .pipe(s => s.trim())\n * .pipe({ name: 'uppercase', fn: s => s.toUpperCase() })\n * .tap(s => console.log('after uppercase:', s))\n * .run(' hello ');\n */\nexport class Pipeline<TIn, TOut> {\n private readonly records: AnyRecord[];\n\n constructor(records: AnyRecord[] = []) {\n this.records = records;\n }\n\n /**\n * Add a transform step. Accepts either a plain function or a `NamedStep`\n * object with an optional per-step `timeoutMs`.\n */\n pipe<TNext>(\n step: StepFn<TOut, TNext> | NamedStep<TOut, TNext>\n ): Pipeline<TIn, TNext> {\n const { fn, name, timeoutMs } = normalizeStep(step);\n const record: StepRecord = {\n fn: fn as StepFn<unknown, unknown>,\n name,\n timeoutMs,\n isFallback: false,\n };\n return new Pipeline<TIn, TNext>([...this.records, record]);\n }\n\n /**\n * Add a step with an inline fallback. If the step throws, `fallback` is\n * called with the error and the original input to that step.\n */\n pipeWithFallback<TNext>(\n step: StepFn<TOut, TNext> | NamedStep<TOut, TNext>,\n fallback: (error: PipelineStepError, input: TOut) => TNext | Promise<TNext>\n ): Pipeline<TIn, TNext> {\n const { fn, name, timeoutMs } = normalizeStep(step);\n const stepRecord: StepRecord = {\n fn: fn as StepFn<unknown, unknown>,\n name,\n timeoutMs,\n isFallback: false,\n };\n const fallbackRecord: FallbackRecord = {\n fn: fallback as (error: PipelineStepError, input: unknown) => unknown,\n isFallback: true,\n };\n return new Pipeline<TIn, TNext>([...this.records, stepRecord, fallbackRecord]);\n }\n\n /**\n * Add a side-effect step. Runs `fn` for its effect only; passes the current\n * value through unchanged. Errors in `fn` propagate normally.\n */\n tap(fn: (value: TOut, context: StepContext) => void | Promise<void>): Pipeline<TIn, TOut> {\n const tapStep: StepFn<TOut, TOut> = async (value, ctx) => {\n await fn(value, ctx);\n return value;\n };\n return this.pipe(tapStep);\n }\n\n async run(input: TIn, options: FlowxOptions = {}): Promise<TOut> {\n const signal = options.signal ?? neverAborted;\n const { onStepComplete, stepTimeoutMs } = options;\n\n let current: unknown = input;\n let stepIndex = 0;\n\n for (let i = 0; i < this.records.length; i++) {\n const record = this.records[i];\n\n if (record.isFallback) {\n // Fallback records are consumed inline by the preceding step handler.\n continue;\n }\n\n if (signal.aborted) {\n throw new DOMException('Pipeline aborted', 'AbortError');\n }\n\n const inputValue = current;\n const ctx: StepContext = { signal, stepIndex, stepName: record.name };\n const effectiveTimeout = record.timeoutMs ?? stepTimeoutMs;\n\n try {\n const rawResult = record.fn(current, ctx);\n let stepPromise: Promise<unknown> = rawResult instanceof Promise ? rawResult : Promise.resolve(rawResult);\n if (effectiveTimeout != null) {\n stepPromise = raceStepTimeout(stepPromise, effectiveTimeout, stepIndex, record.name);\n }\n current = await stepPromise;\n onStepComplete?.(stepIndex, record.name, current);\n } catch (err) {\n // Check if the next record is a fallback for this step.\n const nextRecord = this.records[i + 1];\n if (nextRecord?.isFallback) {\n const pipelineErr = new PipelineStepError(stepIndex, record.name, err, inputValue);\n current = await nextRecord.fn(pipelineErr, inputValue);\n i++; // skip the fallback record in the outer loop\n onStepComplete?.(stepIndex, record.name, current);\n } else {\n throw new PipelineStepError(stepIndex, record.name, err, inputValue);\n }\n }\n\n stepIndex++;\n }\n\n return current as TOut;\n }\n\n get stepCount(): number {\n return this.records.filter((r) => !r.isFallback).length;\n }\n\n /** Returns step metadata for debugging/tooling. */\n toArray(): Array<{ index: number; name: string | undefined; timeoutMs: number | undefined }> {\n let idx = 0;\n const result: Array<{ index: number; name: string | undefined; timeoutMs: number | undefined }> = [];\n for (const r of this.records) {\n if (!r.isFallback) {\n result.push({ index: idx++, name: r.name, timeoutMs: r.timeoutMs });\n }\n }\n return result;\n }\n}\n\n// ─── Factory ─────────────────────────────────────────────────────────────────\n\n/** Create a new empty pipeline starting with type `T`. */\nexport function pipeline<T>(): Pipeline<T, T> {\n return new Pipeline<T, T>();\n}\n\n// ─── parallel ────────────────────────────────────────────────────────────────\n\n/**\n * Run tasks concurrently, return results in declaration order.\n * Rejects if any task throws (like `Promise.all`).\n */\nexport async function parallel<T>(\n tasks: Array<() => Promise<T>>,\n options: ParallelOptions = {}\n): Promise<T[]> {\n const { concurrency, signal } = options;\n\n if (concurrency == null || concurrency >= tasks.length) {\n // Unbounded — just Promise.all\n if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');\n return Promise.all(tasks.map((t) => t()));\n }\n\n // Bounded concurrency via semaphore\n const results: T[] = new Array(tasks.length);\n let nextIndex = 0;\n let hasError = false;\n let firstError: unknown;\n\n const run = async (): Promise<void> => {\n while (nextIndex < tasks.length) {\n if (signal?.aborted || hasError) break;\n const idx = nextIndex++;\n try {\n results[idx] = await tasks[idx]();\n } catch (err) {\n hasError = true;\n firstError = err;\n break;\n }\n }\n };\n\n const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, run);\n await Promise.all(workers);\n\n if (hasError) throw firstError;\n return results;\n}\n\n/**\n * Run tasks concurrently and return all results regardless of success/failure.\n * Equivalent to `Promise.allSettled` but with optional concurrency control.\n */\nexport async function parallelSettled<T>(\n tasks: Array<() => Promise<T>>,\n options: Pick<ParallelOptions, 'concurrency' | 'signal'> = {}\n): Promise<PromiseSettledResult<T>[]> {\n const { concurrency, signal } = options;\n const wrapped = tasks.map((t) => (): Promise<PromiseSettledResult<T>> =>\n t().then(\n (value) => ({ status: 'fulfilled' as const, value }),\n (reason) => ({ status: 'rejected' as const, reason })\n )\n );\n\n return parallel(wrapped, { concurrency, signal }) as Promise<PromiseSettledResult<T>[]>;\n}\n\n// ─── sequence ────────────────────────────────────────────────────────────────\n\n/**\n * Async left-fold over an array. Like `Array.reduce` but supports async reducers\n * and can be cancelled via `AbortSignal`.\n */\nexport async function sequence<T, A>(\n items: T[],\n initial: A,\n fn: (acc: A, item: T, index: number) => Promise<A> | A,\n options: SequenceOptions = {}\n): Promise<A> {\n const { signal } = options;\n let acc = initial;\n for (let i = 0; i < items.length; i++) {\n if (signal?.aborted) throw new DOMException('Sequence aborted', 'AbortError');\n acc = await fn(acc, items[i], i);\n }\n return acc;\n}\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// src/flowx.ts
|
|
2
|
+
var PipelineStepError = class extends Error {
|
|
3
|
+
constructor(stepIndex, stepName, cause, inputValue) {
|
|
4
|
+
super(
|
|
5
|
+
`Pipeline failed at step ${stepIndex}${stepName ? ` (${stepName})` : ""}: ${String(cause)}`
|
|
6
|
+
);
|
|
7
|
+
this.stepIndex = stepIndex;
|
|
8
|
+
this.stepName = stepName;
|
|
9
|
+
this.cause = cause;
|
|
10
|
+
this.inputValue = inputValue;
|
|
11
|
+
this.name = "PipelineStepError";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var PipelineTimeoutError = class extends Error {
|
|
15
|
+
constructor(stepIndex, stepName, timeoutMs) {
|
|
16
|
+
super(`Step ${stepIndex}${stepName ? ` (${stepName})` : ""} timed out after ${timeoutMs}ms`);
|
|
17
|
+
this.stepIndex = stepIndex;
|
|
18
|
+
this.stepName = stepName;
|
|
19
|
+
this.timeoutMs = timeoutMs;
|
|
20
|
+
this.name = "PipelineTimeoutError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var neverAborted = new AbortController().signal;
|
|
24
|
+
function raceStepTimeout(promise, ms, index, name) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const timer = setTimeout(() => reject(new PipelineTimeoutError(index, name, ms)), ms);
|
|
27
|
+
promise.then(
|
|
28
|
+
(v) => {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
resolve(v);
|
|
31
|
+
},
|
|
32
|
+
(e) => {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
reject(e);
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function normalizeStep(step) {
|
|
40
|
+
if (typeof step === "function") {
|
|
41
|
+
return { fn: step, name: void 0, timeoutMs: void 0 };
|
|
42
|
+
}
|
|
43
|
+
return { fn: step.fn, name: step.name, timeoutMs: step.timeoutMs };
|
|
44
|
+
}
|
|
45
|
+
var Pipeline = class _Pipeline {
|
|
46
|
+
records;
|
|
47
|
+
constructor(records = []) {
|
|
48
|
+
this.records = records;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Add a transform step. Accepts either a plain function or a `NamedStep`
|
|
52
|
+
* object with an optional per-step `timeoutMs`.
|
|
53
|
+
*/
|
|
54
|
+
pipe(step) {
|
|
55
|
+
const { fn, name, timeoutMs } = normalizeStep(step);
|
|
56
|
+
const record = {
|
|
57
|
+
fn,
|
|
58
|
+
name,
|
|
59
|
+
timeoutMs,
|
|
60
|
+
isFallback: false
|
|
61
|
+
};
|
|
62
|
+
return new _Pipeline([...this.records, record]);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Add a step with an inline fallback. If the step throws, `fallback` is
|
|
66
|
+
* called with the error and the original input to that step.
|
|
67
|
+
*/
|
|
68
|
+
pipeWithFallback(step, fallback) {
|
|
69
|
+
const { fn, name, timeoutMs } = normalizeStep(step);
|
|
70
|
+
const stepRecord = {
|
|
71
|
+
fn,
|
|
72
|
+
name,
|
|
73
|
+
timeoutMs,
|
|
74
|
+
isFallback: false
|
|
75
|
+
};
|
|
76
|
+
const fallbackRecord = {
|
|
77
|
+
fn: fallback,
|
|
78
|
+
isFallback: true
|
|
79
|
+
};
|
|
80
|
+
return new _Pipeline([...this.records, stepRecord, fallbackRecord]);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Add a side-effect step. Runs `fn` for its effect only; passes the current
|
|
84
|
+
* value through unchanged. Errors in `fn` propagate normally.
|
|
85
|
+
*/
|
|
86
|
+
tap(fn) {
|
|
87
|
+
const tapStep = async (value, ctx) => {
|
|
88
|
+
await fn(value, ctx);
|
|
89
|
+
return value;
|
|
90
|
+
};
|
|
91
|
+
return this.pipe(tapStep);
|
|
92
|
+
}
|
|
93
|
+
async run(input, options = {}) {
|
|
94
|
+
const signal = options.signal ?? neverAborted;
|
|
95
|
+
const { onStepComplete, stepTimeoutMs } = options;
|
|
96
|
+
let current = input;
|
|
97
|
+
let stepIndex = 0;
|
|
98
|
+
for (let i = 0; i < this.records.length; i++) {
|
|
99
|
+
const record = this.records[i];
|
|
100
|
+
if (record.isFallback) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (signal.aborted) {
|
|
104
|
+
throw new DOMException("Pipeline aborted", "AbortError");
|
|
105
|
+
}
|
|
106
|
+
const inputValue = current;
|
|
107
|
+
const ctx = { signal, stepIndex, stepName: record.name };
|
|
108
|
+
const effectiveTimeout = record.timeoutMs ?? stepTimeoutMs;
|
|
109
|
+
try {
|
|
110
|
+
const rawResult = record.fn(current, ctx);
|
|
111
|
+
let stepPromise = rawResult instanceof Promise ? rawResult : Promise.resolve(rawResult);
|
|
112
|
+
if (effectiveTimeout != null) {
|
|
113
|
+
stepPromise = raceStepTimeout(stepPromise, effectiveTimeout, stepIndex, record.name);
|
|
114
|
+
}
|
|
115
|
+
current = await stepPromise;
|
|
116
|
+
onStepComplete?.(stepIndex, record.name, current);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
const nextRecord = this.records[i + 1];
|
|
119
|
+
if (nextRecord?.isFallback) {
|
|
120
|
+
const pipelineErr = new PipelineStepError(stepIndex, record.name, err, inputValue);
|
|
121
|
+
current = await nextRecord.fn(pipelineErr, inputValue);
|
|
122
|
+
i++;
|
|
123
|
+
onStepComplete?.(stepIndex, record.name, current);
|
|
124
|
+
} else {
|
|
125
|
+
throw new PipelineStepError(stepIndex, record.name, err, inputValue);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
stepIndex++;
|
|
129
|
+
}
|
|
130
|
+
return current;
|
|
131
|
+
}
|
|
132
|
+
get stepCount() {
|
|
133
|
+
return this.records.filter((r) => !r.isFallback).length;
|
|
134
|
+
}
|
|
135
|
+
/** Returns step metadata for debugging/tooling. */
|
|
136
|
+
toArray() {
|
|
137
|
+
let idx = 0;
|
|
138
|
+
const result = [];
|
|
139
|
+
for (const r of this.records) {
|
|
140
|
+
if (!r.isFallback) {
|
|
141
|
+
result.push({ index: idx++, name: r.name, timeoutMs: r.timeoutMs });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
function pipeline() {
|
|
148
|
+
return new Pipeline();
|
|
149
|
+
}
|
|
150
|
+
async function parallel(tasks, options = {}) {
|
|
151
|
+
const { concurrency, signal } = options;
|
|
152
|
+
if (concurrency == null || concurrency >= tasks.length) {
|
|
153
|
+
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
154
|
+
return Promise.all(tasks.map((t) => t()));
|
|
155
|
+
}
|
|
156
|
+
const results = new Array(tasks.length);
|
|
157
|
+
let nextIndex = 0;
|
|
158
|
+
let hasError = false;
|
|
159
|
+
let firstError;
|
|
160
|
+
const run = async () => {
|
|
161
|
+
while (nextIndex < tasks.length) {
|
|
162
|
+
if (signal?.aborted || hasError) break;
|
|
163
|
+
const idx = nextIndex++;
|
|
164
|
+
try {
|
|
165
|
+
results[idx] = await tasks[idx]();
|
|
166
|
+
} catch (err) {
|
|
167
|
+
hasError = true;
|
|
168
|
+
firstError = err;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, run);
|
|
174
|
+
await Promise.all(workers);
|
|
175
|
+
if (hasError) throw firstError;
|
|
176
|
+
return results;
|
|
177
|
+
}
|
|
178
|
+
async function parallelSettled(tasks, options = {}) {
|
|
179
|
+
const { concurrency, signal } = options;
|
|
180
|
+
const wrapped = tasks.map(
|
|
181
|
+
(t) => () => t().then(
|
|
182
|
+
(value) => ({ status: "fulfilled", value }),
|
|
183
|
+
(reason) => ({ status: "rejected", reason })
|
|
184
|
+
)
|
|
185
|
+
);
|
|
186
|
+
return parallel(wrapped, { concurrency, signal });
|
|
187
|
+
}
|
|
188
|
+
async function sequence(items, initial, fn, options = {}) {
|
|
189
|
+
const { signal } = options;
|
|
190
|
+
let acc = initial;
|
|
191
|
+
for (let i = 0; i < items.length; i++) {
|
|
192
|
+
if (signal?.aborted) throw new DOMException("Sequence aborted", "AbortError");
|
|
193
|
+
acc = await fn(acc, items[i], i);
|
|
194
|
+
}
|
|
195
|
+
return acc;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export { Pipeline, PipelineStepError, PipelineTimeoutError, parallel, parallelSettled, pipeline, sequence };
|
|
199
|
+
//# sourceMappingURL=index.mjs.map
|
|
200
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/flowx.ts"],"names":[],"mappings":";AAKO,IAAM,iBAAA,GAAN,cAAgC,KAAA,CAAM;AAAA,EAC3C,WAAA,CACkB,SAAA,EACA,QAAA,EACS,KAAA,EACT,UAAA,EAChB;AACA,IAAA,KAAA;AAAA,MACE,CAAA,wBAAA,EAA2B,SAAS,CAAA,EAAG,QAAA,GAAW,CAAA,EAAA,EAAK,QAAQ,CAAA,CAAA,CAAA,GAAM,EAAE,CAAA,EAAA,EAAK,MAAA,CAAO,KAAK,CAAC,CAAA;AAAA,KAC3F;AAPgB,IAAA,IAAA,CAAA,SAAA,GAAA,SAAA;AACA,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AACS,IAAA,IAAA,CAAA,KAAA,GAAA,KAAA;AACT,IAAA,IAAA,CAAA,UAAA,GAAA,UAAA;AAKhB,IAAA,IAAA,CAAK,IAAA,GAAO,mBAAA;AAAA,EACd;AACF;AAEO,IAAM,oBAAA,GAAN,cAAmC,KAAA,CAAM;AAAA,EAC9C,WAAA,CACkB,SAAA,EACA,QAAA,EACA,SAAA,EAChB;AACA,IAAA,KAAA,CAAM,CAAA,KAAA,EAAQ,SAAS,CAAA,EAAG,QAAA,GAAW,CAAA,EAAA,EAAK,QAAQ,CAAA,CAAA,CAAA,GAAM,EAAE,CAAA,iBAAA,EAAoB,SAAS,CAAA,EAAA,CAAI,CAAA;AAJ3E,IAAA,IAAA,CAAA,SAAA,GAAA,SAAA;AACA,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AACA,IAAA,IAAA,CAAA,SAAA,GAAA,SAAA;AAGhB,IAAA,IAAA,CAAK,IAAA,GAAO,sBAAA;AAAA,EACd;AACF;AAoBA,IAAM,YAAA,GAAe,IAAI,eAAA,EAAgB,CAAE,MAAA;AAE3C,SAAS,eAAA,CACP,OAAA,EACA,EAAA,EACA,KAAA,EACA,IAAA,EACY;AACZ,EAAA,OAAO,IAAI,OAAA,CAAW,CAAC,OAAA,EAAS,MAAA,KAAW;AACzC,IAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,MAAM,MAAA,CAAO,IAAI,oBAAA,CAAqB,KAAA,EAAO,IAAA,EAAM,EAAE,CAAC,CAAA,EAAG,EAAE,CAAA;AACpF,IAAA,OAAA,CAAQ,IAAA;AAAA,MACN,CAAC,CAAA,KAAM;AAAE,QAAA,YAAA,CAAa,KAAK,CAAA;AAAG,QAAA,OAAA,CAAQ,CAAC,CAAA;AAAA,MAAG,CAAA;AAAA,MAC1C,CAAC,CAAA,KAAM;AAAE,QAAA,YAAA,CAAa,KAAK,CAAA;AAAG,QAAA,MAAA,CAAO,CAAC,CAAA;AAAA,MAAG;AAAA,KAC3C;AAAA,EACF,CAAC,CAAA;AACH;AAEA,SAAS,cACP,IAAA,EACkF;AAClF,EAAA,IAAI,OAAO,SAAS,UAAA,EAAY;AAC9B,IAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,IAAA,EAAM,MAAA,EAAW,WAAW,MAAA,EAAU;AAAA,EAC3D;AACA,EAAA,OAAO,EAAE,IAAI,IAAA,CAAK,EAAA,EAAI,MAAM,IAAA,CAAK,IAAA,EAAM,SAAA,EAAW,IAAA,CAAK,SAAA,EAAU;AACnE;AAiBO,IAAM,QAAA,GAAN,MAAM,SAAA,CAAoB;AAAA,EACd,OAAA;AAAA,EAEjB,WAAA,CAAY,OAAA,GAAuB,EAAC,EAAG;AACrC,IAAA,IAAA,CAAK,OAAA,GAAU,OAAA;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,KACE,IAAA,EACsB;AACtB,IAAA,MAAM,EAAE,EAAA,EAAI,IAAA,EAAM,SAAA,EAAU,GAAI,cAAc,IAAI,CAAA;AAClD,IAAA,MAAM,MAAA,GAAqB;AAAA,MACzB,EAAA;AAAA,MACA,IAAA;AAAA,MACA,SAAA;AAAA,MACA,UAAA,EAAY;AAAA,KACd;AACA,IAAA,OAAO,IAAI,SAAA,CAAqB,CAAC,GAAG,IAAA,CAAK,OAAA,EAAS,MAAM,CAAC,CAAA;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,gBAAA,CACE,MACA,QAAA,EACsB;AACtB,IAAA,MAAM,EAAE,EAAA,EAAI,IAAA,EAAM,SAAA,EAAU,GAAI,cAAc,IAAI,CAAA;AAClD,IAAA,MAAM,UAAA,GAAyB;AAAA,MAC7B,EAAA;AAAA,MACA,IAAA;AAAA,MACA,SAAA;AAAA,MACA,UAAA,EAAY;AAAA,KACd;AACA,IAAA,MAAM,cAAA,GAAiC;AAAA,MACrC,EAAA,EAAI,QAAA;AAAA,MACJ,UAAA,EAAY;AAAA,KACd;AACA,IAAA,OAAO,IAAI,UAAqB,CAAC,GAAG,KAAK,OAAA,EAAS,UAAA,EAAY,cAAc,CAAC,CAAA;AAAA,EAC/E;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAI,EAAA,EAAsF;AACxF,IAAA,MAAM,OAAA,GAA8B,OAAO,KAAA,EAAO,GAAA,KAAQ;AACxD,MAAA,MAAM,EAAA,CAAG,OAAO,GAAG,CAAA;AACnB,MAAA,OAAO,KAAA;AAAA,IACT,CAAA;AACA,IAAA,OAAO,IAAA,CAAK,KAAK,OAAO,CAAA;AAAA,EAC1B;AAAA,EAEA,MAAM,GAAA,CAAI,KAAA,EAAY,OAAA,GAAwB,EAAC,EAAkB;AAC/D,IAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,IAAU,YAAA;AACjC,IAAA,MAAM,EAAE,cAAA,EAAgB,aAAA,EAAc,GAAI,OAAA;AAE1C,IAAA,IAAI,OAAA,GAAmB,KAAA;AACvB,IAAA,IAAI,SAAA,GAAY,CAAA;AAEhB,IAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,IAAA,CAAK,OAAA,CAAQ,QAAQ,CAAA,EAAA,EAAK;AAC5C,MAAA,MAAM,MAAA,GAAS,IAAA,CAAK,OAAA,CAAQ,CAAC,CAAA;AAE7B,MAAA,IAAI,OAAO,UAAA,EAAY;AAErB,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,OAAO,OAAA,EAAS;AAClB,QAAA,MAAM,IAAI,YAAA,CAAa,kBAAA,EAAoB,YAAY,CAAA;AAAA,MACzD;AAEA,MAAA,MAAM,UAAA,GAAa,OAAA;AACnB,MAAA,MAAM,MAAmB,EAAE,MAAA,EAAQ,SAAA,EAAW,QAAA,EAAU,OAAO,IAAA,EAAK;AACpE,MAAA,MAAM,gBAAA,GAAmB,OAAO,SAAA,IAAa,aAAA;AAE7C,MAAA,IAAI;AACF,QAAA,MAAM,SAAA,GAAY,MAAA,CAAO,EAAA,CAAG,OAAA,EAAS,GAAG,CAAA;AACxC,QAAA,IAAI,cAAgC,SAAA,YAAqB,OAAA,GAAU,SAAA,GAAY,OAAA,CAAQ,QAAQ,SAAS,CAAA;AACxG,QAAA,IAAI,oBAAoB,IAAA,EAAM;AAC5B,UAAA,WAAA,GAAc,eAAA,CAAgB,WAAA,EAAa,gBAAA,EAAkB,SAAA,EAAW,OAAO,IAAI,CAAA;AAAA,QACrF;AACA,QAAA,OAAA,GAAU,MAAM,WAAA;AAChB,QAAA,cAAA,GAAiB,SAAA,EAAW,MAAA,CAAO,IAAA,EAAM,OAAO,CAAA;AAAA,MAClD,SAAS,GAAA,EAAK;AAEZ,QAAA,MAAM,UAAA,GAAa,IAAA,CAAK,OAAA,CAAQ,CAAA,GAAI,CAAC,CAAA;AACrC,QAAA,IAAI,YAAY,UAAA,EAAY;AAC1B,UAAA,MAAM,cAAc,IAAI,iBAAA,CAAkB,WAAW,MAAA,CAAO,IAAA,EAAM,KAAK,UAAU,CAAA;AACjF,UAAA,OAAA,GAAU,MAAM,UAAA,CAAW,EAAA,CAAG,WAAA,EAAa,UAAU,CAAA;AACrD,UAAA,CAAA,EAAA;AACA,UAAA,cAAA,GAAiB,SAAA,EAAW,MAAA,CAAO,IAAA,EAAM,OAAO,CAAA;AAAA,QAClD,CAAA,MAAO;AACL,UAAA,MAAM,IAAI,iBAAA,CAAkB,SAAA,EAAW,MAAA,CAAO,IAAA,EAAM,KAAK,UAAU,CAAA;AAAA,QACrE;AAAA,MACF;AAEA,MAAA,SAAA,EAAA;AAAA,IACF;AAEA,IAAA,OAAO,OAAA;AAAA,EACT;AAAA,EAEA,IAAI,SAAA,GAAoB;AACtB,IAAA,OAAO,IAAA,CAAK,QAAQ,MAAA,CAAO,CAAC,MAAM,CAAC,CAAA,CAAE,UAAU,CAAA,CAAE,MAAA;AAAA,EACnD;AAAA;AAAA,EAGA,OAAA,GAA6F;AAC3F,IAAA,IAAI,GAAA,GAAM,CAAA;AACV,IAAA,MAAM,SAA4F,EAAC;AACnG,IAAA,KAAA,MAAW,CAAA,IAAK,KAAK,OAAA,EAAS;AAC5B,MAAA,IAAI,CAAC,EAAE,UAAA,EAAY;AACjB,QAAA,MAAA,CAAO,IAAA,CAAK,EAAE,KAAA,EAAO,GAAA,EAAA,EAAO,IAAA,EAAM,EAAE,IAAA,EAAM,SAAA,EAAW,CAAA,CAAE,SAAA,EAAW,CAAA;AAAA,MACpE;AAAA,IACF;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AACF;AAKO,SAAS,QAAA,GAA8B;AAC5C,EAAA,OAAO,IAAI,QAAA,EAAe;AAC5B;AAQA,eAAsB,QAAA,CACpB,KAAA,EACA,OAAA,GAA2B,EAAC,EACd;AACd,EAAA,MAAM,EAAE,WAAA,EAAa,MAAA,EAAO,GAAI,OAAA;AAEhC,EAAA,IAAI,WAAA,IAAe,IAAA,IAAQ,WAAA,IAAe,KAAA,CAAM,MAAA,EAAQ;AAEtD,IAAA,IAAI,QAAQ,OAAA,EAAS,MAAM,IAAI,YAAA,CAAa,WAAW,YAAY,CAAA;AACnE,IAAA,OAAO,OAAA,CAAQ,IAAI,KAAA,CAAM,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,EAAG,CAAC,CAAA;AAAA,EAC1C;AAGA,EAAA,MAAM,OAAA,GAAe,IAAI,KAAA,CAAM,KAAA,CAAM,MAAM,CAAA;AAC3C,EAAA,IAAI,SAAA,GAAY,CAAA;AAChB,EAAA,IAAI,QAAA,GAAW,KAAA;AACf,EAAA,IAAI,UAAA;AAEJ,EAAA,MAAM,MAAM,YAA2B;AACrC,IAAA,OAAO,SAAA,GAAY,MAAM,MAAA,EAAQ;AAC/B,MAAA,IAAI,MAAA,EAAQ,WAAW,QAAA,EAAU;AACjC,MAAA,MAAM,GAAA,GAAM,SAAA,EAAA;AACZ,MAAA,IAAI;AACF,QAAA,OAAA,CAAQ,GAAG,CAAA,GAAI,MAAM,KAAA,CAAM,GAAG,CAAA,EAAE;AAAA,MAClC,SAAS,GAAA,EAAK;AACZ,QAAA,QAAA,GAAW,IAAA;AACX,QAAA,UAAA,GAAa,GAAA;AACb,QAAA;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,OAAA,GAAU,KAAA,CAAM,IAAA,CAAK,EAAE,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,WAAA,EAAa,KAAA,CAAM,MAAM,CAAA,EAAE,EAAG,GAAG,CAAA;AAC/E,EAAA,MAAM,OAAA,CAAQ,IAAI,OAAO,CAAA;AAEzB,EAAA,IAAI,UAAU,MAAM,UAAA;AACpB,EAAA,OAAO,OAAA;AACT;AAMA,eAAsB,eAAA,CACpB,KAAA,EACA,OAAA,GAA2D,EAAC,EACxB;AACpC,EAAA,MAAM,EAAE,WAAA,EAAa,MAAA,EAAO,GAAI,OAAA;AAChC,EAAA,MAAM,UAAU,KAAA,CAAM,GAAA;AAAA,IAAI,CAAC,CAAA,KAAM,MAC/B,CAAA,EAAE,CAAE,IAAA;AAAA,MACF,CAAC,KAAA,MAAW,EAAE,MAAA,EAAQ,aAAsB,KAAA,EAAM,CAAA;AAAA,MAClD,CAAC,MAAA,MAAY,EAAE,MAAA,EAAQ,YAAqB,MAAA,EAAO;AAAA;AACrD,GACF;AAEA,EAAA,OAAO,QAAA,CAAS,OAAA,EAAS,EAAE,WAAA,EAAa,QAAQ,CAAA;AAClD;AAQA,eAAsB,SACpB,KAAA,EACA,OAAA,EACA,EAAA,EACA,OAAA,GAA2B,EAAC,EAChB;AACZ,EAAA,MAAM,EAAE,QAAO,GAAI,OAAA;AACnB,EAAA,IAAI,GAAA,GAAM,OAAA;AACV,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACrC,IAAA,IAAI,QAAQ,OAAA,EAAS,MAAM,IAAI,YAAA,CAAa,oBAAoB,YAAY,CAAA;AAC5E,IAAA,GAAA,GAAM,MAAM,EAAA,CAAG,GAAA,EAAK,KAAA,CAAM,CAAC,GAAG,CAAC,CAAA;AAAA,EACjC;AACA,EAAA,OAAO,GAAA;AACT","file":"index.mjs","sourcesContent":["export type { StepFn, StepContext, NamedStep, FlowxOptions, ParallelOptions, SequenceOptions } from './types.js';\nimport type { StepFn, StepContext, NamedStep, FlowxOptions, ParallelOptions, SequenceOptions } from './types.js';\n\n// ─── Errors ──────────────────────────────────────────────────────────────────\n\nexport class PipelineStepError extends Error {\n constructor(\n public readonly stepIndex: number,\n public readonly stepName: string | undefined,\n public override readonly cause: unknown,\n public readonly inputValue: unknown\n ) {\n super(\n `Pipeline failed at step ${stepIndex}${stepName ? ` (${stepName})` : ''}: ${String(cause)}`\n );\n this.name = 'PipelineStepError';\n }\n}\n\nexport class PipelineTimeoutError extends Error {\n constructor(\n public readonly stepIndex: number,\n public readonly stepName: string | undefined,\n public readonly timeoutMs: number\n ) {\n super(`Step ${stepIndex}${stepName ? ` (${stepName})` : ''} timed out after ${timeoutMs}ms`);\n this.name = 'PipelineTimeoutError';\n }\n}\n\n// ─── Internal Step Record ────────────────────────────────────────────────────\n\ninterface StepRecord {\n fn: StepFn<unknown, unknown>;\n name: string | undefined;\n timeoutMs: number | undefined;\n isFallback: false;\n}\n\ninterface FallbackRecord {\n fn: (error: PipelineStepError, input: unknown) => unknown | Promise<unknown>;\n isFallback: true;\n}\n\ntype AnyRecord = StepRecord | FallbackRecord;\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nconst neverAborted = new AbortController().signal;\n\nfunction raceStepTimeout<T>(\n promise: Promise<T>,\n ms: number,\n index: number,\n name: string | undefined\n): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => reject(new PipelineTimeoutError(index, name, ms)), ms);\n promise.then(\n (v) => { clearTimeout(timer); resolve(v); },\n (e) => { clearTimeout(timer); reject(e); }\n );\n });\n}\n\nfunction normalizeStep<In, Out>(\n step: StepFn<In, Out> | NamedStep<In, Out>\n): { fn: StepFn<In, Out>; name: string | undefined; timeoutMs: number | undefined } {\n if (typeof step === 'function') {\n return { fn: step, name: undefined, timeoutMs: undefined };\n }\n return { fn: step.fn, name: step.name, timeoutMs: step.timeoutMs };\n}\n\n// ─── Pipeline ────────────────────────────────────────────────────────────────\n\n/**\n * Flowx — composable, type-safe async pipeline builder.\n *\n * Each step receives the output of the previous step as its first argument,\n * plus a `StepContext` carrying the abort signal, step index, and step name.\n *\n * @example\n * const result = await pipeline<string>()\n * .pipe(s => s.trim())\n * .pipe({ name: 'uppercase', fn: s => s.toUpperCase() })\n * .tap(s => console.log('after uppercase:', s))\n * .run(' hello ');\n */\nexport class Pipeline<TIn, TOut> {\n private readonly records: AnyRecord[];\n\n constructor(records: AnyRecord[] = []) {\n this.records = records;\n }\n\n /**\n * Add a transform step. Accepts either a plain function or a `NamedStep`\n * object with an optional per-step `timeoutMs`.\n */\n pipe<TNext>(\n step: StepFn<TOut, TNext> | NamedStep<TOut, TNext>\n ): Pipeline<TIn, TNext> {\n const { fn, name, timeoutMs } = normalizeStep(step);\n const record: StepRecord = {\n fn: fn as StepFn<unknown, unknown>,\n name,\n timeoutMs,\n isFallback: false,\n };\n return new Pipeline<TIn, TNext>([...this.records, record]);\n }\n\n /**\n * Add a step with an inline fallback. If the step throws, `fallback` is\n * called with the error and the original input to that step.\n */\n pipeWithFallback<TNext>(\n step: StepFn<TOut, TNext> | NamedStep<TOut, TNext>,\n fallback: (error: PipelineStepError, input: TOut) => TNext | Promise<TNext>\n ): Pipeline<TIn, TNext> {\n const { fn, name, timeoutMs } = normalizeStep(step);\n const stepRecord: StepRecord = {\n fn: fn as StepFn<unknown, unknown>,\n name,\n timeoutMs,\n isFallback: false,\n };\n const fallbackRecord: FallbackRecord = {\n fn: fallback as (error: PipelineStepError, input: unknown) => unknown,\n isFallback: true,\n };\n return new Pipeline<TIn, TNext>([...this.records, stepRecord, fallbackRecord]);\n }\n\n /**\n * Add a side-effect step. Runs `fn` for its effect only; passes the current\n * value through unchanged. Errors in `fn` propagate normally.\n */\n tap(fn: (value: TOut, context: StepContext) => void | Promise<void>): Pipeline<TIn, TOut> {\n const tapStep: StepFn<TOut, TOut> = async (value, ctx) => {\n await fn(value, ctx);\n return value;\n };\n return this.pipe(tapStep);\n }\n\n async run(input: TIn, options: FlowxOptions = {}): Promise<TOut> {\n const signal = options.signal ?? neverAborted;\n const { onStepComplete, stepTimeoutMs } = options;\n\n let current: unknown = input;\n let stepIndex = 0;\n\n for (let i = 0; i < this.records.length; i++) {\n const record = this.records[i];\n\n if (record.isFallback) {\n // Fallback records are consumed inline by the preceding step handler.\n continue;\n }\n\n if (signal.aborted) {\n throw new DOMException('Pipeline aborted', 'AbortError');\n }\n\n const inputValue = current;\n const ctx: StepContext = { signal, stepIndex, stepName: record.name };\n const effectiveTimeout = record.timeoutMs ?? stepTimeoutMs;\n\n try {\n const rawResult = record.fn(current, ctx);\n let stepPromise: Promise<unknown> = rawResult instanceof Promise ? rawResult : Promise.resolve(rawResult);\n if (effectiveTimeout != null) {\n stepPromise = raceStepTimeout(stepPromise, effectiveTimeout, stepIndex, record.name);\n }\n current = await stepPromise;\n onStepComplete?.(stepIndex, record.name, current);\n } catch (err) {\n // Check if the next record is a fallback for this step.\n const nextRecord = this.records[i + 1];\n if (nextRecord?.isFallback) {\n const pipelineErr = new PipelineStepError(stepIndex, record.name, err, inputValue);\n current = await nextRecord.fn(pipelineErr, inputValue);\n i++; // skip the fallback record in the outer loop\n onStepComplete?.(stepIndex, record.name, current);\n } else {\n throw new PipelineStepError(stepIndex, record.name, err, inputValue);\n }\n }\n\n stepIndex++;\n }\n\n return current as TOut;\n }\n\n get stepCount(): number {\n return this.records.filter((r) => !r.isFallback).length;\n }\n\n /** Returns step metadata for debugging/tooling. */\n toArray(): Array<{ index: number; name: string | undefined; timeoutMs: number | undefined }> {\n let idx = 0;\n const result: Array<{ index: number; name: string | undefined; timeoutMs: number | undefined }> = [];\n for (const r of this.records) {\n if (!r.isFallback) {\n result.push({ index: idx++, name: r.name, timeoutMs: r.timeoutMs });\n }\n }\n return result;\n }\n}\n\n// ─── Factory ─────────────────────────────────────────────────────────────────\n\n/** Create a new empty pipeline starting with type `T`. */\nexport function pipeline<T>(): Pipeline<T, T> {\n return new Pipeline<T, T>();\n}\n\n// ─── parallel ────────────────────────────────────────────────────────────────\n\n/**\n * Run tasks concurrently, return results in declaration order.\n * Rejects if any task throws (like `Promise.all`).\n */\nexport async function parallel<T>(\n tasks: Array<() => Promise<T>>,\n options: ParallelOptions = {}\n): Promise<T[]> {\n const { concurrency, signal } = options;\n\n if (concurrency == null || concurrency >= tasks.length) {\n // Unbounded — just Promise.all\n if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');\n return Promise.all(tasks.map((t) => t()));\n }\n\n // Bounded concurrency via semaphore\n const results: T[] = new Array(tasks.length);\n let nextIndex = 0;\n let hasError = false;\n let firstError: unknown;\n\n const run = async (): Promise<void> => {\n while (nextIndex < tasks.length) {\n if (signal?.aborted || hasError) break;\n const idx = nextIndex++;\n try {\n results[idx] = await tasks[idx]();\n } catch (err) {\n hasError = true;\n firstError = err;\n break;\n }\n }\n };\n\n const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, run);\n await Promise.all(workers);\n\n if (hasError) throw firstError;\n return results;\n}\n\n/**\n * Run tasks concurrently and return all results regardless of success/failure.\n * Equivalent to `Promise.allSettled` but with optional concurrency control.\n */\nexport async function parallelSettled<T>(\n tasks: Array<() => Promise<T>>,\n options: Pick<ParallelOptions, 'concurrency' | 'signal'> = {}\n): Promise<PromiseSettledResult<T>[]> {\n const { concurrency, signal } = options;\n const wrapped = tasks.map((t) => (): Promise<PromiseSettledResult<T>> =>\n t().then(\n (value) => ({ status: 'fulfilled' as const, value }),\n (reason) => ({ status: 'rejected' as const, reason })\n )\n );\n\n return parallel(wrapped, { concurrency, signal }) as Promise<PromiseSettledResult<T>[]>;\n}\n\n// ─── sequence ────────────────────────────────────────────────────────────────\n\n/**\n * Async left-fold over an array. Like `Array.reduce` but supports async reducers\n * and can be cancelled via `AbortSignal`.\n */\nexport async function sequence<T, A>(\n items: T[],\n initial: A,\n fn: (acc: A, item: T, index: number) => Promise<A> | A,\n options: SequenceOptions = {}\n): Promise<A> {\n const { signal } = options;\n let acc = initial;\n for (let i = 0; i < items.length; i++) {\n if (signal?.aborted) throw new DOMException('Sequence aborted', 'AbortError');\n acc = await fn(acc, items[i], i);\n }\n return acc;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@async-kit/flowx",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Composable async pipeline builder for JavaScript/TypeScript",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"async",
|
|
7
|
+
"pipeline",
|
|
8
|
+
"flow",
|
|
9
|
+
"stream",
|
|
10
|
+
"compose",
|
|
11
|
+
"transform"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/NexaLeaf/async-kit",
|
|
17
|
+
"directory": "packages/flowx"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/NexaLeaf/async-kit/tree/main/packages/flowx#readme",
|
|
20
|
+
"main": "./dist/index.cjs",
|
|
21
|
+
"module": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"import": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"require": {
|
|
30
|
+
"types": "./dist/index.d.cts",
|
|
31
|
+
"default": "./dist/index.cjs"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md",
|
|
41
|
+
"CHANGELOG.md"
|
|
42
|
+
],
|
|
43
|
+
"sideEffects": false,
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup",
|
|
46
|
+
"typecheck": "tsc -p tsconfig.lib.json --noEmit"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {}
|
|
49
|
+
}
|