@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 ADDED
@@ -0,0 +1,9 @@
1
+ ## 0.1.2 (2026-03-11)
2
+
3
+ ### 🩹 Fixes
4
+
5
+ - **release:** add publishConfig access public to all packages ([82c12ca](https://github.com/NexaLeaf/async-kit/commit/82c12ca))
6
+
7
+ ### ❤️ Thank You
8
+
9
+ - Palanisamy Muthusamy
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&section=header&text=flowx&fontSize=60&fontColor=fff&animation=fadeIn&desc=%40async-kit%2Fflowx&descAlignY=75&descAlign=50" width="100%"/>
4
+
5
+ <br/>
6
+
7
+ [![npm](https://img.shields.io/npm/v/@async-kit/flowx?style=for-the-badge&logo=npm&color=45B7D1)](https://www.npmjs.com/package/@async-kit/flowx)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-3178C6?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](../../LICENSE)
10
+ [![Bundle size](https://img.shields.io/bundlephobia/minzip/@async-kit/flowx?style=for-the-badge&color=45B7D1)](https://bundlephobia.com/package/@async-kit/flowx)
11
+ [![Node](https://img.shields.io/badge/Node-%3E%3D18-339933?style=for-the-badge&logo=node.js&logoColor=white)](https://nodejs.org/)
12
+ [![Browser](https://img.shields.io/badge/Browser-Supported-4285F4?style=for-the-badge&logo=googlechrome&logoColor=white)](#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
@@ -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 };
@@ -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":"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","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
+ }