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 +55 -0
- package/dist/{FlowBuilder-Dqppgla9.d.ts → FlowBuilder-C3B91Fzo.d.ts} +17 -11
- package/dist/index.d.ts +1 -1
- package/dist/index.js +219 -201
- package/dist/patterns-C3oKxgYZ.d.ts +195 -0
- package/dist/plugins/agent/index.d.ts +129 -172
- package/dist/plugins/agent/index.js +386 -201
- package/dist/plugins/dev/index.d.ts +1 -1
- package/dist/plugins/eval/index.d.ts +1 -1
- package/dist/plugins/graph/index.d.ts +48 -2
- package/dist/plugins/graph/index.js +56 -0
- package/dist/plugins/index.d.ts +2 -2
- package/dist/plugins/index.js +219 -203
- package/dist/plugins/llm/index.d.ts +1 -1
- package/dist/plugins/memory/index.d.ts +1 -1
- package/dist/plugins/messaging/index.d.ts +1 -1
- package/dist/plugins/observability/index.d.ts +1 -1
- package/dist/plugins/output/index.d.ts +1 -1
- package/dist/plugins/persistence/index.d.ts +1 -1
- package/dist/plugins/resilience/index.d.ts +54 -2
- package/dist/plugins/resilience/index.js +77 -1
- package/dist/plugins/telemetry/index.d.ts +1 -1
- package/dist/plugins/tools/index.d.ts +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +219 -201
- package/package.json +3 -2
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
|
-
|
|
151
|
-
private
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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.
|
|
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
|
|
249
|
+
let notify = null;
|
|
197
250
|
const push = (event) => {
|
|
198
251
|
queue.push(event);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
resolve = null;
|
|
202
|
-
}
|
|
252
|
+
notify?.();
|
|
253
|
+
notify = null;
|
|
203
254
|
};
|
|
204
|
-
const
|
|
205
|
-
|
|
255
|
+
const drain = () => queue.length > 0 ? Promise.resolve() : new Promise((r) => {
|
|
256
|
+
notify = r;
|
|
206
257
|
});
|
|
207
258
|
this._setHooks({
|
|
208
|
-
beforeStep: (meta) => {
|
|
209
|
-
|
|
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
|
|
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.
|
|
238
|
-
const
|
|
239
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
379
|
-
throw new FlowError(
|
|
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"
|