flowneer 0.1.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 +396 -0
- package/dist/index.d.ts +133 -0
- package/dist/index.js +218 -0
- package/dist/plugins/dev/index.d.ts +25 -0
- package/dist/plugins/dev/index.js +31 -0
- package/dist/plugins/index.d.ts +6 -0
- package/dist/plugins/index.js +265 -0
- package/dist/plugins/llm/index.d.ts +40 -0
- package/dist/plugins/llm/index.js +55 -0
- package/dist/plugins/observability/index.d.ts +30 -0
- package/dist/plugins/observability/index.js +56 -0
- package/dist/plugins/persistence/index.d.ts +51 -0
- package/dist/plugins/persistence/index.js +56 -0
- package/dist/plugins/resilience/index.d.ts +46 -0
- package/dist/plugins/resilience/index.js +71 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# Flowneer
|
|
2
|
+
|
|
3
|
+
A tiny, zero-dependency fluent flow builder for TypeScript. Chain steps, branch on conditions, loop, batch-process, and run tasks in parallel — all through a single `FlowBuilder` class. Extend it with plugins.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add flowneer
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { FlowBuilder } from "flowneer";
|
|
15
|
+
|
|
16
|
+
interface State {
|
|
17
|
+
count: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
await new FlowBuilder<State>()
|
|
21
|
+
.startWith(async (s) => {
|
|
22
|
+
s.count = 0;
|
|
23
|
+
})
|
|
24
|
+
.then(async (s) => {
|
|
25
|
+
s.count += 1;
|
|
26
|
+
})
|
|
27
|
+
.then(async (s) => {
|
|
28
|
+
console.log(s.count);
|
|
29
|
+
}) // 1
|
|
30
|
+
.run({ count: 0 });
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Every step receives a **shared state object** (`s`) that you mutate directly. That's the whole data model.
|
|
34
|
+
|
|
35
|
+
## API
|
|
36
|
+
|
|
37
|
+
### `startWith(fn, options?)`
|
|
38
|
+
|
|
39
|
+
Set the first step, resetting any prior chain.
|
|
40
|
+
|
|
41
|
+
### `then(fn, options?)`
|
|
42
|
+
|
|
43
|
+
Append a sequential step.
|
|
44
|
+
|
|
45
|
+
### `branch(router, branches, options?)`
|
|
46
|
+
|
|
47
|
+
Route to a named branch based on the return value of `router`.
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
interface AuthState {
|
|
51
|
+
role: string;
|
|
52
|
+
message: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await new FlowBuilder<AuthState>()
|
|
56
|
+
.startWith(async (s) => {
|
|
57
|
+
s.role = "admin";
|
|
58
|
+
})
|
|
59
|
+
.branch((s) => s.role, {
|
|
60
|
+
admin: async (s) => {
|
|
61
|
+
s.message = "Welcome, admin!";
|
|
62
|
+
},
|
|
63
|
+
guest: async (s) => {
|
|
64
|
+
s.message = "Limited access.";
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
.then(async (s) => console.log(s.message))
|
|
68
|
+
.run({ role: "", message: "" });
|
|
69
|
+
// → Welcome, admin!
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### `loop(condition, body)`
|
|
73
|
+
|
|
74
|
+
Repeat a sub-flow while `condition` returns `true`.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
interface TickState {
|
|
78
|
+
ticks: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await new FlowBuilder<TickState>()
|
|
82
|
+
.startWith(async (s) => {
|
|
83
|
+
s.ticks = 0;
|
|
84
|
+
})
|
|
85
|
+
.loop(
|
|
86
|
+
(s) => s.ticks < 3,
|
|
87
|
+
(b) =>
|
|
88
|
+
b.startWith(async (s) => {
|
|
89
|
+
s.ticks += 1;
|
|
90
|
+
}),
|
|
91
|
+
)
|
|
92
|
+
.then(async (s) => console.log("done, ticks =", s.ticks))
|
|
93
|
+
.run({ ticks: 0 });
|
|
94
|
+
// → done, ticks = 3
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `batch(items, processor)`
|
|
98
|
+
|
|
99
|
+
Run a sub-flow once per item. The current item is available as `shared.__batchItem`.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
interface SumState {
|
|
103
|
+
numbers: number[];
|
|
104
|
+
results: number[];
|
|
105
|
+
__batchItem?: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await new FlowBuilder<SumState>()
|
|
109
|
+
.startWith(async (s) => {
|
|
110
|
+
s.results = [];
|
|
111
|
+
})
|
|
112
|
+
.batch(
|
|
113
|
+
(s) => s.numbers,
|
|
114
|
+
(b) =>
|
|
115
|
+
b.startWith(async (s) => {
|
|
116
|
+
s.results.push((s.__batchItem ?? 0) * 2);
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
119
|
+
.then(async (s) => console.log(s.results))
|
|
120
|
+
.run({ numbers: [1, 2, 3], results: [] });
|
|
121
|
+
// → [2, 4, 6]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `parallel(fns, options?)`
|
|
125
|
+
|
|
126
|
+
Run multiple functions concurrently against the same shared state.
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
interface FetchState {
|
|
130
|
+
posts?: any[];
|
|
131
|
+
users?: any[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await new FlowBuilder<FetchState>()
|
|
135
|
+
.parallel([
|
|
136
|
+
async (s) => {
|
|
137
|
+
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
|
|
138
|
+
s.posts = await res.json();
|
|
139
|
+
},
|
|
140
|
+
async (s) => {
|
|
141
|
+
const res = await fetch("https://jsonplaceholder.typicode.com/users");
|
|
142
|
+
s.users = await res.json();
|
|
143
|
+
},
|
|
144
|
+
])
|
|
145
|
+
.then(async (s) => {
|
|
146
|
+
console.log(
|
|
147
|
+
"Fetched",
|
|
148
|
+
s.posts?.length,
|
|
149
|
+
"posts and",
|
|
150
|
+
s.users?.length,
|
|
151
|
+
"users",
|
|
152
|
+
);
|
|
153
|
+
})
|
|
154
|
+
.run({});
|
|
155
|
+
// → Fetched 100 posts and 10 users
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### `run(shared, params?, options?)`
|
|
159
|
+
|
|
160
|
+
Execute the flow. Optionally pass a `params` object that every step receives as a second argument.
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// Basic
|
|
164
|
+
await flow.run(shared);
|
|
165
|
+
|
|
166
|
+
// With params
|
|
167
|
+
await flow.run(shared, { userId: "123" });
|
|
168
|
+
|
|
169
|
+
// With AbortSignal — cancels between steps when the signal fires
|
|
170
|
+
const controller = new AbortController();
|
|
171
|
+
await flow.run(shared, undefined, { signal: controller.signal });
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Options
|
|
175
|
+
|
|
176
|
+
Any step that accepts `options` supports:
|
|
177
|
+
|
|
178
|
+
| Option | Default | Description |
|
|
179
|
+
| ----------- | ------- | ------------------------------------------------------ |
|
|
180
|
+
| `retries` | `1` | Number of attempts before throwing |
|
|
181
|
+
| `delaySec` | `0` | Seconds to wait between retries |
|
|
182
|
+
| `timeoutMs` | `0` | Milliseconds before the step is aborted (0 = no limit) |
|
|
183
|
+
|
|
184
|
+
## Error handling
|
|
185
|
+
|
|
186
|
+
When a step throws, the error is wrapped in a `FlowError` with the step index and type:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import { FlowBuilder, FlowError } from "flowneer";
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
await new FlowBuilder()
|
|
193
|
+
.startWith(async () => {})
|
|
194
|
+
.then(async () => {
|
|
195
|
+
throw new Error("boom");
|
|
196
|
+
})
|
|
197
|
+
.run({});
|
|
198
|
+
} catch (err) {
|
|
199
|
+
if (err instanceof FlowError) {
|
|
200
|
+
console.log(err.step); // "step 1"
|
|
201
|
+
console.log(err.cause); // Error: boom
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Errors inside `loop` and `batch` sub-flows are wrapped the same way:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
FlowError: Flow failed at loop (step 1): exploded on tick 2
|
|
210
|
+
FlowError: Flow failed at batch (step 0): bad item: 3
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Plugins
|
|
214
|
+
|
|
215
|
+
The core is intentionally small. Use `FlowBuilder.use(plugin)` to add chain methods.
|
|
216
|
+
|
|
217
|
+
A plugin is an object of functions that get copied onto `FlowBuilder.prototype`. Each function receives the builder as `this` and should return `this` for chaining.
|
|
218
|
+
|
|
219
|
+
### Writing a plugin
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
import type { FlowBuilder, FlowneerPlugin, StepMeta } from "flowneer";
|
|
223
|
+
|
|
224
|
+
// 1. Augment the FlowBuilder interface for type safety
|
|
225
|
+
declare module "flowneer" {
|
|
226
|
+
interface FlowBuilder<S, P> {
|
|
227
|
+
withTracing(fn: (meta: StepMeta, event: string) => void): this;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 2. Implement the plugin
|
|
232
|
+
export const observePlugin: FlowneerPlugin = {
|
|
233
|
+
withTracing(this: FlowBuilder<any, any>, fn) {
|
|
234
|
+
(this as any)._setHooks({
|
|
235
|
+
beforeStep: (meta: StepMeta) => fn(meta, "before"),
|
|
236
|
+
afterStep: (meta: StepMeta) => fn(meta, "after"),
|
|
237
|
+
onError: (meta: StepMeta) => fn(meta, "error"),
|
|
238
|
+
});
|
|
239
|
+
return this;
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Using a plugin
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { FlowBuilder } from "flowneer";
|
|
248
|
+
import { observePlugin } from "./observePlugin";
|
|
249
|
+
|
|
250
|
+
FlowBuilder.use(observePlugin); // one-time registration
|
|
251
|
+
|
|
252
|
+
const flow = new FlowBuilder<MyState>()
|
|
253
|
+
.withTracing((meta, event) => console.log(event, meta.type, meta.index))
|
|
254
|
+
.startWith(step1)
|
|
255
|
+
.then(step2);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Lifecycle hooks
|
|
259
|
+
|
|
260
|
+
Plugins register hooks via `_setHooks()`. Three hook points are available:
|
|
261
|
+
|
|
262
|
+
| Hook | Called | Arguments |
|
|
263
|
+
| ------------ | -------------------------------------------- | ------------------------------- |
|
|
264
|
+
| `beforeStep` | Before each step executes | `(meta, shared, params)` |
|
|
265
|
+
| `afterStep` | After each step completes | `(meta, shared, params)` |
|
|
266
|
+
| `onError` | When a step throws (before re-throwing) | `(meta, error, shared, params)` |
|
|
267
|
+
| `afterFlow` | After the flow finishes (success or failure) | `(shared, params)` |
|
|
268
|
+
|
|
269
|
+
### What plugins are for
|
|
270
|
+
|
|
271
|
+
| Concern | Example plugin | Hook it uses |
|
|
272
|
+
| --------------------------- | --------------- | -------------------------------------- |
|
|
273
|
+
| Observability / tracing | `observePlugin` | `beforeStep` + `afterStep` + `onError` |
|
|
274
|
+
| Persistence / checkpointing | `persistPlugin` | `afterStep` |
|
|
275
|
+
| Timing / metrics | custom | `beforeStep` + `afterStep` |
|
|
276
|
+
| Cleanup / teardown | custom | `afterFlow` |
|
|
277
|
+
|
|
278
|
+
See [examples/observePlugin.ts](examples/observePlugin.ts) and [examples/persistPlugin.ts](examples/persistPlugin.ts) for complete implementations.
|
|
279
|
+
|
|
280
|
+
## AI agent example
|
|
281
|
+
|
|
282
|
+
Flowneer's primitives map directly to common agent patterns:
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
import { FlowBuilder } from "flowneer";
|
|
286
|
+
|
|
287
|
+
interface AgentState {
|
|
288
|
+
question: string;
|
|
289
|
+
history: Message[];
|
|
290
|
+
intent?: string;
|
|
291
|
+
answer?: string;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const agent = new FlowBuilder<AgentState>()
|
|
295
|
+
.startWith(classifyIntent)
|
|
296
|
+
.branch((s) => s.intent, {
|
|
297
|
+
weather: fetchWeather,
|
|
298
|
+
joke: tellJoke,
|
|
299
|
+
default: generalAnswer,
|
|
300
|
+
})
|
|
301
|
+
.then(formatAndRespond);
|
|
302
|
+
|
|
303
|
+
await agent.run({ question: "What's the weather in Paris?", history: [] });
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
A ReAct-style loop:
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
const reactAgent = new FlowBuilder<AgentState>()
|
|
310
|
+
.startWith(think)
|
|
311
|
+
.loop(
|
|
312
|
+
(s) => !s.done,
|
|
313
|
+
(b) =>
|
|
314
|
+
b
|
|
315
|
+
.startWith(selectTool)
|
|
316
|
+
.branch(routeTool, {
|
|
317
|
+
search: webSearch,
|
|
318
|
+
code: runCode,
|
|
319
|
+
default: respond,
|
|
320
|
+
})
|
|
321
|
+
.then(observe),
|
|
322
|
+
)
|
|
323
|
+
.then(formatOutput);
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
See [examples/assistantFlow.ts](examples/assistantFlow.ts) for a full interactive agent.
|
|
327
|
+
|
|
328
|
+
### Agent-to-agent delegation
|
|
329
|
+
|
|
330
|
+
There is no special primitive for sub-agents — just call `anotherFlow.run(shared)` inside a `then`. Since `shared` is passed by reference, the sub-agent reads and writes the same state seamlessly:
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
const researchAgent = new FlowBuilder<ReportState>()
|
|
334
|
+
.startWith(searchWeb)
|
|
335
|
+
.then(summariseSources);
|
|
336
|
+
|
|
337
|
+
const writeAgent = new FlowBuilder<ReportState>()
|
|
338
|
+
.startWith(draftReport)
|
|
339
|
+
.then(formatMarkdown);
|
|
340
|
+
|
|
341
|
+
const orchestrator = new FlowBuilder<ReportState>()
|
|
342
|
+
.startWith(async (s) => {
|
|
343
|
+
s.query = "LLM benchmarks 2025";
|
|
344
|
+
})
|
|
345
|
+
.then(async (s) => researchAgent.run(s)) // delegate → sub-agent mutates s
|
|
346
|
+
.then(async (s) => writeAgent.run(s)) // delegate → sub-agent mutates s
|
|
347
|
+
.then(async (s) => console.log(s.report));
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Any number of flows can be composed this way. Each sub-agent is itself a `FlowBuilder`, so it can have its own retries, branches, and plugins.
|
|
351
|
+
|
|
352
|
+
### Parallel sub-agents
|
|
353
|
+
|
|
354
|
+
Use `parallel` when sub-agents are independent and can run concurrently:
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
const sentimentAgent = new FlowBuilder<AnalysisState>()
|
|
358
|
+
.startWith(classifySentiment)
|
|
359
|
+
.then(scoreSentiment);
|
|
360
|
+
|
|
361
|
+
const summaryAgent = new FlowBuilder<AnalysisState>()
|
|
362
|
+
.startWith(extractKeyPoints)
|
|
363
|
+
.then(writeSummary);
|
|
364
|
+
|
|
365
|
+
const toxicityAgent = new FlowBuilder<AnalysisState>().startWith(checkToxicity);
|
|
366
|
+
|
|
367
|
+
const orchestrator = new FlowBuilder<AnalysisState>()
|
|
368
|
+
.startWith(async (s) => {
|
|
369
|
+
s.text = "...input text...";
|
|
370
|
+
})
|
|
371
|
+
.parallel([
|
|
372
|
+
(s) => sentimentAgent.run(s), // writes s.sentiment
|
|
373
|
+
(s) => summaryAgent.run(s), // writes s.summary
|
|
374
|
+
(s) => toxicityAgent.run(s), // writes s.toxicity
|
|
375
|
+
])
|
|
376
|
+
.then(async (s) => {
|
|
377
|
+
console.log(s.sentiment, s.summary, s.toxicity);
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
All three sub-agents share the same `shared` object and run concurrently. Avoid writing to the same key from parallel sub-agents — writes are not synchronised.
|
|
382
|
+
|
|
383
|
+
## Project structure
|
|
384
|
+
|
|
385
|
+
```
|
|
386
|
+
Flowneer.ts Core — FlowBuilder, FlowError, types (~380 lines)
|
|
387
|
+
index.ts Public exports
|
|
388
|
+
examples/
|
|
389
|
+
assistantFlow.ts Interactive LLM assistant with branching
|
|
390
|
+
observePlugin.ts Tracing plugin example
|
|
391
|
+
persistPlugin.ts Checkpoint plugin example
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## License
|
|
395
|
+
|
|
396
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Function signature for all step logic.
|
|
3
|
+
* Return an action string to route, or undefined/void to continue.
|
|
4
|
+
*/
|
|
5
|
+
type NodeFn<S = any, P extends Record<string, unknown> = Record<string, unknown>> = (shared: S, params: P) => Promise<string | undefined | void> | string | undefined | void;
|
|
6
|
+
interface NodeOptions {
|
|
7
|
+
retries?: number;
|
|
8
|
+
delaySec?: number;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
interface RunOptions {
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}
|
|
14
|
+
interface FnStep<S, P extends Record<string, unknown>> {
|
|
15
|
+
type: "fn";
|
|
16
|
+
fn: NodeFn<S, P>;
|
|
17
|
+
retries: number;
|
|
18
|
+
delaySec: number;
|
|
19
|
+
timeoutMs: number;
|
|
20
|
+
}
|
|
21
|
+
interface BranchStep<S, P extends Record<string, unknown>> {
|
|
22
|
+
type: "branch";
|
|
23
|
+
router: NodeFn<S, P>;
|
|
24
|
+
branches: Record<string, NodeFn<S, P>>;
|
|
25
|
+
retries: number;
|
|
26
|
+
delaySec: number;
|
|
27
|
+
timeoutMs: number;
|
|
28
|
+
}
|
|
29
|
+
interface LoopStep<S, P extends Record<string, unknown>> {
|
|
30
|
+
type: "loop";
|
|
31
|
+
condition: (shared: S, params: P) => Promise<boolean> | boolean;
|
|
32
|
+
body: FlowBuilder<S, P>;
|
|
33
|
+
}
|
|
34
|
+
interface BatchStep<S, P extends Record<string, unknown>> {
|
|
35
|
+
type: "batch";
|
|
36
|
+
itemsExtractor: (shared: S, params: P) => Promise<any[]> | any[];
|
|
37
|
+
processor: FlowBuilder<S, P>;
|
|
38
|
+
}
|
|
39
|
+
interface ParallelStep<S, P extends Record<string, unknown>> {
|
|
40
|
+
type: "parallel";
|
|
41
|
+
fns: NodeFn<S, P>[];
|
|
42
|
+
retries: number;
|
|
43
|
+
delaySec: number;
|
|
44
|
+
timeoutMs: number;
|
|
45
|
+
}
|
|
46
|
+
type Step<S, P extends Record<string, unknown>> = FnStep<S, P> | BranchStep<S, P> | LoopStep<S, P> | BatchStep<S, P> | ParallelStep<S, P>;
|
|
47
|
+
/** Metadata exposed to hooks — intentionally minimal to avoid coupling. */
|
|
48
|
+
interface StepMeta {
|
|
49
|
+
index: number;
|
|
50
|
+
type: Step<any, any>["type"];
|
|
51
|
+
}
|
|
52
|
+
/** Lifecycle hooks that plugins can register. */
|
|
53
|
+
interface FlowHooks<S = any, P extends Record<string, unknown> = Record<string, unknown>> {
|
|
54
|
+
/** Fires once before the first step runs. */
|
|
55
|
+
beforeFlow?: (shared: S, params: P) => void | Promise<void>;
|
|
56
|
+
beforeStep?: (meta: StepMeta, shared: S, params: P) => void | Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Wraps step execution — call `next()` to invoke the step body.
|
|
59
|
+
* Omitting `next()` skips execution (dry-run, mock, etc.).
|
|
60
|
+
* Multiple `wrapStep` registrations are composed innermost-first.
|
|
61
|
+
*/
|
|
62
|
+
wrapStep?: (meta: StepMeta, next: () => Promise<void>, shared: S, params: P) => Promise<void>;
|
|
63
|
+
afterStep?: (meta: StepMeta, shared: S, params: P) => void | Promise<void>;
|
|
64
|
+
onError?: (meta: StepMeta, error: unknown, shared: S, params: P) => void;
|
|
65
|
+
afterFlow?: (shared: S, params: P) => void | Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A plugin is an object whose keys become methods on `FlowBuilder.prototype`.
|
|
69
|
+
* Each method receives the builder as `this` and should return `this` for chaining.
|
|
70
|
+
*
|
|
71
|
+
* Use declaration merging to get type-safe access:
|
|
72
|
+
* ```ts
|
|
73
|
+
* declare module "flowneer" {
|
|
74
|
+
* interface FlowBuilder<S, P> { withTracing(fn: TraceCallback): this; }
|
|
75
|
+
* }
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
type FlowneerPlugin = Record<string, (this: FlowBuilder<any, any>, ...args: any[]) => any>;
|
|
79
|
+
/** Wraps step failures with context about which step failed. */
|
|
80
|
+
declare class FlowError extends Error {
|
|
81
|
+
readonly step: string;
|
|
82
|
+
readonly cause: unknown;
|
|
83
|
+
constructor(step: string, cause: unknown);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Fluent builder for composable flows.
|
|
87
|
+
*
|
|
88
|
+
* Steps execute sequentially in the order added. Call `.run(shared)` to execute.
|
|
89
|
+
*
|
|
90
|
+
* **Shared-state safety**: all steps operate on the same shared object.
|
|
91
|
+
* Mutate it directly; avoid spreading/replacing the entire object.
|
|
92
|
+
*/
|
|
93
|
+
declare class FlowBuilder<S = any, P extends Record<string, unknown> = Record<string, unknown>> {
|
|
94
|
+
private steps;
|
|
95
|
+
private _hooksList;
|
|
96
|
+
/** Register a plugin — copies its methods onto `FlowBuilder.prototype`. */
|
|
97
|
+
static use(plugin: FlowneerPlugin): void;
|
|
98
|
+
/** Register lifecycle hooks (called by plugin methods, not by consumers). */
|
|
99
|
+
protected _setHooks(hooks: Partial<FlowHooks<S, P>>): void;
|
|
100
|
+
/** Set the first step, resetting any prior chain. */
|
|
101
|
+
startWith(fn: NodeFn<S, P>, options?: NodeOptions): this;
|
|
102
|
+
/** Append a sequential step. */
|
|
103
|
+
then(fn: NodeFn<S, P>, options?: NodeOptions): this;
|
|
104
|
+
/**
|
|
105
|
+
* Append a routing step.
|
|
106
|
+
* `router` returns a key; the matching branch flow executes, then the chain continues.
|
|
107
|
+
*/
|
|
108
|
+
branch(router: NodeFn<S, P>, branches: Record<string, NodeFn<S, P>>, options?: NodeOptions): this;
|
|
109
|
+
/**
|
|
110
|
+
* Append a looping step.
|
|
111
|
+
* Repeatedly runs `body` while `condition` returns true.
|
|
112
|
+
*/
|
|
113
|
+
loop(condition: (shared: S, params: P) => Promise<boolean> | boolean, body: (b: FlowBuilder<S, P>) => void): this;
|
|
114
|
+
/**
|
|
115
|
+
* Append a batch step.
|
|
116
|
+
* Runs `processor` once per item extracted by `items`, setting `shared.__batchItem` each time.
|
|
117
|
+
*/
|
|
118
|
+
batch(items: (shared: S, params: P) => Promise<any[]> | any[], processor: (b: FlowBuilder<S, P>) => void): this;
|
|
119
|
+
/**
|
|
120
|
+
* Append a parallel step.
|
|
121
|
+
* Runs all `fns` concurrently against the same shared state.
|
|
122
|
+
*/
|
|
123
|
+
parallel(fns: NodeFn<S, P>[], options?: NodeOptions): this;
|
|
124
|
+
/** Execute the flow. */
|
|
125
|
+
run(shared: S, params?: P, options?: RunOptions): Promise<void>;
|
|
126
|
+
protected _execute(shared: S, params: P, signal?: AbortSignal): Promise<void>;
|
|
127
|
+
private _addFn;
|
|
128
|
+
private _runSub;
|
|
129
|
+
private _retry;
|
|
130
|
+
private _withTimeout;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export { FlowBuilder, FlowError, type FlowHooks, type FlowneerPlugin, type NodeFn, type NodeOptions, type RunOptions, type StepMeta };
|