flowneer 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -263,6 +263,61 @@ await new FlowBuilder<RefineState>()
263
263
 
264
264
  > **Tip:** Pair with [`withCycles`](#withcycles) to cap the maximum number of jumps.
265
265
 
266
+ ### `fragment()` and `.add(fragment)`
267
+
268
+ Fragments are reusable partial flows — composable step chains that can be spliced into any `FlowBuilder`. Think of them like Zod partials for flows.
269
+
270
+ Create a fragment with the `fragment()` factory, chain steps on it, then embed it with `.add()`:
271
+
272
+ ```typescript
273
+ import { FlowBuilder, fragment } from "flowneer";
274
+
275
+ interface State {
276
+ input: string;
277
+ enriched: boolean;
278
+ summary: string;
279
+ }
280
+
281
+ // Define reusable fragments
282
+ const enrich = fragment<State>()
283
+ .then(async (s) => {
284
+ s.enriched = true;
285
+ })
286
+ .then(async (s) => {
287
+ s.input = s.input.trim();
288
+ });
289
+
290
+ const summarise = fragment<State>().loop(
291
+ (s) => !s.summary,
292
+ (b) =>
293
+ b.then(async (s) => {
294
+ s.summary = s.input.slice(0, 10);
295
+ }),
296
+ );
297
+
298
+ // Compose into a full flow
299
+ await new FlowBuilder<State>()
300
+ .startWith(async (s) => {
301
+ s.input = " hello world ";
302
+ })
303
+ .add(enrich) // splices enrich's steps inline
304
+ .add(summarise) // splices summarise's steps inline
305
+ .then(async (s) => console.log(s.summary))
306
+ .run({ input: "", enriched: false, summary: "" });
307
+ // → hello worl
308
+ ```
309
+
310
+ Fragments support all step types — `.then()`, `.loop()`, `.batch()`, `.branch()`, `.parallel()`, `.anchor()`. They **cannot** be run directly — calling `.run()` or `.stream()` on a fragment throws.
311
+
312
+ The same fragment can be reused across multiple flows:
313
+
314
+ ```typescript
315
+ const validate = fragment<State>().then(checkInput).then(sanitize);
316
+
317
+ const flowA = new FlowBuilder<State>().add(validate).then(handleA);
318
+ const flowB = new FlowBuilder<State>().add(validate).then(handleB);
319
+ ```
320
+
266
321
  ## using with `withCycles` plugin
267
322
 
268
323
  `withCycles` guards against infinite anchor-jump loops. Each call registers one limit; multiple calls stack.
@@ -147,13 +147,12 @@ type Step<S, P extends Record<string, unknown>> = FnStep<S, P> | BranchStep<S, P
147
147
  declare class FlowBuilder<S = any, P extends Record<string, unknown> = Record<string, unknown>> {
148
148
  protected steps: Step<S, P>[];
149
149
  private _hooksList;
150
- /** Cached flat arrays of present hooks — invalidated whenever a hook is added. */
151
- private _cachedHooks;
152
- private _getHooks;
153
- /** Register a plugin — copies its methods onto `FlowBuilder.prototype`. */
154
- static use(plugin: FlowneerPlugin): void;
150
+ private _hooksCache;
151
+ private _hooks;
155
152
  /** Register lifecycle hooks (called by plugin methods, not by consumers). */
156
153
  protected _setHooks(hooks: Partial<FlowHooks<S, P>>): void;
154
+ /** Register a plugin — copies its methods onto `FlowBuilder.prototype`. */
155
+ static use(plugin: FlowneerPlugin): void;
157
156
  /** Set the first step, resetting any prior chain. */
158
157
  startWith(fn: NodeFn<S, P>, options?: NodeOptions<S, P>): this;
159
158
  /** Append a sequential step. */
@@ -180,7 +179,7 @@ declare class FlowBuilder<S = any, P extends Record<string, unknown> = Record<st
180
179
  add(frag: FlowBuilder<S, P>): this;
181
180
  /**
182
181
  * Append a routing step.
183
- * `router` returns a key; the matching branch flow executes, then the chain continues.
182
+ * `router` returns a key; the matching branch executes, then the chain continues.
184
183
  */
185
184
  branch(router: NodeFn<S, P>, branches: Record<string, NodeFn<S, P>>, options?: NodeOptions<S, P>): this;
186
185
  /**
@@ -236,12 +235,19 @@ declare class FlowBuilder<S = any, P extends Record<string, unknown> = Record<st
236
235
  */
237
236
  stream(shared: S, params?: P, options?: RunOptions): AsyncGenerator<StreamEvent<S>>;
238
237
  protected _execute(shared: S, params: P, signal?: AbortSignal): Promise<void>;
238
+ /**
239
+ * Apply timeout and `wrapStep` middleware around a single step, then
240
+ * delegate to `_dispatchStep`. Returns an anchor target if the step
241
+ * issued a goto, otherwise `undefined`.
242
+ */
243
+ private _runStep;
244
+ /**
245
+ * Pure step dispatch — no timeout, no `wrapStep`. Returns goto target if any.
246
+ */
247
+ private _dispatchStep;
248
+ private _runParallel;
239
249
  private _addFn;
240
- /** Resolve a NumberOrFn value against the current shared state and params. */
241
- private _res;
242
250
  private _runSub;
243
- private _retry;
244
- private _withTimeout;
245
251
  }
246
252
 
247
- export { FlowBuilder as F, type NodeFn as N, type RunOptions as R, type StepMeta as S, type Validator as V, type NodeOptions as a, type FlowneerPlugin as b, type FlowHooks as c, type NumberOrFn as d, type StreamEvent as e };
253
+ export { type AnchorStep as A, type BatchStep as B, FlowBuilder as F, type LoopStep as L, type NodeFn as N, type ParallelStep as P, type RunOptions as R, type StepMeta as S, type Validator as V, type FlowneerPlugin as a, type NodeOptions as b, type FlowHooks as c, type BranchStep as d, type FnStep as e, type NumberOrFn as f, type Step as g, type StreamEvent as h };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { F as FlowBuilder, c as FlowHooks, b as FlowneerPlugin, N as NodeFn, a as NodeOptions, d as NumberOrFn, R as RunOptions, S as StepMeta, e as StreamEvent, V as Validator } from './FlowBuilder-Dqppgla9.js';
1
+ export { F as FlowBuilder, c as FlowHooks, a as FlowneerPlugin, N as NodeFn, b as NodeOptions, f as NumberOrFn, R as RunOptions, S as StepMeta, h as StreamEvent, V as Validator } from './FlowBuilder-C3B91Fzo.js';
2
2
  export { FlowError, Fragment, InterruptError, fragment } from './src/index.js';
package/dist/index.js CHANGED
@@ -21,42 +21,93 @@ var InterruptError = class extends Error {
21
21
  };
22
22
 
23
23
  // src/FlowBuilder.ts
24
+ function buildHookCache(list) {
25
+ const pick = (key) => list.map((h) => h[key]).filter(Boolean);
26
+ return {
27
+ beforeFlow: pick("beforeFlow"),
28
+ beforeStep: pick("beforeStep"),
29
+ wrapStep: pick("wrapStep"),
30
+ afterStep: pick("afterStep"),
31
+ wrapParallelFn: pick("wrapParallelFn"),
32
+ onError: pick("onError"),
33
+ afterFlow: pick("afterFlow")
34
+ };
35
+ }
36
+ function resolveNumber(val, fallback, shared, params) {
37
+ if (val === void 0) return fallback;
38
+ return typeof val === "function" ? val(shared, params) : val;
39
+ }
40
+ async function retry(times, delaySec, fn) {
41
+ if (times === 1) return fn();
42
+ while (true) {
43
+ try {
44
+ return await fn();
45
+ } catch (err) {
46
+ if (!--times) throw err;
47
+ if (delaySec > 0)
48
+ await new Promise((r) => setTimeout(r, delaySec * 1e3));
49
+ }
50
+ }
51
+ }
52
+ function withTimeout(ms, fn) {
53
+ let timer;
54
+ return Promise.race([
55
+ fn().finally(() => clearTimeout(timer)),
56
+ new Promise((_, reject) => {
57
+ timer = setTimeout(
58
+ () => reject(new Error(`step timed out after ${ms}ms`)),
59
+ ms
60
+ );
61
+ })
62
+ ]);
63
+ }
64
+ function isAnchorTarget(value) {
65
+ return typeof value === "string" && value[0] === "#";
66
+ }
67
+ async function runFnResult(result, shared) {
68
+ if (result != null && typeof result[Symbol.asyncIterator] === "function") {
69
+ const gen = result;
70
+ let next = await gen.next();
71
+ while (!next.done) {
72
+ shared.__stream?.(next.value);
73
+ next = await gen.next();
74
+ }
75
+ return isAnchorTarget(next.value) ? next.value.slice(1) : void 0;
76
+ }
77
+ return isAnchorTarget(result) ? result.slice(1) : void 0;
78
+ }
79
+ function buildAnchorMap(steps) {
80
+ const map = /* @__PURE__ */ new Map();
81
+ for (let i = 0; i < steps.length; i++) {
82
+ const s = steps[i];
83
+ if (s.type === "anchor") map.set(s.name, i);
84
+ }
85
+ return map;
86
+ }
24
87
  var FlowBuilder = class _FlowBuilder {
25
88
  steps = [];
26
89
  _hooksList = [];
27
- /** Cached flat arrays of present hooks — invalidated whenever a hook is added. */
28
- _cachedHooks = null;
29
- _getHooks() {
30
- if (this._cachedHooks) return this._cachedHooks;
31
- const hl = this._hooksList;
32
- this._cachedHooks = {
33
- beforeFlow: hl.map((h) => h.beforeFlow).filter(Boolean),
34
- beforeStep: hl.map((h) => h.beforeStep).filter(Boolean),
35
- wrapStep: hl.map((h) => h.wrapStep).filter(Boolean),
36
- afterStep: hl.map((h) => h.afterStep).filter(Boolean),
37
- wrapParallelFn: hl.map((h) => h.wrapParallelFn).filter(Boolean),
38
- onError: hl.map((h) => h.onError).filter(Boolean),
39
- afterFlow: hl.map((h) => h.afterFlow).filter(Boolean)
40
- };
41
- return this._cachedHooks;
90
+ _hooksCache = null;
91
+ // -------------------------------------------------------------------------
92
+ // Hooks & plugins
93
+ // -------------------------------------------------------------------------
94
+ _hooks() {
95
+ return this._hooksCache ??= buildHookCache(this._hooksList);
96
+ }
97
+ /** Register lifecycle hooks (called by plugin methods, not by consumers). */
98
+ _setHooks(hooks) {
99
+ this._hooksList.push(hooks);
100
+ this._hooksCache = null;
42
101
  }
43
- // -----------------------------------------------------------------------
44
- // Plugin registration
45
- // -----------------------------------------------------------------------
46
102
  /** Register a plugin — copies its methods onto `FlowBuilder.prototype`. */
47
103
  static use(plugin) {
48
104
  for (const [name, fn] of Object.entries(plugin)) {
49
105
  _FlowBuilder.prototype[name] = fn;
50
106
  }
51
107
  }
52
- /** Register lifecycle hooks (called by plugin methods, not by consumers). */
53
- _setHooks(hooks) {
54
- this._hooksList.push(hooks);
55
- this._cachedHooks = null;
56
- }
57
- // -----------------------------------------------------------------------
58
- // Public API
59
- // -----------------------------------------------------------------------
108
+ // -------------------------------------------------------------------------
109
+ // Builder API
110
+ // -------------------------------------------------------------------------
60
111
  /** Set the first step, resetting any prior chain. */
61
112
  startWith(fn, options) {
62
113
  this.steps = [];
@@ -91,7 +142,7 @@ var FlowBuilder = class _FlowBuilder {
91
142
  }
92
143
  /**
93
144
  * Append a routing step.
94
- * `router` returns a key; the matching branch flow executes, then the chain continues.
145
+ * `router` returns a key; the matching branch executes, then the chain continues.
95
146
  */
96
147
  branch(router, branches, options) {
97
148
  this.steps.push({
@@ -133,12 +184,11 @@ var FlowBuilder = class _FlowBuilder {
133
184
  batch(items, processor, options) {
134
185
  const inner = new _FlowBuilder();
135
186
  processor(inner);
136
- const key = options?.key ?? "__batchItem";
137
187
  this.steps.push({
138
188
  type: "batch",
139
189
  itemsExtractor: items,
140
190
  processor: inner,
141
- key
191
+ key: options?.key ?? "__batchItem"
142
192
  });
143
193
  return this;
144
194
  }
@@ -169,10 +219,13 @@ var FlowBuilder = class _FlowBuilder {
169
219
  this.steps.push({ type: "anchor", name });
170
220
  return this;
171
221
  }
222
+ // -------------------------------------------------------------------------
223
+ // Execution API
224
+ // -------------------------------------------------------------------------
172
225
  /** Execute the flow. */
173
226
  async run(shared, params, options) {
174
227
  const p = params ?? {};
175
- const hooks = this._getHooks();
228
+ const hooks = this._hooks();
176
229
  for (const h of hooks.beforeFlow) await h(shared, p);
177
230
  try {
178
231
  await this._execute(shared, p, options?.signal);
@@ -193,24 +246,18 @@ var FlowBuilder = class _FlowBuilder {
193
246
  */
194
247
  async *stream(shared, params, options) {
195
248
  const queue = [];
196
- let resolve = null;
249
+ let notify = null;
197
250
  const push = (event) => {
198
251
  queue.push(event);
199
- if (resolve) {
200
- resolve();
201
- resolve = null;
202
- }
252
+ notify?.();
253
+ notify = null;
203
254
  };
204
- const pull = () => queue.length > 0 ? Promise.resolve() : new Promise((r) => {
205
- resolve = r;
255
+ const drain = () => queue.length > 0 ? Promise.resolve() : new Promise((r) => {
256
+ notify = r;
206
257
  });
207
258
  this._setHooks({
208
- beforeStep: (meta) => {
209
- push({ type: "step:before", meta });
210
- },
211
- afterStep: (meta, s) => {
212
- push({ type: "step:after", meta, shared: s });
213
- }
259
+ beforeStep: (meta) => push({ type: "step:before", meta }),
260
+ afterStep: (meta, s) => push({ type: "step:after", meta, shared: s })
214
261
  });
215
262
  const prevStream = shared.__stream;
216
263
  shared.__stream = (chunk) => {
@@ -219,7 +266,7 @@ var FlowBuilder = class _FlowBuilder {
219
266
  };
220
267
  this.run(shared, params, options).catch((err) => push({ type: "error", error: err })).then(() => push(null));
221
268
  while (true) {
222
- await pull();
269
+ await drain();
223
270
  while (queue.length > 0) {
224
271
  const event = queue.shift();
225
272
  if (event === null) {
@@ -230,18 +277,13 @@ var FlowBuilder = class _FlowBuilder {
230
277
  }
231
278
  }
232
279
  }
233
- // -----------------------------------------------------------------------
234
- // Internal execution
235
- // -----------------------------------------------------------------------
280
+ // -------------------------------------------------------------------------
281
+ // Internal execution engine
282
+ // -------------------------------------------------------------------------
236
283
  async _execute(shared, params, signal) {
237
- const hooks = this._getHooks();
238
- const labels = /* @__PURE__ */ new Map();
239
- if (this.steps.some((s) => s.type === "anchor")) {
240
- for (let j = 0; j < this.steps.length; j++) {
241
- const s = this.steps[j];
242
- if (s.type === "anchor") labels.set(s.name, j);
243
- }
244
- }
284
+ const hooks = this._hooks();
285
+ const hasAnchors = this.steps.some((s) => s.type === "anchor");
286
+ const labels = hasAnchors ? buildAnchorMap(this.steps) : /* @__PURE__ */ new Map();
245
287
  for (let i = 0; i < this.steps.length; i++) {
246
288
  signal?.throwIfAborted();
247
289
  const step = this.steps[i];
@@ -249,123 +291,16 @@ var FlowBuilder = class _FlowBuilder {
249
291
  const meta = { index: i, type: step.type };
250
292
  try {
251
293
  for (const h of hooks.beforeStep) await h(meta, shared, params);
252
- let gotoTarget;
253
- const runBody = async () => {
254
- switch (step.type) {
255
- case "fn": {
256
- const result = await this._retry(
257
- this._res(step.retries, 1, shared, params),
258
- this._res(step.delaySec, 0, shared, params),
259
- () => step.fn(shared, params)
260
- );
261
- if (result != null && typeof result[Symbol.asyncIterator] === "function") {
262
- const gen = result;
263
- let genResult = await gen.next();
264
- while (!genResult.done) {
265
- shared.__stream?.(genResult.value);
266
- genResult = await gen.next();
267
- }
268
- if (typeof genResult.value === "string" && genResult.value[0] === "#")
269
- gotoTarget = genResult.value.slice(1);
270
- } else if (typeof result === "string" && result[0] === "#") {
271
- gotoTarget = result.slice(1);
272
- }
273
- break;
274
- }
275
- case "branch": {
276
- const bRetries = this._res(step.retries, 1, shared, params);
277
- const bDelay = this._res(step.delaySec, 0, shared, params);
278
- const action = await this._retry(
279
- bRetries,
280
- bDelay,
281
- () => step.router(shared, params)
282
- );
283
- const key = action ? String(action) : "default";
284
- const fn = step.branches[key] ?? step.branches["default"];
285
- if (fn) {
286
- const branchResult = await this._retry(
287
- bRetries,
288
- bDelay,
289
- () => fn(shared, params)
290
- );
291
- if (typeof branchResult === "string" && branchResult[0] === "#")
292
- gotoTarget = branchResult.slice(1);
293
- }
294
- break;
295
- }
296
- case "loop":
297
- while (await step.condition(shared, params))
298
- await this._runSub(
299
- `loop (step ${i})`,
300
- () => step.body._execute(shared, params, signal)
301
- );
302
- break;
303
- case "batch": {
304
- const k = step.key;
305
- const prev = shared[k];
306
- const hadKey = Object.prototype.hasOwnProperty.call(shared, k);
307
- const list = await step.itemsExtractor(shared, params);
308
- for (const item of list) {
309
- shared[k] = item;
310
- await this._runSub(
311
- `batch (step ${i})`,
312
- () => step.processor._execute(shared, params, signal)
313
- );
314
- }
315
- if (!hadKey) delete shared[k];
316
- else shared[k] = prev;
317
- break;
318
- }
319
- case "parallel": {
320
- const pfnWrappers = hooks.wrapParallelFn;
321
- const pRetries = this._res(step.retries, 1, shared, params);
322
- const pDelay = this._res(step.delaySec, 0, shared, params);
323
- if (step.reducer) {
324
- const drafts = [];
325
- await Promise.all(
326
- step.fns.map(async (fn, fi) => {
327
- const draft = { ...shared };
328
- drafts[fi] = draft;
329
- const exec = () => this._retry(pRetries, pDelay, () => fn(draft, params));
330
- const wrapped2 = pfnWrappers.reduceRight(
331
- (next, wrap) => () => wrap(meta, fi, next, draft, params),
332
- async () => {
333
- await exec();
334
- }
335
- );
336
- await wrapped2();
337
- })
338
- );
339
- step.reducer(shared, drafts);
340
- } else {
341
- await Promise.all(
342
- step.fns.map((fn, fi) => {
343
- const exec = () => this._retry(pRetries, pDelay, () => fn(shared, params));
344
- const wrapped2 = pfnWrappers.reduceRight(
345
- (next, wrap) => () => wrap(meta, fi, next, shared, params),
346
- async () => {
347
- await exec();
348
- }
349
- );
350
- return wrapped2();
351
- })
352
- );
353
- }
354
- break;
355
- }
356
- }
357
- };
358
- const rawTimeout = step.timeoutMs;
359
- const resolvedTimeout = this._res(rawTimeout, 0, shared, params);
360
- const baseExec = () => resolvedTimeout > 0 ? this._withTimeout(resolvedTimeout, runBody) : runBody();
361
- const wrappers = hooks.wrapStep;
362
- const wrapped = wrappers.reduceRight(
363
- (next, wrap) => () => wrap(meta, next, shared, params),
364
- baseExec
294
+ const gotoTarget = await this._runStep(
295
+ step,
296
+ meta,
297
+ shared,
298
+ params,
299
+ signal,
300
+ hooks
365
301
  );
366
- await wrapped();
367
302
  for (const h of hooks.afterStep) await h(meta, shared, params);
368
- if (gotoTarget) {
303
+ if (gotoTarget !== void 0) {
369
304
  const target = labels.get(gotoTarget);
370
305
  if (target === void 0)
371
306
  throw new Error(`goto target anchor "${gotoTarget}" not found`);
@@ -375,11 +310,121 @@ var FlowBuilder = class _FlowBuilder {
375
310
  if (err instanceof InterruptError) throw err;
376
311
  for (const h of hooks.onError) h(meta, err, shared, params);
377
312
  if (err instanceof FlowError) throw err;
378
- const stepLabel = step.type === "fn" ? `step ${i}` : `${step.type} (step ${i})`;
379
- throw new FlowError(stepLabel, err);
313
+ const label = step.type === "fn" ? `step ${i}` : `${step.type} (step ${i})`;
314
+ throw new FlowError(label, err);
315
+ }
316
+ }
317
+ }
318
+ /**
319
+ * Apply timeout and `wrapStep` middleware around a single step, then
320
+ * delegate to `_dispatchStep`. Returns an anchor target if the step
321
+ * issued a goto, otherwise `undefined`.
322
+ */
323
+ async _runStep(step, meta, shared, params, signal, hooks) {
324
+ let gotoTarget;
325
+ const execute = async () => {
326
+ gotoTarget = await this._dispatchStep(
327
+ step,
328
+ meta,
329
+ shared,
330
+ params,
331
+ signal,
332
+ hooks
333
+ );
334
+ };
335
+ const timeoutMs = resolveNumber(
336
+ step.timeoutMs,
337
+ 0,
338
+ shared,
339
+ params
340
+ );
341
+ const baseExec = timeoutMs > 0 ? () => withTimeout(timeoutMs, execute) : execute;
342
+ const wrapped = hooks.wrapStep.reduceRight(
343
+ (next, wrap) => () => wrap(meta, next, shared, params),
344
+ baseExec
345
+ );
346
+ await wrapped();
347
+ return gotoTarget;
348
+ }
349
+ /**
350
+ * Pure step dispatch — no timeout, no `wrapStep`. Returns goto target if any.
351
+ */
352
+ async _dispatchStep(step, meta, shared, params, signal, hooks) {
353
+ switch (step.type) {
354
+ case "fn": {
355
+ const result = await retry(
356
+ resolveNumber(step.retries, 1, shared, params),
357
+ resolveNumber(step.delaySec, 0, shared, params),
358
+ () => step.fn(shared, params)
359
+ );
360
+ return runFnResult(result, shared);
361
+ }
362
+ case "branch": {
363
+ const r = resolveNumber(step.retries, 1, shared, params);
364
+ const d = resolveNumber(step.delaySec, 0, shared, params);
365
+ const action = await retry(r, d, () => step.router(shared, params));
366
+ const key = action ? String(action) : "default";
367
+ const fn = step.branches[key] ?? step.branches["default"];
368
+ if (fn) {
369
+ const result = await retry(r, d, () => fn(shared, params));
370
+ if (isAnchorTarget(result)) return result.slice(1);
371
+ }
372
+ return void 0;
373
+ }
374
+ case "loop": {
375
+ while (await step.condition(shared, params))
376
+ await this._runSub(
377
+ `loop (step ${meta.index})`,
378
+ () => step.body._execute(shared, params, signal)
379
+ );
380
+ return void 0;
381
+ }
382
+ case "batch": {
383
+ const { key, itemsExtractor, processor } = step;
384
+ const prev = shared[key];
385
+ const hadKey = Object.prototype.hasOwnProperty.call(shared, key);
386
+ const list = await itemsExtractor(shared, params);
387
+ for (const item of list) {
388
+ shared[key] = item;
389
+ await this._runSub(
390
+ `batch (step ${meta.index})`,
391
+ () => processor._execute(shared, params, signal)
392
+ );
393
+ }
394
+ if (!hadKey) delete shared[key];
395
+ else shared[key] = prev;
396
+ return void 0;
397
+ }
398
+ case "parallel": {
399
+ await this._runParallel(step, meta, shared, params, hooks);
400
+ return void 0;
380
401
  }
381
402
  }
382
403
  }
404
+ async _runParallel(step, meta, shared, params, hooks) {
405
+ const r = resolveNumber(step.retries, 1, shared, params);
406
+ const d = resolveNumber(step.delaySec, 0, shared, params);
407
+ const wrappers = hooks.wrapParallelFn;
408
+ const runFn = (fn, s, fi) => {
409
+ const exec = async () => {
410
+ await retry(r, d, () => fn(s, params));
411
+ };
412
+ return wrappers.reduceRight(
413
+ (next, wrap) => () => wrap(meta, fi, next, s, params),
414
+ exec
415
+ )();
416
+ };
417
+ if (step.reducer) {
418
+ const drafts = step.fns.map(() => ({ ...shared }));
419
+ await Promise.all(step.fns.map((fn, fi) => runFn(fn, drafts[fi], fi)));
420
+ step.reducer(shared, drafts);
421
+ } else {
422
+ await Promise.all(step.fns.map((fn, fi) => runFn(fn, shared, fi)));
423
+ }
424
+ }
425
+ // -------------------------------------------------------------------------
426
+ // Private helpers
427
+ // -------------------------------------------------------------------------
383
428
  _addFn(fn, options) {
384
429
  this.steps.push({
385
430
  type: "fn",
@@ -390,11 +435,6 @@ var FlowBuilder = class _FlowBuilder {
390
435
  });
391
436
  return this;
392
437
  }
393
- /** Resolve a NumberOrFn value against the current shared state and params. */
394
- _res(val, def, shared, params) {
395
- if (val === void 0) return def;
396
- return typeof val === "function" ? val(shared, params) : val;
397
- }
398
438
  async _runSub(label, fn) {
399
439
  try {
400
440
  return await fn();
@@ -402,30 +442,6 @@ var FlowBuilder = class _FlowBuilder {
402
442
  throw new FlowError(label, err instanceof FlowError ? err.cause : err);
403
443
  }
404
444
  }
405
- async _retry(times, delaySec, fn) {
406
- if (times === 1) return fn();
407
- while (true) {
408
- try {
409
- return await fn();
410
- } catch (err) {
411
- if (!--times) throw err;
412
- if (delaySec > 0)
413
- await new Promise((r) => setTimeout(r, delaySec * 1e3));
414
- }
415
- }
416
- }
417
- _withTimeout(ms, fn) {
418
- let timer;
419
- return Promise.race([
420
- fn().finally(() => clearTimeout(timer)),
421
- new Promise((_, reject) => {
422
- timer = setTimeout(
423
- () => reject(new Error(`step timed out after ${ms}ms`)),
424
- ms
425
- );
426
- })
427
- ]);
428
- }
429
445
  };
430
446
 
431
447
  // src/Fragment.ts
@@ -437,6 +453,8 @@ var Fragment = class extends FlowBuilder {
437
453
  );
438
454
  }
439
455
  /** @internal Fragments cannot be streamed — embed them via `.add()`. */
456
+ // v8 ignore next 5 — async generator has an implicit "resume after yield"
457
+ // branch that is unreachable here because we always throw before yielding.
440
458
  async *stream(_shared, _params) {
441
459
  throw new Error(
442
460
  "Fragment cannot be streamed directly \u2014 use .add() to embed it in a FlowBuilder"