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 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 };