yaml-flow 2.0.0 → 2.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 +251 -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/index.cjs +181 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +179 -1
- package/dist/index.js.map +1 -1
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -509,11 +509,250 @@ 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
|
+
|
|
512
751
|
## Package Exports
|
|
513
752
|
|
|
514
753
|
```typescript
|
|
515
|
-
// Everything (both modes + stores)
|
|
516
|
-
import { StepMachine, next, apply, MemoryStore } from 'yaml-flow';
|
|
754
|
+
// Everything (both modes + stores + batch)
|
|
755
|
+
import { StepMachine, next, apply, MemoryStore, batch } from 'yaml-flow';
|
|
517
756
|
|
|
518
757
|
// Step Machine only
|
|
519
758
|
import { StepMachine, createStepMachine, loadStepFlow } from 'yaml-flow/step-machine';
|
|
@@ -527,6 +766,13 @@ import { TASK_STATUS, COMPLETION_STRATEGIES, CONFLICT_STRATEGIES } from 'yaml-fl
|
|
|
527
766
|
// Stores
|
|
528
767
|
import { MemoryStore, LocalStorageStore, FileStore } from 'yaml-flow/stores';
|
|
529
768
|
|
|
769
|
+
// Batch
|
|
770
|
+
import { batch } from 'yaml-flow/batch';
|
|
771
|
+
import type { BatchOptions, BatchResult, BatchItemResult, BatchProgress } from 'yaml-flow/batch';
|
|
772
|
+
|
|
773
|
+
// Config utilities
|
|
774
|
+
import { resolveVariables, resolveConfigTemplates } from 'yaml-flow/config';
|
|
775
|
+
|
|
530
776
|
// Backward compatibility (v1 names → v2)
|
|
531
777
|
import { FlowEngine, createEngine } from 'yaml-flow'; // aliases for StepMachine, createStepMachine
|
|
532
778
|
```
|
|
@@ -587,6 +833,9 @@ See the [examples/](./examples) directory:
|
|
|
587
833
|
| [AI Conversation](./examples/node/ai-conversation.ts) | Step Machine | Retry, circuit breakers, component injection |
|
|
588
834
|
| [Research Pipeline](./examples/event-graph/research-pipeline.ts) | Event Graph | Parallel tasks, goal-based completion |
|
|
589
835
|
| [CI/CD Pipeline](./examples/event-graph/ci-cd-pipeline.ts) | Event Graph | External events, conditional routing, failure tokens |
|
|
836
|
+
| [Batch Tickets](./examples/batch/batch-step-machine.ts) | Batch | Concurrent processing, progress tracking |
|
|
837
|
+
| [URL Pipeline](./examples/graph-of-graphs/url-processing-pipeline.ts) | Graph-of-Graphs | Outer event-graph → batch × inner event-graph per item |
|
|
838
|
+
| [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
839
|
| [Order Processing](./examples/flows/order-processing.yaml) | Step Machine | YAML flow definition |
|
|
591
840
|
| [Browser Demo](./examples/browser/index.html) | Step Machine | In-browser usage |
|
|
592
841
|
|
|
@@ -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":";;;AAoDA,eAAsB,KAAA,CACpB,OACA,OAAA,EACsC;AACtC,EAAA,MAAM;AAAA,IACJ,WAAA,GAAc,CAAA;AAAA,IACd,SAAA;AAAA,IACA,cAAA;AAAA,IACA,WAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA,GACF,GAAI,OAAA;AAEJ,EAAA,MAAM,QAAQ,KAAA,CAAM,MAAA;AACpB,EAAA,MAAM,OAAA,GAA6C,IAAI,KAAA,CAAM,KAAK,CAAA;AAClE,EAAA,MAAM,UAAA,GAAa,KAAK,GAAA,EAAI;AAE5B,EAAA,IAAI,SAAA,GAAY,CAAA;AAChB,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,IAAI,SAAA,GAAY,CAAA;AAEhB,EAAA,SAAS,aAAa,MAAA,EAA+B;AACnD,IAAA,MAAM,OAAO,SAAA,GAAY,MAAA;AACzB,IAAA,OAAO;AAAA,MACL,SAAA;AAAA,MACA,MAAA;AAAA,MACA,MAAA;AAAA,MACA,OAAA,EAAS,QAAQ,IAAA,GAAO,MAAA;AAAA,MACxB,KAAA;AAAA,MACA,OAAA,EAAS,UAAU,CAAA,GAAI,GAAA,GAAM,KAAK,KAAA,CAAO,IAAA,GAAO,QAAS,GAAG,CAAA;AAAA,MAC5D,SAAA,EAAW,IAAA,CAAK,GAAA,EAAI,GAAI;AAAA,KAC1B;AAAA,EACF;AAGA,EAAA,IAAI,UAAU,CAAA,EAAG;AACf,IAAA,OAAO,EAAE,KAAA,EAAO,EAAC,EAAG,SAAA,EAAW,CAAA,EAAG,MAAA,EAAQ,CAAA,EAAG,KAAA,EAAO,CAAA,EAAG,UAAA,EAAY,CAAA,EAAE;AAAA,EACvE;AAEA,EAAA,OAAO,IAAI,OAAA,CAAqC,CAAC,OAAA,KAAY;AAC3D,IAAA,IAAI,MAAA,GAAS,CAAA;AAEb,IAAA,SAAS,YAAA,GAAe;AACtB,MAAA,OAAO,MAAA,GAAS,WAAA,IAAe,SAAA,GAAY,KAAA,EAAO;AAEhD,QAAA,IAAI,QAAQ,OAAA,EAAS;AAEnB,UAAA,OAAO,YAAY,KAAA,EAAO;AACxB,YAAA,MAAMA,IAAAA,GAAM,SAAA,EAAA;AACZ,YAAA,OAAA,CAAQA,IAAG,CAAA,GAAI;AAAA,cACb,IAAA,EAAM,MAAMA,IAAG,CAAA;AAAA,cACf,KAAA,EAAOA,IAAAA;AAAA,cACP,MAAA,EAAQ,QAAA;AAAA,cACR,KAAA,EAAO,IAAI,KAAA,CAAM,eAAe,CAAA;AAAA,cAChC,UAAA,EAAY;AAAA,aACd;AACA,YAAA,MAAA,EAAA;AAAA,UACF;AAEA,UAAA,IAAI,MAAA,KAAW,CAAA,IAAK,SAAA,GAAY,MAAA,KAAW,KAAA,EAAO;AAChD,YAAA,OAAA,CAAQ;AAAA,cACN,KAAA,EAAO,OAAA;AAAA,cACP,SAAA;AAAA,cACA,MAAA;AAAA,cACA,KAAA;AAAA,cACA,UAAA,EAAY,IAAA,CAAK,GAAA,EAAI,GAAI;AAAA,aAC1B,CAAA;AAAA,UACH;AACA,UAAA;AAAA,QACF;AAEA,QAAA,MAAM,GAAA,GAAM,SAAA,EAAA;AACZ,QAAA,MAAM,IAAA,GAAO,MAAM,GAAG,CAAA;AACtB,QAAA,MAAA,EAAA;AACA,QAAA,MAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAE3B,QAAA,SAAA,CAAU,IAAA,EAAM,GAAG,CAAA,CAChB,IAAA,CAAK,CAAC,MAAA,KAAW;AAChB,UAAA,OAAA,CAAQ,GAAG,CAAA,GAAI;AAAA,YACb,IAAA;AAAA,YACA,KAAA,EAAO,GAAA;AAAA,YACP,MAAA,EAAQ,WAAA;AAAA,YACR,MAAA;AAAA,YACA,UAAA,EAAY,IAAA,CAAK,GAAA,EAAI,GAAI;AAAA,WAC3B;AACA,UAAA,SAAA,EAAA;AACA,UAAA,cAAA,GAAiB,IAAA,EAAM,QAAQ,GAAG,CAAA;AAAA,QACpC,CAAC,CAAA,CACA,KAAA,CAAM,CAAC,GAAA,KAAQ;AACd,UAAA,MAAM,KAAA,GAAQ,eAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAA;AAChE,UAAA,OAAA,CAAQ,GAAG,CAAA,GAAI;AAAA,YACb,IAAA;AAAA,YACA,KAAA,EAAO,GAAA;AAAA,YACP,MAAA,EAAQ,QAAA;AAAA,YACR,KAAA;AAAA,YACA,UAAA,EAAY,IAAA,CAAK,GAAA,EAAI,GAAI;AAAA,WAC3B;AACA,UAAA,MAAA,EAAA;AACA,UAAA,WAAA,GAAc,IAAA,EAAM,OAAO,GAAG,CAAA;AAAA,QAChC,CAAC,CAAA,CACA,OAAA,CAAQ,MAAM;AACb,UAAA,MAAA,EAAA;AACA,UAAA,UAAA,GAAa,YAAA,CAAa,MAAM,CAAC,CAAA;AAEjC,UAAA,IAAI,SAAA,GAAY,WAAW,KAAA,EAAO;AAChC,YAAA,OAAA,CAAQ;AAAA,cACN,KAAA,EAAO,OAAA;AAAA,cACP,SAAA;AAAA,cACA,MAAA;AAAA,cACA,KAAA;AAAA,cACA,UAAA,EAAY,IAAA,CAAK,GAAA,EAAI,GAAI;AAAA,aAC1B,CAAA;AAAA,UACH,CAAA,MAAO;AACL,YAAA,YAAA,EAAa;AAAA,UACf;AAAA,QACF,CAAC,CAAA;AAAA,MACL;AAAA,IACF;AAEA,IAAA,YAAA,EAAa;AAAA,EACf,CAAC,CAAA;AACH","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 };
|
|
@@ -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 };
|