yaml-flow 2.0.0 → 2.2.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 +332 -2
- package/dist/batch/index.cjs +109 -0
- package/dist/batch/index.cjs.map +1 -0
- package/dist/batch/index.d.cts +126 -0
- package/dist/batch/index.d.ts +126 -0
- package/dist/batch/index.js +107 -0
- package/dist/batch/index.js.map +1 -0
- package/dist/config/index.cjs +80 -0
- package/dist/config/index.cjs.map +1 -0
- package/dist/config/index.d.cts +71 -0
- package/dist/config/index.d.ts +71 -0
- package/dist/config/index.js +77 -0
- package/dist/config/index.js.map +1 -0
- package/dist/{constants-D1fTEbbM.d.ts → constants-Bwvkbr5s.d.cts} +128 -1
- package/dist/{constants-D1fTEbbM.d.cts → constants-Ewufm5cK.d.ts} +128 -1
- package/dist/event-graph/index.cjs +409 -0
- package/dist/event-graph/index.cjs.map +1 -1
- package/dist/event-graph/index.d.cts +3 -2
- package/dist/event-graph/index.d.ts +3 -2
- package/dist/event-graph/index.js +403 -1
- package/dist/event-graph/index.js.map +1 -1
- package/dist/index.cjs +590 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +581 -1
- package/dist/index.js.map +1 -1
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -509,11 +509,322 @@ interface StepMachineStore {
|
|
|
509
509
|
|
|
510
510
|
---
|
|
511
511
|
|
|
512
|
+
## Batch Processing
|
|
513
|
+
|
|
514
|
+
yaml-flow includes a `batch()` utility for running multiple items through a flow concurrently. It works with both Step Machine and Event Graph — you provide the processor, it manages concurrency.
|
|
515
|
+
|
|
516
|
+
### Quick Start
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
import { batch } from 'yaml-flow/batch';
|
|
520
|
+
import { createStepMachine } from 'yaml-flow/step-machine';
|
|
521
|
+
|
|
522
|
+
const tickets = [
|
|
523
|
+
{ id: 'T-001', message: 'Billing error' },
|
|
524
|
+
{ id: 'T-002', message: 'App crashes on login' },
|
|
525
|
+
{ id: 'T-003', message: 'Password reset help' },
|
|
526
|
+
];
|
|
527
|
+
|
|
528
|
+
const result = await batch(tickets, {
|
|
529
|
+
concurrency: 3,
|
|
530
|
+
processor: async (ticket) => {
|
|
531
|
+
const machine = createStepMachine(flow, handlers);
|
|
532
|
+
return machine.run({ message: ticket.message });
|
|
533
|
+
},
|
|
534
|
+
onProgress: (p) => console.log(`${p.percent}% done`),
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
console.log(`${result.completed} succeeded, ${result.failed} failed`);
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Options
|
|
541
|
+
|
|
542
|
+
| Option | Type | Default | Description |
|
|
543
|
+
|---|---|---|---|
|
|
544
|
+
| `concurrency` | `number` | `5` | Max parallel processors |
|
|
545
|
+
| `processor` | `(item, index) => Promise<TResult>` | *required* | Async function to process each item |
|
|
546
|
+
| `signal` | `AbortSignal` | — | Cancel remaining items |
|
|
547
|
+
| `onItemComplete` | `(item, result, index) => void` | — | Called when an item succeeds |
|
|
548
|
+
| `onItemError` | `(item, error, index) => void` | — | Called when an item fails |
|
|
549
|
+
| `onProgress` | `(progress) => void` | — | Called after each item with `{ completed, failed, active, pending, total, percent, elapsedMs }` |
|
|
550
|
+
|
|
551
|
+
### Result Shape
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
{
|
|
555
|
+
items: BatchItemResult[]; // Per-item: { item, index, status, result?, error?, durationMs }
|
|
556
|
+
completed: number; // Items that succeeded
|
|
557
|
+
failed: number; // Items that threw
|
|
558
|
+
total: number;
|
|
559
|
+
durationMs: number; // Wall-clock time for entire batch
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Works with Event Graph too
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
import { batch } from 'yaml-flow/batch';
|
|
567
|
+
import { next, apply, createInitialExecutionState } from 'yaml-flow/event-graph';
|
|
568
|
+
|
|
569
|
+
const result = await batch(items, {
|
|
570
|
+
concurrency: 5,
|
|
571
|
+
processor: async (item) => {
|
|
572
|
+
let state = createInitialExecutionState(graph, `run-${item.id}`);
|
|
573
|
+
state = apply(state, { type: 'inject-tokens', tokens: [item.readyToken], timestamp: Date.now() }, graph);
|
|
574
|
+
// ... drive graph loop with next() + apply()
|
|
575
|
+
return state;
|
|
576
|
+
},
|
|
577
|
+
});
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
---
|
|
581
|
+
|
|
582
|
+
## Config Utilities
|
|
583
|
+
|
|
584
|
+
Pure pre-processing transforms you apply before passing config to the engine. They never touch engine state — just config in, config out.
|
|
585
|
+
|
|
586
|
+
### Variable Interpolation
|
|
587
|
+
|
|
588
|
+
Replace `${KEY}` patterns in any config object. Works with both GraphConfig and StepFlowConfig.
|
|
589
|
+
|
|
590
|
+
```typescript
|
|
591
|
+
import { resolveVariables } from 'yaml-flow/config';
|
|
592
|
+
|
|
593
|
+
const resolved = resolveVariables(graphConfig, {
|
|
594
|
+
ENTITY_ID: 'ticket-42',
|
|
595
|
+
TOOLS_DIR: '/opt/tools',
|
|
596
|
+
WORKDIR: '/data/workdata',
|
|
597
|
+
});
|
|
598
|
+
// Every ${ENTITY_ID} in task configs, cmd-args, etc. → replaced
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### Config Templates
|
|
602
|
+
|
|
603
|
+
DRY reusable config blocks. Tasks reference a named template via `config-template`; the function deep-merges template + task overrides and removes the reference.
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
import { resolveConfigTemplates } from 'yaml-flow/config';
|
|
607
|
+
|
|
608
|
+
const config = {
|
|
609
|
+
configTemplates: { // or 'config-templates' (kebab-case)
|
|
610
|
+
PYTHON_TOOL: { cmd: 'python', timeout: 30000, cwd: '/workdata' },
|
|
611
|
+
NODE_CMD: { cmd: 'node', timeout: 60000 },
|
|
612
|
+
},
|
|
613
|
+
tasks: {
|
|
614
|
+
analyze: { provides: ['analysis'], config: { 'config-template': 'PYTHON_TOOL', 'cmd-args': 'analyze.py' } },
|
|
615
|
+
build: { provides: ['build'], config: { 'config-template': 'NODE_CMD', script: 'build.js' } },
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const resolved = resolveConfigTemplates(config);
|
|
620
|
+
// analyze.config → { cmd: 'python', timeout: 30000, cwd: '/workdata', 'cmd-args': 'analyze.py' }
|
|
621
|
+
// configTemplates key removed from output
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### Composing Both
|
|
625
|
+
|
|
626
|
+
Templates first (expands references), then variables (fills in `${...}` placeholders):
|
|
627
|
+
|
|
628
|
+
```typescript
|
|
629
|
+
import { resolveConfigTemplates, resolveVariables } from 'yaml-flow/config';
|
|
630
|
+
|
|
631
|
+
const raw = loadYaml('pipeline.yaml'); // has configTemplates + ${VAR} refs
|
|
632
|
+
const resolved = resolveVariables(
|
|
633
|
+
resolveConfigTemplates(raw),
|
|
634
|
+
{ ENTITY_ID: 'url-42', TOOLS_DIR: '/opt/tools' },
|
|
635
|
+
);
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
## Graph-of-Graphs Pattern
|
|
641
|
+
|
|
642
|
+
Real-world pipelines are often **layered**: an outer orchestration graph where some tasks are themselves entire sub-workflows — each processing a batch of items through their own DAG or step flow. yaml-flow doesn't bake this into the engine (the pure scheduler stays simple), but the primitives compose cleanly.
|
|
643
|
+
|
|
644
|
+
### The Shape
|
|
645
|
+
|
|
646
|
+
```
|
|
647
|
+
Outer graph (event-graph)
|
|
648
|
+
├── prep-workdata → plain task
|
|
649
|
+
├── copy-input-files → plain task
|
|
650
|
+
├── evidence-gathering → batch × inner event-graph (N items, 5 concurrent)
|
|
651
|
+
├── grade-synthesis → batch × inner step-machine (N items, 3 concurrent)
|
|
652
|
+
├── analyze-mismatches → plain task
|
|
653
|
+
├── [health-check ∥ report]→ parallel plain tasks
|
|
654
|
+
└── archive-results → waits for both
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
The outer graph sequences coarse stages. Some stages fan out into batches where each item runs through its own sub-workflow. The sub can be either mode.
|
|
658
|
+
|
|
659
|
+
### How to Wire It
|
|
660
|
+
|
|
661
|
+
Each "sub-graph task" in the outer graph is just a handler that composes `resolveConfigTemplates` → `resolveVariables` → `batch` → engine:
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
import { next, apply, createInitialExecutionState } from 'yaml-flow/event-graph';
|
|
665
|
+
import { createStepMachine } from 'yaml-flow/step-machine';
|
|
666
|
+
import { batch } from 'yaml-flow/batch';
|
|
667
|
+
import { resolveVariables, resolveConfigTemplates } from 'yaml-flow/config';
|
|
668
|
+
|
|
669
|
+
// Outer graph handler for a sub-graph task (event-graph sub)
|
|
670
|
+
async function runEvidenceBatch(items, rawSubConfig) {
|
|
671
|
+
return batch(items, {
|
|
672
|
+
concurrency: 5,
|
|
673
|
+
processor: async (item) => {
|
|
674
|
+
// Resolve config per-item (each item gets its own ENTITY_ID)
|
|
675
|
+
const config = resolveVariables(
|
|
676
|
+
resolveConfigTemplates(rawSubConfig),
|
|
677
|
+
{ ENTITY_ID: item.id, TOOLS_DIR: '/opt/tools' },
|
|
678
|
+
);
|
|
679
|
+
// Drive the inner event-graph
|
|
680
|
+
let state = createInitialExecutionState(config, `run-${item.id}`);
|
|
681
|
+
while (true) {
|
|
682
|
+
const { eligibleTasks, isComplete } = next(config, state);
|
|
683
|
+
if (isComplete) break;
|
|
684
|
+
for (const task of eligibleTasks) {
|
|
685
|
+
state = apply(state, { type: 'task-started', taskName: task, timestamp: new Date().toISOString() }, config);
|
|
686
|
+
const result = await executeTask(task, config.tasks[task], item);
|
|
687
|
+
state = apply(state, { type: 'task-completed', taskName: task, result, timestamp: new Date().toISOString() }, config);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return state;
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Outer graph handler for a sub-graph task (step-machine sub)
|
|
696
|
+
async function runGradeBatch(items, flowConfig, handlers) {
|
|
697
|
+
return batch(items, {
|
|
698
|
+
concurrency: 3,
|
|
699
|
+
processor: async (item) => {
|
|
700
|
+
const machine = createStepMachine(flowConfig, handlers);
|
|
701
|
+
return machine.run({ entityId: item.id, evidence: item.evidence });
|
|
702
|
+
},
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
### Driving the Outer Graph
|
|
708
|
+
|
|
709
|
+
The outer graph itself is an event-graph. Each handler maps to a task:
|
|
710
|
+
|
|
711
|
+
```typescript
|
|
712
|
+
const outerHandlers = {
|
|
713
|
+
'prep-workdata': async () => { /* setup */ },
|
|
714
|
+
'copy-input-files': async () => { /* parse CSV, return items */ },
|
|
715
|
+
'evidence-batch': async (ctx) => runEvidenceBatch(ctx.items, evidenceConfig),
|
|
716
|
+
'grade-batch': async (ctx) => runGradeBatch(ctx.items, gradeFlow, gradeHandlers),
|
|
717
|
+
'analyze-mismatches':async (ctx) => { /* compare grades */ },
|
|
718
|
+
'health-check': async (ctx) => { /* validate */ },
|
|
719
|
+
'generate-report': async (ctx) => { /* summarize */ },
|
|
720
|
+
'archive': async (ctx) => { /* move outputs */ },
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
// Simple outer loop
|
|
724
|
+
let state = createInitialExecutionState(outerGraph, 'pipeline-run-1');
|
|
725
|
+
while (true) {
|
|
726
|
+
const { eligibleTasks, isComplete } = next(outerGraph, state);
|
|
727
|
+
if (isComplete) break;
|
|
728
|
+
await Promise.all(eligibleTasks.map(async (taskName) => {
|
|
729
|
+
state = apply(state, { type: 'task-started', taskName, timestamp: now() }, outerGraph);
|
|
730
|
+
try {
|
|
731
|
+
await outerHandlers[taskName](context);
|
|
732
|
+
state = apply(state, { type: 'task-completed', taskName, timestamp: now() }, outerGraph);
|
|
733
|
+
} catch (err) {
|
|
734
|
+
state = apply(state, { type: 'task-failed', taskName, error: err.message, timestamp: now() }, outerGraph);
|
|
735
|
+
}
|
|
736
|
+
}));
|
|
737
|
+
}
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
### Why Not Bake It Into the Engine?
|
|
741
|
+
|
|
742
|
+
- The pure scheduler (`next`/`apply`) stays a simple `f(state, event) → newState`.
|
|
743
|
+
- Sub-graph execution involves file I/O, process spawning, HTTP calls — all driver concerns.
|
|
744
|
+
- Every deployment customizes how sub-tasks execute: in-process, `execSync`, HTTP, serverless.
|
|
745
|
+
- The primitives (`batch` + `resolveVariables` + `resolveConfigTemplates` + both engines) compose without coupling.
|
|
746
|
+
|
|
747
|
+
See the [examples/graph-of-graphs/](./examples/graph-of-graphs/) directory for complete runnable examples.
|
|
748
|
+
|
|
749
|
+
---
|
|
750
|
+
|
|
751
|
+
## Execution Plan (Dry Run)
|
|
752
|
+
|
|
753
|
+
Compute the full execution plan from a graph config without running anything — like `terraform plan` for workflows.
|
|
754
|
+
|
|
755
|
+
```typescript
|
|
756
|
+
import { planExecution } from 'yaml-flow/event-graph';
|
|
757
|
+
|
|
758
|
+
const plan = planExecution(graph);
|
|
759
|
+
|
|
760
|
+
plan.phases; // [['prep'], ['copy'], ['evidence'], ['synthesis'], ['analyze'], ['health', 'report'], ['archive']]
|
|
761
|
+
plan.depth; // 7
|
|
762
|
+
plan.maxParallelism; // 2
|
|
763
|
+
plan.entryPoints; // ['prep']
|
|
764
|
+
plan.leafTasks; // ['archive']
|
|
765
|
+
plan.conflicts; // { 'output-token': ['task-a', 'task-b'] } — multiple producers
|
|
766
|
+
plan.unreachableTokens; // ['human-approval'] — required but no task produces it
|
|
767
|
+
plan.blockedTasks; // ['approve'] — blocked by unreachable tokens
|
|
768
|
+
plan.dependencies; // { 'copy': ['prep'], 'evidence': ['copy'], ... }
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
---
|
|
772
|
+
|
|
773
|
+
## Mermaid Diagrams
|
|
774
|
+
|
|
775
|
+
Generate Mermaid syntax from any config — useful for docs, debugging, and CI reports.
|
|
776
|
+
|
|
777
|
+
```typescript
|
|
778
|
+
import { graphToMermaid, flowToMermaid } from 'yaml-flow/event-graph';
|
|
779
|
+
|
|
780
|
+
// Event graph → dependency diagram
|
|
781
|
+
console.log(graphToMermaid(graph));
|
|
782
|
+
// graph TD
|
|
783
|
+
// build([build])
|
|
784
|
+
// test[test]
|
|
785
|
+
// deploy[[deploy]]
|
|
786
|
+
// build -->|artifact| test
|
|
787
|
+
// test -->|tested| deploy
|
|
788
|
+
|
|
789
|
+
// Step machine → flowchart
|
|
790
|
+
console.log(flowToMermaid(flow));
|
|
791
|
+
// graph TD
|
|
792
|
+
// START(( ))
|
|
793
|
+
// START --> classify
|
|
794
|
+
// classify -->|billing| handle
|
|
795
|
+
// handle -->|resolved| done
|
|
796
|
+
// done([done: resolved])
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
Options: `{ direction: 'LR' | 'TD', showTokens: boolean, title: string }`.
|
|
800
|
+
Entry points (no requires) get rounded shapes, leaf tasks get double-bracketed shapes, unreachable deps get warning markers.
|
|
801
|
+
|
|
802
|
+
---
|
|
803
|
+
|
|
804
|
+
## Loading & Exporting Graph Configs
|
|
805
|
+
|
|
806
|
+
```typescript
|
|
807
|
+
import { loadGraphConfig, exportGraphConfig, exportGraphConfigToFile } from 'yaml-flow/event-graph';
|
|
808
|
+
|
|
809
|
+
// Load from file, URL, JSON string, or object (validates automatically)
|
|
810
|
+
const graph = await loadGraphConfig('./pipeline.yaml');
|
|
811
|
+
const graph2 = await loadGraphConfig('https://example.com/graph.json');
|
|
812
|
+
|
|
813
|
+
// Export to string
|
|
814
|
+
const json = exportGraphConfig(graph); // JSON (default)
|
|
815
|
+
const yaml = exportGraphConfig(graph, { format: 'yaml' }); // YAML
|
|
816
|
+
|
|
817
|
+
// Export to file (format auto-detected from extension)
|
|
818
|
+
await exportGraphConfigToFile(graph, './output/pipeline.yaml');
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
---
|
|
822
|
+
|
|
512
823
|
## Package Exports
|
|
513
824
|
|
|
514
825
|
```typescript
|
|
515
|
-
// Everything (both modes + stores)
|
|
516
|
-
import { StepMachine, next, apply, MemoryStore } from 'yaml-flow';
|
|
826
|
+
// Everything (both modes + stores + batch)
|
|
827
|
+
import { StepMachine, next, apply, MemoryStore, batch } from 'yaml-flow';
|
|
517
828
|
|
|
518
829
|
// Step Machine only
|
|
519
830
|
import { StepMachine, createStepMachine, loadStepFlow } from 'yaml-flow/step-machine';
|
|
@@ -522,11 +833,20 @@ import { applyStepResult, checkCircuitBreaker, createInitialState } from 'yaml-f
|
|
|
522
833
|
// Event Graph only
|
|
523
834
|
import { next, apply, applyAll, getCandidateTasks } from 'yaml-flow/event-graph';
|
|
524
835
|
import { createInitialExecutionState, isExecutionComplete, detectStuckState } from 'yaml-flow/event-graph';
|
|
836
|
+
import { planExecution, graphToMermaid, flowToMermaid } from 'yaml-flow/event-graph';
|
|
837
|
+
import { loadGraphConfig, validateGraphConfig, exportGraphConfig } from 'yaml-flow/event-graph';
|
|
525
838
|
import { TASK_STATUS, COMPLETION_STRATEGIES, CONFLICT_STRATEGIES } from 'yaml-flow/event-graph';
|
|
526
839
|
|
|
527
840
|
// Stores
|
|
528
841
|
import { MemoryStore, LocalStorageStore, FileStore } from 'yaml-flow/stores';
|
|
529
842
|
|
|
843
|
+
// Batch
|
|
844
|
+
import { batch } from 'yaml-flow/batch';
|
|
845
|
+
import type { BatchOptions, BatchResult, BatchItemResult, BatchProgress } from 'yaml-flow/batch';
|
|
846
|
+
|
|
847
|
+
// Config utilities
|
|
848
|
+
import { resolveVariables, resolveConfigTemplates } from 'yaml-flow/config';
|
|
849
|
+
|
|
530
850
|
// Backward compatibility (v1 names → v2)
|
|
531
851
|
import { FlowEngine, createEngine } from 'yaml-flow'; // aliases for StepMachine, createStepMachine
|
|
532
852
|
```
|
|
@@ -562,6 +882,13 @@ import { FlowEngine, createEngine } from 'yaml-flow'; // aliases for StepMachin
|
|
|
562
882
|
| `isExecutionComplete(graph, state)` | Check completion against configured strategy |
|
|
563
883
|
| `detectStuckState({graph, state, ...})` | Check if execution is stuck |
|
|
564
884
|
| `addDynamicTask(graph, name, config)` | Immutably add a task to a graph config |
|
|
885
|
+
| `planExecution(graph)` | Dry-run: compute phases, parallelism, conflicts, unreachable tokens |
|
|
886
|
+
| `graphToMermaid(graph, options?)` | Generate Mermaid dependency diagram from an event-graph |
|
|
887
|
+
| `flowToMermaid(flow, options?)` | Generate Mermaid flowchart from a step-machine |
|
|
888
|
+
| `loadGraphConfig(source)` | Load + validate a YAML/JSON/URL graph config |
|
|
889
|
+
| `validateGraphConfig(config)` | Validate a GraphConfig, returns error strings |
|
|
890
|
+
| `exportGraphConfig(config, options?)` | Export a GraphConfig to JSON or YAML string |
|
|
891
|
+
| `exportGraphConfigToFile(config, path)` | Export a GraphConfig to a file |
|
|
565
892
|
|
|
566
893
|
### Event Types (for `apply()`)
|
|
567
894
|
|
|
@@ -587,6 +914,9 @@ See the [examples/](./examples) directory:
|
|
|
587
914
|
| [AI Conversation](./examples/node/ai-conversation.ts) | Step Machine | Retry, circuit breakers, component injection |
|
|
588
915
|
| [Research Pipeline](./examples/event-graph/research-pipeline.ts) | Event Graph | Parallel tasks, goal-based completion |
|
|
589
916
|
| [CI/CD Pipeline](./examples/event-graph/ci-cd-pipeline.ts) | Event Graph | External events, conditional routing, failure tokens |
|
|
917
|
+
| [Batch Tickets](./examples/batch/batch-step-machine.ts) | Batch | Concurrent processing, progress tracking |
|
|
918
|
+
| [URL Pipeline](./examples/graph-of-graphs/url-processing-pipeline.ts) | Graph-of-Graphs | Outer event-graph → batch × inner event-graph per item |
|
|
919
|
+
| [Multi-Stage ETL](./examples/graph-of-graphs/multi-stage-etl.ts) | Graph-of-Graphs | Mixed modes: event-graph outer → step-machine + event-graph subs |
|
|
590
920
|
| [Order Processing](./examples/flows/order-processing.yaml) | Step Machine | YAML flow definition |
|
|
591
921
|
| [Browser Demo](./examples/browser/index.html) | Step Machine | In-browser usage |
|
|
592
922
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/batch/runner.ts
|
|
4
|
+
async function batch(items, options) {
|
|
5
|
+
const {
|
|
6
|
+
concurrency = 5,
|
|
7
|
+
processor,
|
|
8
|
+
onItemComplete,
|
|
9
|
+
onItemError,
|
|
10
|
+
onProgress,
|
|
11
|
+
signal
|
|
12
|
+
} = options;
|
|
13
|
+
const total = items.length;
|
|
14
|
+
const results = new Array(total);
|
|
15
|
+
const batchStart = Date.now();
|
|
16
|
+
let completed = 0;
|
|
17
|
+
let failed = 0;
|
|
18
|
+
let nextIndex = 0;
|
|
19
|
+
function makeProgress(active) {
|
|
20
|
+
const done = completed + failed;
|
|
21
|
+
return {
|
|
22
|
+
completed,
|
|
23
|
+
failed,
|
|
24
|
+
active,
|
|
25
|
+
pending: total - done - active,
|
|
26
|
+
total,
|
|
27
|
+
percent: total === 0 ? 100 : Math.round(done / total * 100),
|
|
28
|
+
elapsedMs: Date.now() - batchStart
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (total === 0) {
|
|
32
|
+
return { items: [], completed: 0, failed: 0, total: 0, durationMs: 0 };
|
|
33
|
+
}
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
let active = 0;
|
|
36
|
+
function tryStartNext() {
|
|
37
|
+
while (active < concurrency && nextIndex < total) {
|
|
38
|
+
if (signal?.aborted) {
|
|
39
|
+
while (nextIndex < total) {
|
|
40
|
+
const idx2 = nextIndex++;
|
|
41
|
+
results[idx2] = {
|
|
42
|
+
item: items[idx2],
|
|
43
|
+
index: idx2,
|
|
44
|
+
status: "failed",
|
|
45
|
+
error: new Error("Batch aborted"),
|
|
46
|
+
durationMs: 0
|
|
47
|
+
};
|
|
48
|
+
failed++;
|
|
49
|
+
}
|
|
50
|
+
if (active === 0 && completed + failed === total) {
|
|
51
|
+
resolve({
|
|
52
|
+
items: results,
|
|
53
|
+
completed,
|
|
54
|
+
failed,
|
|
55
|
+
total,
|
|
56
|
+
durationMs: Date.now() - batchStart
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
const idx = nextIndex++;
|
|
62
|
+
const item = items[idx];
|
|
63
|
+
active++;
|
|
64
|
+
const itemStart = Date.now();
|
|
65
|
+
processor(item, idx).then((result) => {
|
|
66
|
+
results[idx] = {
|
|
67
|
+
item,
|
|
68
|
+
index: idx,
|
|
69
|
+
status: "completed",
|
|
70
|
+
result,
|
|
71
|
+
durationMs: Date.now() - itemStart
|
|
72
|
+
};
|
|
73
|
+
completed++;
|
|
74
|
+
onItemComplete?.(item, result, idx);
|
|
75
|
+
}).catch((err) => {
|
|
76
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
77
|
+
results[idx] = {
|
|
78
|
+
item,
|
|
79
|
+
index: idx,
|
|
80
|
+
status: "failed",
|
|
81
|
+
error,
|
|
82
|
+
durationMs: Date.now() - itemStart
|
|
83
|
+
};
|
|
84
|
+
failed++;
|
|
85
|
+
onItemError?.(item, error, idx);
|
|
86
|
+
}).finally(() => {
|
|
87
|
+
active--;
|
|
88
|
+
onProgress?.(makeProgress(active));
|
|
89
|
+
if (completed + failed === total) {
|
|
90
|
+
resolve({
|
|
91
|
+
items: results,
|
|
92
|
+
completed,
|
|
93
|
+
failed,
|
|
94
|
+
total,
|
|
95
|
+
durationMs: Date.now() - batchStart
|
|
96
|
+
});
|
|
97
|
+
} else {
|
|
98
|
+
tryStartNext();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
tryStartNext();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
exports.batch = batch;
|
|
108
|
+
//# sourceMappingURL=index.cjs.map
|
|
109
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/batch/runner.ts"],"names":["idx"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Batch Runner — Core\n *\n * Slot-based concurrent processor. Pure control flow — no I/O opinions.\n *\n * @example Step Machine batch\n * ```ts\n * import { batch } from 'yaml-flow/batch';\n * import { createStepMachine, loadStepFlow } from 'yaml-flow/step-machine';\n *\n * const flow = await loadStepFlow('./support-ticket.yaml');\n * const results = await batch(tickets, {\n * concurrency: 5,\n * processor: async (ticket) => {\n * const machine = createStepMachine(flow, handlers);\n * return machine.run(ticket);\n * },\n * });\n * ```\n *\n * @example Event Graph batch\n * ```ts\n * import { batch } from 'yaml-flow/batch';\n * import { next, apply, createInitialExecutionState } from 'yaml-flow/event-graph';\n *\n * const results = await batch(items, {\n * concurrency: 3,\n * processor: async (item, index) => {\n * let state = createInitialExecutionState(graph, `exec-${index}`);\n * state = apply(state, { type: 'inject-tokens', tokens: [item.token], timestamp: new Date().toISOString() }, graph);\n * // ... drive the graph loop\n * return state;\n * },\n * });\n * ```\n */\n\nimport type {\n BatchOptions,\n BatchResult,\n BatchItemResult,\n BatchProgress,\n} from './types.js';\n\n/**\n * Run an array of items through an async processor with concurrency control.\n *\n * - Items are started in order, up to `concurrency` at a time.\n * - Results are returned in the original item order.\n * - If a processor throws, the item is marked as failed; other items continue.\n * - An AbortSignal prevents new items from starting (in-flight items are not cancelled).\n */\nexport async function batch<TItem, TResult>(\n items: TItem[],\n options: BatchOptions<TItem, TResult>\n): Promise<BatchResult<TItem, TResult>> {\n const {\n concurrency = 5,\n processor,\n onItemComplete,\n onItemError,\n onProgress,\n signal,\n } = options;\n\n const total = items.length;\n const results: BatchItemResult<TItem, TResult>[] = new Array(total);\n const batchStart = Date.now();\n\n let completed = 0;\n let failed = 0;\n let nextIndex = 0;\n\n function makeProgress(active: number): BatchProgress {\n const done = completed + failed;\n return {\n completed,\n failed,\n active,\n pending: total - done - active,\n total,\n percent: total === 0 ? 100 : Math.round((done / total) * 100),\n elapsedMs: Date.now() - batchStart,\n };\n }\n\n // Empty input — short-circuit\n if (total === 0) {\n return { items: [], completed: 0, failed: 0, total: 0, durationMs: 0 };\n }\n\n return new Promise<BatchResult<TItem, TResult>>((resolve) => {\n let active = 0;\n\n function tryStartNext() {\n while (active < concurrency && nextIndex < total) {\n // Respect abort signal — don't start new items\n if (signal?.aborted) {\n // Mark remaining as failed with abort error\n while (nextIndex < total) {\n const idx = nextIndex++;\n results[idx] = {\n item: items[idx],\n index: idx,\n status: 'failed',\n error: new Error('Batch aborted'),\n durationMs: 0,\n };\n failed++;\n }\n // If nothing is in-flight, resolve immediately\n if (active === 0 && completed + failed === total) {\n resolve({\n items: results,\n completed,\n failed,\n total,\n durationMs: Date.now() - batchStart,\n });\n }\n break;\n }\n\n const idx = nextIndex++;\n const item = items[idx];\n active++;\n const itemStart = Date.now();\n\n processor(item, idx)\n .then((result) => {\n results[idx] = {\n item,\n index: idx,\n status: 'completed',\n result,\n durationMs: Date.now() - itemStart,\n };\n completed++;\n onItemComplete?.(item, result, idx);\n })\n .catch((err) => {\n const error = err instanceof Error ? err : new Error(String(err));\n results[idx] = {\n item,\n index: idx,\n status: 'failed',\n error,\n durationMs: Date.now() - itemStart,\n };\n failed++;\n onItemError?.(item, error, idx);\n })\n .finally(() => {\n active--;\n onProgress?.(makeProgress(active));\n\n if (completed + failed === total) {\n resolve({\n items: results,\n completed,\n failed,\n total,\n durationMs: Date.now() - batchStart,\n });\n } else {\n tryStartNext();\n }\n });\n }\n }\n\n tryStartNext();\n });\n}\n"]}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batch Runner — Types
|
|
3
|
+
*
|
|
4
|
+
* Generic concurrent batch processor.
|
|
5
|
+
* Works with both step-machine and event-graph (or any async processor).
|
|
6
|
+
*/
|
|
7
|
+
interface BatchOptions<TItem, TResult> {
|
|
8
|
+
/**
|
|
9
|
+
* Max concurrent items in flight (slots).
|
|
10
|
+
* @default 5
|
|
11
|
+
*/
|
|
12
|
+
concurrency?: number;
|
|
13
|
+
/**
|
|
14
|
+
* The async function that processes a single item.
|
|
15
|
+
* Receives the item and its 0-based index.
|
|
16
|
+
*/
|
|
17
|
+
processor: (item: TItem, index: number) => Promise<TResult>;
|
|
18
|
+
/**
|
|
19
|
+
* Called when a single item completes successfully.
|
|
20
|
+
*/
|
|
21
|
+
onItemComplete?: (item: TItem, result: TResult, index: number) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Called when a single item fails (processor threw).
|
|
24
|
+
* If not provided, the error is captured in BatchItemResult.
|
|
25
|
+
*/
|
|
26
|
+
onItemError?: (item: TItem, error: Error, index: number) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Called after every item settles (success or failure).
|
|
29
|
+
* Receives a snapshot of progress.
|
|
30
|
+
*/
|
|
31
|
+
onProgress?: (progress: BatchProgress) => void;
|
|
32
|
+
/**
|
|
33
|
+
* AbortSignal — if aborted, no new items are started.
|
|
34
|
+
* Items already in-flight are NOT cancelled (your processor should check its own signal).
|
|
35
|
+
*/
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
}
|
|
38
|
+
interface BatchProgress {
|
|
39
|
+
/** Items completed successfully so far */
|
|
40
|
+
completed: number;
|
|
41
|
+
/** Items that threw an error */
|
|
42
|
+
failed: number;
|
|
43
|
+
/** Items currently in-flight */
|
|
44
|
+
active: number;
|
|
45
|
+
/** Items not yet started */
|
|
46
|
+
pending: number;
|
|
47
|
+
/** Total items */
|
|
48
|
+
total: number;
|
|
49
|
+
/** Percentage complete (0–100) */
|
|
50
|
+
percent: number;
|
|
51
|
+
/** Elapsed time in ms since batch started */
|
|
52
|
+
elapsedMs: number;
|
|
53
|
+
}
|
|
54
|
+
interface BatchResult<TItem, TResult> {
|
|
55
|
+
/** All item results in original order */
|
|
56
|
+
items: BatchItemResult<TItem, TResult>[];
|
|
57
|
+
/** Summary counts */
|
|
58
|
+
completed: number;
|
|
59
|
+
failed: number;
|
|
60
|
+
total: number;
|
|
61
|
+
/** Total wall-clock time in ms */
|
|
62
|
+
durationMs: number;
|
|
63
|
+
}
|
|
64
|
+
interface BatchItemResult<TItem, TResult> {
|
|
65
|
+
/** Original item */
|
|
66
|
+
item: TItem;
|
|
67
|
+
/** 0-based index in the input array */
|
|
68
|
+
index: number;
|
|
69
|
+
/** 'completed' or 'failed' */
|
|
70
|
+
status: 'completed' | 'failed';
|
|
71
|
+
/** Result if completed */
|
|
72
|
+
result?: TResult;
|
|
73
|
+
/** Error if failed */
|
|
74
|
+
error?: Error;
|
|
75
|
+
/** Per-item wall-clock time in ms */
|
|
76
|
+
durationMs: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Batch Runner — Core
|
|
81
|
+
*
|
|
82
|
+
* Slot-based concurrent processor. Pure control flow — no I/O opinions.
|
|
83
|
+
*
|
|
84
|
+
* @example Step Machine batch
|
|
85
|
+
* ```ts
|
|
86
|
+
* import { batch } from 'yaml-flow/batch';
|
|
87
|
+
* import { createStepMachine, loadStepFlow } from 'yaml-flow/step-machine';
|
|
88
|
+
*
|
|
89
|
+
* const flow = await loadStepFlow('./support-ticket.yaml');
|
|
90
|
+
* const results = await batch(tickets, {
|
|
91
|
+
* concurrency: 5,
|
|
92
|
+
* processor: async (ticket) => {
|
|
93
|
+
* const machine = createStepMachine(flow, handlers);
|
|
94
|
+
* return machine.run(ticket);
|
|
95
|
+
* },
|
|
96
|
+
* });
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* @example Event Graph batch
|
|
100
|
+
* ```ts
|
|
101
|
+
* import { batch } from 'yaml-flow/batch';
|
|
102
|
+
* import { next, apply, createInitialExecutionState } from 'yaml-flow/event-graph';
|
|
103
|
+
*
|
|
104
|
+
* const results = await batch(items, {
|
|
105
|
+
* concurrency: 3,
|
|
106
|
+
* processor: async (item, index) => {
|
|
107
|
+
* let state = createInitialExecutionState(graph, `exec-${index}`);
|
|
108
|
+
* state = apply(state, { type: 'inject-tokens', tokens: [item.token], timestamp: new Date().toISOString() }, graph);
|
|
109
|
+
* // ... drive the graph loop
|
|
110
|
+
* return state;
|
|
111
|
+
* },
|
|
112
|
+
* });
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Run an array of items through an async processor with concurrency control.
|
|
118
|
+
*
|
|
119
|
+
* - Items are started in order, up to `concurrency` at a time.
|
|
120
|
+
* - Results are returned in the original item order.
|
|
121
|
+
* - If a processor throws, the item is marked as failed; other items continue.
|
|
122
|
+
* - An AbortSignal prevents new items from starting (in-flight items are not cancelled).
|
|
123
|
+
*/
|
|
124
|
+
declare function batch<TItem, TResult>(items: TItem[], options: BatchOptions<TItem, TResult>): Promise<BatchResult<TItem, TResult>>;
|
|
125
|
+
|
|
126
|
+
export { type BatchItemResult, type BatchOptions, type BatchProgress, type BatchResult, batch };
|