yaml-flow 3.0.0 → 3.1.1
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 +44 -23
- package/dist/{constants-B_ftYTTE.d.ts → constants-B2zqu10b.d.ts} +7 -57
- package/dist/{constants-CiyHX8L-.d.cts → constants-DJZU1pwJ.d.cts} +7 -57
- package/dist/continuous-event-graph/index.cjs +1161 -182
- package/dist/continuous-event-graph/index.cjs.map +1 -1
- package/dist/continuous-event-graph/index.d.cts +567 -48
- package/dist/continuous-event-graph/index.d.ts +567 -48
- package/dist/continuous-event-graph/index.js +1151 -183
- package/dist/continuous-event-graph/index.js.map +1 -1
- package/dist/event-graph/index.cjs +35 -11
- package/dist/event-graph/index.cjs.map +1 -1
- package/dist/event-graph/index.d.cts +14 -5
- package/dist/event-graph/index.d.ts +14 -5
- package/dist/event-graph/index.js +34 -11
- package/dist/event-graph/index.js.map +1 -1
- package/dist/index.cjs +945 -414
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +936 -415
- package/dist/index.js.map +1 -1
- package/dist/inference/index.cjs +31 -7
- package/dist/inference/index.cjs.map +1 -1
- package/dist/inference/index.d.cts +2 -2
- package/dist/inference/index.d.ts +2 -2
- package/dist/inference/index.js +31 -7
- package/dist/inference/index.js.map +1 -1
- package/dist/{types-CxJg9Jrt.d.cts → types-BwvgvlOO.d.cts} +2 -2
- package/dist/{types-BuEo3wVG.d.ts → types-ClRA8hzC.d.ts} +2 -2
- package/dist/{types-BpWrH1sf.d.cts → types-DEj7OakX.d.cts} +14 -4
- package/dist/{types-BpWrH1sf.d.ts → types-DEj7OakX.d.ts} +14 -4
- package/dist/validate-DEZ2Ymdb.d.ts +53 -0
- package/dist/validate-DqKTZg_o.d.cts +53 -0
- package/examples/batch/batch-step-machine.ts +121 -0
- package/examples/browser/index.html +367 -0
- package/examples/continuous-event-graph/live-cards-board.ts +215 -0
- package/examples/continuous-event-graph/live-portfolio-dashboard.ts +555 -0
- package/examples/continuous-event-graph/portfolio-tracker.ts +287 -0
- package/examples/continuous-event-graph/reactive-monitoring.ts +265 -0
- package/examples/continuous-event-graph/reactive-pipeline.ts +168 -0
- package/examples/continuous-event-graph/soc-incident-board.ts +287 -0
- package/examples/continuous-event-graph/stock-dashboard.ts +229 -0
- package/examples/event-graph/ci-cd-pipeline.ts +243 -0
- package/examples/event-graph/executor-diamond.ts +165 -0
- package/examples/event-graph/executor-pipeline.ts +161 -0
- package/examples/event-graph/research-pipeline.ts +137 -0
- package/examples/flows/ai-conversation.yaml +116 -0
- package/examples/flows/order-processing.yaml +143 -0
- package/examples/flows/simple-greeting.yaml +54 -0
- package/examples/graph-of-graphs/multi-stage-etl.ts +307 -0
- package/examples/graph-of-graphs/url-processing-pipeline.ts +254 -0
- package/examples/inference/azure-deployment.ts +149 -0
- package/examples/inference/copilot-cli.ts +138 -0
- package/examples/inference/data-pipeline.ts +145 -0
- package/examples/inference/pluggable-adapters.ts +254 -0
- package/examples/ingest.js +733 -0
- package/examples/node/ai-conversation.ts +195 -0
- package/examples/node/simple-greeting.ts +101 -0
- package/package.json +3 -2
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# yaml-language-server: $schema=../schema/flow.schema.json
|
|
2
|
+
|
|
3
|
+
# AI Conversation Flow with Validation Loop
|
|
4
|
+
# Demonstrates retry logic, circuit breakers, and multi-step AI workflows
|
|
5
|
+
|
|
6
|
+
settings:
|
|
7
|
+
start_step: generate_response
|
|
8
|
+
max_total_steps: 50
|
|
9
|
+
timeout_ms: 60000
|
|
10
|
+
|
|
11
|
+
steps:
|
|
12
|
+
generate_response:
|
|
13
|
+
description: "Generate AI response based on user input"
|
|
14
|
+
expects_data:
|
|
15
|
+
- user_message
|
|
16
|
+
- conversation_history
|
|
17
|
+
produces_data:
|
|
18
|
+
- ai_response
|
|
19
|
+
- confidence_score
|
|
20
|
+
transitions:
|
|
21
|
+
success: validate_response
|
|
22
|
+
failure: error_state
|
|
23
|
+
retry:
|
|
24
|
+
max_attempts: 3
|
|
25
|
+
delay_ms: 1000
|
|
26
|
+
backoff_multiplier: 2
|
|
27
|
+
|
|
28
|
+
validate_response:
|
|
29
|
+
description: "Validate the AI response meets quality criteria"
|
|
30
|
+
expects_data:
|
|
31
|
+
- ai_response
|
|
32
|
+
- confidence_score
|
|
33
|
+
produces_data:
|
|
34
|
+
- validation_result
|
|
35
|
+
- validation_feedback
|
|
36
|
+
transitions:
|
|
37
|
+
valid: check_approval
|
|
38
|
+
needs_refinement: refine_response
|
|
39
|
+
failure: error_state
|
|
40
|
+
|
|
41
|
+
refine_response:
|
|
42
|
+
description: "Refine AI response based on validation feedback"
|
|
43
|
+
expects_data:
|
|
44
|
+
- ai_response
|
|
45
|
+
- validation_feedback
|
|
46
|
+
- user_message
|
|
47
|
+
produces_data:
|
|
48
|
+
- ai_response
|
|
49
|
+
- confidence_score
|
|
50
|
+
transitions:
|
|
51
|
+
success: validate_response
|
|
52
|
+
failure: error_state
|
|
53
|
+
circuit_breaker:
|
|
54
|
+
max_iterations: 3
|
|
55
|
+
on_open: max_refinements
|
|
56
|
+
|
|
57
|
+
check_approval:
|
|
58
|
+
description: "Present response and check for user approval"
|
|
59
|
+
expects_data:
|
|
60
|
+
- ai_response
|
|
61
|
+
produces_data:
|
|
62
|
+
- user_approved
|
|
63
|
+
- user_feedback
|
|
64
|
+
transitions:
|
|
65
|
+
approved: success_state
|
|
66
|
+
rejected: incorporate_feedback
|
|
67
|
+
timeout: timeout_state
|
|
68
|
+
|
|
69
|
+
incorporate_feedback:
|
|
70
|
+
description: "Incorporate user feedback into response"
|
|
71
|
+
expects_data:
|
|
72
|
+
- ai_response
|
|
73
|
+
- user_feedback
|
|
74
|
+
- user_message
|
|
75
|
+
produces_data:
|
|
76
|
+
- ai_response
|
|
77
|
+
- confidence_score
|
|
78
|
+
transitions:
|
|
79
|
+
success: validate_response
|
|
80
|
+
failure: error_state
|
|
81
|
+
circuit_breaker:
|
|
82
|
+
max_iterations: 5
|
|
83
|
+
on_open: max_feedback_loops
|
|
84
|
+
|
|
85
|
+
terminal_states:
|
|
86
|
+
success_state:
|
|
87
|
+
description: "User approved the AI response"
|
|
88
|
+
return_intent: "approved"
|
|
89
|
+
return_artifacts:
|
|
90
|
+
- ai_response
|
|
91
|
+
- conversation_history
|
|
92
|
+
|
|
93
|
+
error_state:
|
|
94
|
+
description: "Flow encountered an error"
|
|
95
|
+
return_intent: "error"
|
|
96
|
+
return_artifacts: false
|
|
97
|
+
|
|
98
|
+
timeout_state:
|
|
99
|
+
description: "User did not respond in time"
|
|
100
|
+
return_intent: "timeout"
|
|
101
|
+
return_artifacts:
|
|
102
|
+
- ai_response
|
|
103
|
+
|
|
104
|
+
max_refinements:
|
|
105
|
+
description: "Maximum refinement attempts reached"
|
|
106
|
+
return_intent: "max_refinements"
|
|
107
|
+
return_artifacts:
|
|
108
|
+
- ai_response
|
|
109
|
+
- validation_feedback
|
|
110
|
+
|
|
111
|
+
max_feedback_loops:
|
|
112
|
+
description: "Maximum feedback incorporation attempts reached"
|
|
113
|
+
return_intent: "max_feedback"
|
|
114
|
+
return_artifacts:
|
|
115
|
+
- ai_response
|
|
116
|
+
- user_feedback
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# yaml-language-server: $schema=../schema/flow.schema.json
|
|
2
|
+
|
|
3
|
+
# Order Processing Workflow
|
|
4
|
+
# Demonstrates a practical e-commerce order flow with inventory, payment, and shipping
|
|
5
|
+
|
|
6
|
+
settings:
|
|
7
|
+
start_step: validate_order
|
|
8
|
+
max_total_steps: 20
|
|
9
|
+
timeout_ms: 120000
|
|
10
|
+
|
|
11
|
+
steps:
|
|
12
|
+
validate_order:
|
|
13
|
+
description: "Validate order details"
|
|
14
|
+
expects_data:
|
|
15
|
+
- order
|
|
16
|
+
produces_data:
|
|
17
|
+
- validated_order
|
|
18
|
+
- validation_errors
|
|
19
|
+
transitions:
|
|
20
|
+
valid: check_inventory
|
|
21
|
+
invalid: order_invalid
|
|
22
|
+
failure: error_state
|
|
23
|
+
|
|
24
|
+
check_inventory:
|
|
25
|
+
description: "Check product availability"
|
|
26
|
+
expects_data:
|
|
27
|
+
- validated_order
|
|
28
|
+
produces_data:
|
|
29
|
+
- inventory_status
|
|
30
|
+
- reserved_items
|
|
31
|
+
transitions:
|
|
32
|
+
available: process_payment
|
|
33
|
+
partial: partial_availability
|
|
34
|
+
unavailable: out_of_stock
|
|
35
|
+
failure: error_state
|
|
36
|
+
retry:
|
|
37
|
+
max_attempts: 2
|
|
38
|
+
delay_ms: 500
|
|
39
|
+
|
|
40
|
+
partial_availability:
|
|
41
|
+
description: "Handle partial availability - ask user to proceed or cancel"
|
|
42
|
+
expects_data:
|
|
43
|
+
- validated_order
|
|
44
|
+
- inventory_status
|
|
45
|
+
produces_data:
|
|
46
|
+
- user_decision
|
|
47
|
+
- adjusted_order
|
|
48
|
+
transitions:
|
|
49
|
+
proceed: process_payment
|
|
50
|
+
cancel: order_cancelled
|
|
51
|
+
failure: error_state
|
|
52
|
+
|
|
53
|
+
process_payment:
|
|
54
|
+
description: "Process payment for the order"
|
|
55
|
+
expects_data:
|
|
56
|
+
- validated_order
|
|
57
|
+
- reserved_items
|
|
58
|
+
produces_data:
|
|
59
|
+
- payment_result
|
|
60
|
+
- transaction_id
|
|
61
|
+
transitions:
|
|
62
|
+
success: create_shipment
|
|
63
|
+
declined: payment_failed
|
|
64
|
+
failure: error_state
|
|
65
|
+
retry:
|
|
66
|
+
max_attempts: 3
|
|
67
|
+
delay_ms: 2000
|
|
68
|
+
backoff_multiplier: 1.5
|
|
69
|
+
|
|
70
|
+
create_shipment:
|
|
71
|
+
description: "Create shipping label and schedule pickup"
|
|
72
|
+
expects_data:
|
|
73
|
+
- validated_order
|
|
74
|
+
- transaction_id
|
|
75
|
+
produces_data:
|
|
76
|
+
- shipment_id
|
|
77
|
+
- tracking_number
|
|
78
|
+
- estimated_delivery
|
|
79
|
+
transitions:
|
|
80
|
+
success: order_complete
|
|
81
|
+
failure: shipment_error
|
|
82
|
+
|
|
83
|
+
shipment_error:
|
|
84
|
+
description: "Handle shipment creation failure - refund and notify"
|
|
85
|
+
expects_data:
|
|
86
|
+
- transaction_id
|
|
87
|
+
- validated_order
|
|
88
|
+
produces_data:
|
|
89
|
+
- refund_status
|
|
90
|
+
transitions:
|
|
91
|
+
refunded: order_refunded
|
|
92
|
+
failure: manual_intervention
|
|
93
|
+
|
|
94
|
+
terminal_states:
|
|
95
|
+
order_complete:
|
|
96
|
+
description: "Order successfully processed"
|
|
97
|
+
return_intent: "success"
|
|
98
|
+
return_artifacts:
|
|
99
|
+
- transaction_id
|
|
100
|
+
- tracking_number
|
|
101
|
+
- estimated_delivery
|
|
102
|
+
|
|
103
|
+
order_invalid:
|
|
104
|
+
description: "Order validation failed"
|
|
105
|
+
return_intent: "invalid_order"
|
|
106
|
+
return_artifacts:
|
|
107
|
+
- validation_errors
|
|
108
|
+
|
|
109
|
+
out_of_stock:
|
|
110
|
+
description: "Products out of stock"
|
|
111
|
+
return_intent: "out_of_stock"
|
|
112
|
+
return_artifacts:
|
|
113
|
+
- inventory_status
|
|
114
|
+
|
|
115
|
+
order_cancelled:
|
|
116
|
+
description: "Order cancelled by user"
|
|
117
|
+
return_intent: "cancelled"
|
|
118
|
+
return_artifacts: false
|
|
119
|
+
|
|
120
|
+
payment_failed:
|
|
121
|
+
description: "Payment was declined"
|
|
122
|
+
return_intent: "payment_declined"
|
|
123
|
+
return_artifacts:
|
|
124
|
+
- payment_result
|
|
125
|
+
|
|
126
|
+
order_refunded:
|
|
127
|
+
description: "Order refunded due to shipment failure"
|
|
128
|
+
return_intent: "refunded"
|
|
129
|
+
return_artifacts:
|
|
130
|
+
- refund_status
|
|
131
|
+
- transaction_id
|
|
132
|
+
|
|
133
|
+
manual_intervention:
|
|
134
|
+
description: "Requires manual intervention"
|
|
135
|
+
return_intent: "manual_required"
|
|
136
|
+
return_artifacts:
|
|
137
|
+
- transaction_id
|
|
138
|
+
- validated_order
|
|
139
|
+
|
|
140
|
+
error_state:
|
|
141
|
+
description: "System error"
|
|
142
|
+
return_intent: "error"
|
|
143
|
+
return_artifacts: false
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# yaml-language-server: $schema=../schema/flow.schema.json
|
|
2
|
+
|
|
3
|
+
# Simple Greeting Flow
|
|
4
|
+
# Demonstrates basic flow structure with validation and error handling
|
|
5
|
+
|
|
6
|
+
settings:
|
|
7
|
+
start_step: greet
|
|
8
|
+
max_total_steps: 10
|
|
9
|
+
|
|
10
|
+
steps:
|
|
11
|
+
greet:
|
|
12
|
+
description: "Generate a greeting message"
|
|
13
|
+
produces_data:
|
|
14
|
+
- greeting
|
|
15
|
+
- user_name
|
|
16
|
+
transitions:
|
|
17
|
+
success: validate
|
|
18
|
+
failure: error_state
|
|
19
|
+
|
|
20
|
+
validate:
|
|
21
|
+
description: "Validate the greeting was generated correctly"
|
|
22
|
+
expects_data:
|
|
23
|
+
- greeting
|
|
24
|
+
- user_name
|
|
25
|
+
produces_data:
|
|
26
|
+
- is_valid
|
|
27
|
+
transitions:
|
|
28
|
+
success: personalize
|
|
29
|
+
invalid: error_state
|
|
30
|
+
failure: error_state
|
|
31
|
+
|
|
32
|
+
personalize:
|
|
33
|
+
description: "Create personalized response"
|
|
34
|
+
expects_data:
|
|
35
|
+
- greeting
|
|
36
|
+
- user_name
|
|
37
|
+
produces_data:
|
|
38
|
+
- final_message
|
|
39
|
+
transitions:
|
|
40
|
+
success: success_state
|
|
41
|
+
failure: error_state
|
|
42
|
+
|
|
43
|
+
terminal_states:
|
|
44
|
+
success_state:
|
|
45
|
+
description: "Flow completed successfully"
|
|
46
|
+
return_intent: "success"
|
|
47
|
+
return_artifacts:
|
|
48
|
+
- final_message
|
|
49
|
+
- user_name
|
|
50
|
+
|
|
51
|
+
error_state:
|
|
52
|
+
description: "Flow encountered an error"
|
|
53
|
+
return_intent: "error"
|
|
54
|
+
return_artifacts: false
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph-of-Graphs Example: Multi-Stage ETL Pipeline
|
|
3
|
+
*
|
|
4
|
+
* An outer event-graph orchestrates an ETL pipeline where:
|
|
5
|
+
* - "extract" stage: batch × inner EVENT-GRAPH (parallel source extraction)
|
|
6
|
+
* - "transform" stage: batch × inner STEP-MACHINE (sequential validation pipeline)
|
|
7
|
+
* - "load" + "validate" stages: run in parallel after transform
|
|
8
|
+
*
|
|
9
|
+
* Demonstrates:
|
|
10
|
+
* - Mixed sub-graph modes (event-graph + step-machine in same pipeline)
|
|
11
|
+
* - Config templates shared across both inner configs
|
|
12
|
+
* - Variables resolved per-item in the batch
|
|
13
|
+
* - Fan-out / fan-in in the outer graph
|
|
14
|
+
*
|
|
15
|
+
* Outer graph:
|
|
16
|
+
* discover-sources → extract-batch → transform-batch → [load ∥ validate] → finalize
|
|
17
|
+
*
|
|
18
|
+
* Inner extract graph (event-graph, per source):
|
|
19
|
+
* connect → [fetch-metadata ∥ fetch-schema] → snapshot-data
|
|
20
|
+
*
|
|
21
|
+
* Inner transform flow (step-machine, per record):
|
|
22
|
+
* parse → validate → normalize → enrich → (accept | reject)
|
|
23
|
+
*
|
|
24
|
+
* Run with: npx tsx examples/graph-of-graphs/multi-stage-etl.ts
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
next, apply, createInitialExecutionState,
|
|
29
|
+
} from '../../src/event-graph/index.js';
|
|
30
|
+
import { createStepMachine } from '../../src/step-machine/index.js';
|
|
31
|
+
import { batch } from '../../src/batch/index.js';
|
|
32
|
+
import { resolveVariables, resolveConfigTemplates } from '../../src/config/index.js';
|
|
33
|
+
import type { GraphConfig } from '../../src/event-graph/types.js';
|
|
34
|
+
import type { StepFlowConfig, StepHandler } from '../../src/step-machine/types.js';
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// 1. Inner configs
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
// --- Extract: event-graph (parallel metadata + schema fetch) ---
|
|
41
|
+
const extractGraphTemplate: Record<string, unknown> = {
|
|
42
|
+
id: 'source-extractor',
|
|
43
|
+
'config-templates': {
|
|
44
|
+
DB_CONN: { driver: 'pg', timeout: 10000, host: '${DB_HOST}' },
|
|
45
|
+
},
|
|
46
|
+
settings: {
|
|
47
|
+
completion: 'all-tasks-complete' as const,
|
|
48
|
+
},
|
|
49
|
+
tasks: {
|
|
50
|
+
connect: {
|
|
51
|
+
provides: ['connected'],
|
|
52
|
+
config: { 'config-template': 'DB_CONN', database: '${SOURCE_DB}' },
|
|
53
|
+
},
|
|
54
|
+
'fetch-metadata': {
|
|
55
|
+
requires: ['connected'],
|
|
56
|
+
provides: ['metadata-ready'],
|
|
57
|
+
config: { 'config-template': 'DB_CONN', query: 'SELECT * FROM information_schema.tables' },
|
|
58
|
+
},
|
|
59
|
+
'fetch-schema': {
|
|
60
|
+
requires: ['connected'],
|
|
61
|
+
provides: ['schema-ready'],
|
|
62
|
+
config: { 'config-template': 'DB_CONN', query: 'SELECT * FROM information_schema.columns' },
|
|
63
|
+
},
|
|
64
|
+
'snapshot-data': {
|
|
65
|
+
requires: ['metadata-ready', 'schema-ready'],
|
|
66
|
+
provides: ['snapshot-complete'],
|
|
67
|
+
config: { 'config-template': 'DB_CONN', 'cmd-args': 'pg_dump ${SOURCE_DB}' },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// --- Transform: step-machine (sequential validation pipeline) ---
|
|
73
|
+
const transformFlow: StepFlowConfig = {
|
|
74
|
+
id: 'record-transformer',
|
|
75
|
+
settings: { start_step: 'parse', max_total_steps: 10 },
|
|
76
|
+
steps: {
|
|
77
|
+
parse: {
|
|
78
|
+
produces_data: ['parsed_record'],
|
|
79
|
+
transitions: { success: 'validate', error: 'reject' },
|
|
80
|
+
},
|
|
81
|
+
validate: {
|
|
82
|
+
expects_data: ['parsed_record'],
|
|
83
|
+
produces_data: ['validation_result'],
|
|
84
|
+
transitions: { valid: 'normalize', invalid: 'reject' },
|
|
85
|
+
},
|
|
86
|
+
normalize: {
|
|
87
|
+
expects_data: ['parsed_record'],
|
|
88
|
+
produces_data: ['normalized_record'],
|
|
89
|
+
transitions: { done: 'enrich' },
|
|
90
|
+
},
|
|
91
|
+
enrich: {
|
|
92
|
+
expects_data: ['normalized_record'],
|
|
93
|
+
produces_data: ['enriched_record'],
|
|
94
|
+
transitions: { done: 'accept' },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
terminal_states: {
|
|
98
|
+
accept: { return_intent: 'accepted', return_artifacts: ['enriched_record'] },
|
|
99
|
+
reject: { return_intent: 'rejected', return_artifacts: ['validation_result'] },
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const transformHandlers: Record<string, StepHandler> = {
|
|
104
|
+
parse: async (input) => {
|
|
105
|
+
const raw = input.raw_data as string || '';
|
|
106
|
+
if (!raw) return { result: 'error' };
|
|
107
|
+
return { result: 'success', data: { parsed_record: JSON.parse(raw) } };
|
|
108
|
+
},
|
|
109
|
+
validate: async (input) => {
|
|
110
|
+
const rec = input.parsed_record as Record<string, unknown>;
|
|
111
|
+
if (!rec.id || !rec.name) return { result: 'invalid', data: { validation_result: 'missing required fields' } };
|
|
112
|
+
return { result: 'valid', data: { validation_result: 'ok' } };
|
|
113
|
+
},
|
|
114
|
+
normalize: async (input) => {
|
|
115
|
+
const rec = input.parsed_record as Record<string, unknown>;
|
|
116
|
+
return { result: 'done', data: { normalized_record: { ...rec, name: (rec.name as string).trim().toLowerCase() } } };
|
|
117
|
+
},
|
|
118
|
+
enrich: async (input) => {
|
|
119
|
+
const rec = input.normalized_record as Record<string, unknown>;
|
|
120
|
+
return { result: 'done', data: { enriched_record: { ...rec, enriched_at: new Date().toISOString() } } };
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// ============================================================================
|
|
125
|
+
// 2. Outer graph
|
|
126
|
+
// ============================================================================
|
|
127
|
+
|
|
128
|
+
const outerGraph: GraphConfig = {
|
|
129
|
+
id: 'etl-pipeline',
|
|
130
|
+
settings: { completion: 'all-tasks-complete' },
|
|
131
|
+
tasks: {
|
|
132
|
+
'discover-sources': {
|
|
133
|
+
provides: ['sources-discovered'],
|
|
134
|
+
},
|
|
135
|
+
'extract-batch': {
|
|
136
|
+
requires: ['sources-discovered'],
|
|
137
|
+
provides: ['extraction-complete'],
|
|
138
|
+
},
|
|
139
|
+
'transform-batch': {
|
|
140
|
+
requires: ['extraction-complete'],
|
|
141
|
+
provides: ['transform-complete'],
|
|
142
|
+
},
|
|
143
|
+
'load-to-warehouse': {
|
|
144
|
+
requires: ['transform-complete'],
|
|
145
|
+
provides: ['load-complete'],
|
|
146
|
+
},
|
|
147
|
+
'validate-integrity': {
|
|
148
|
+
requires: ['transform-complete'],
|
|
149
|
+
provides: ['validation-complete'],
|
|
150
|
+
},
|
|
151
|
+
'finalize': {
|
|
152
|
+
requires: ['load-complete', 'validation-complete'],
|
|
153
|
+
provides: ['pipeline-done'],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// ============================================================================
|
|
159
|
+
// 3. Sub-graph drivers
|
|
160
|
+
// ============================================================================
|
|
161
|
+
|
|
162
|
+
/** Drive one source through the extract event-graph */
|
|
163
|
+
async function runExtractGraph(source: { id: string; db: string }) {
|
|
164
|
+
const config = resolveVariables(
|
|
165
|
+
resolveConfigTemplates(extractGraphTemplate),
|
|
166
|
+
{ SOURCE_DB: source.db, DB_HOST: 'db.internal' },
|
|
167
|
+
) as unknown as GraphConfig;
|
|
168
|
+
|
|
169
|
+
let state = createInitialExecutionState(config, `extract-${source.id}`);
|
|
170
|
+
while (true) {
|
|
171
|
+
const { eligibleTasks, isComplete } = next(config, state);
|
|
172
|
+
if (isComplete || eligibleTasks.length === 0) break;
|
|
173
|
+
await Promise.all(
|
|
174
|
+
eligibleTasks.map(async (taskName) => {
|
|
175
|
+
state = apply(state, { type: 'task-started', taskName, timestamp: new Date().toISOString() }, config);
|
|
176
|
+
await new Promise((r) => setTimeout(r, 5 + Math.random() * 15)); // simulate work
|
|
177
|
+
state = apply(state, { type: 'task-completed', taskName, timestamp: new Date().toISOString() }, config);
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return { sourceId: source.id, tokens: state.availableOutputs };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Drive one record through the transform step-machine */
|
|
185
|
+
async function runTransformFlow(record: { id: string; raw_data: string }) {
|
|
186
|
+
const machine = createStepMachine(transformFlow, transformHandlers);
|
|
187
|
+
return machine.run({ raw_data: record.raw_data });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ============================================================================
|
|
191
|
+
// 4. Sample data
|
|
192
|
+
// ============================================================================
|
|
193
|
+
|
|
194
|
+
const sources = [
|
|
195
|
+
{ id: 'src-orders', db: 'orders_db' },
|
|
196
|
+
{ id: 'src-users', db: 'users_db' },
|
|
197
|
+
{ id: 'src-products', db: 'products_db' },
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
const records = [
|
|
201
|
+
{ id: 'rec-1', raw_data: '{"id": 1, "name": " Alice "}' },
|
|
202
|
+
{ id: 'rec-2', raw_data: '{"id": 2, "name": " Bob "}' },
|
|
203
|
+
{ id: 'rec-3', raw_data: '{"name": "no-id"}' }, // will be rejected (no id)
|
|
204
|
+
{ id: 'rec-4', raw_data: '' }, // will fail to parse
|
|
205
|
+
{ id: 'rec-5', raw_data: '{"id": 5, "name": " Charlie "}' },
|
|
206
|
+
{ id: 'rec-6', raw_data: '{"id": 6, "name": " Diana "}' },
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
// ============================================================================
|
|
210
|
+
// 5. Outer graph handlers
|
|
211
|
+
// ============================================================================
|
|
212
|
+
|
|
213
|
+
const outerHandlers: Record<string, () => Promise<void>> = {
|
|
214
|
+
'discover-sources': async () => {
|
|
215
|
+
console.log(` Found ${sources.length} data sources`);
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
'extract-batch': async () => {
|
|
219
|
+
console.log(` Extracting from ${sources.length} sources (concurrency: 2, mode: event-graph)`);
|
|
220
|
+
const result = await batch(sources, {
|
|
221
|
+
concurrency: 2,
|
|
222
|
+
processor: runExtractGraph,
|
|
223
|
+
onItemComplete: (src, res) => {
|
|
224
|
+
console.log(` ✓ ${src.id} (${src.db}): [${res.tokens.join(', ')}]`);
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
console.log(` Extract done: ${result.completed}/${result.total} in ${result.durationMs}ms`);
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
'transform-batch': async () => {
|
|
231
|
+
console.log(` Transforming ${records.length} records (concurrency: 4, mode: step-machine)`);
|
|
232
|
+
const result = await batch(records, {
|
|
233
|
+
concurrency: 4,
|
|
234
|
+
processor: runTransformFlow,
|
|
235
|
+
onItemComplete: (rec, res) => {
|
|
236
|
+
console.log(` ✓ ${rec.id}: ${res.intent} — [${res.stepHistory.join(' → ')}]`);
|
|
237
|
+
},
|
|
238
|
+
onItemError: (rec, err) => {
|
|
239
|
+
console.log(` ✗ ${rec.id}: ${err.message}`);
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
console.log(` Transform done: ${result.completed} ok, ${result.failed} failed in ${result.durationMs}ms`);
|
|
243
|
+
|
|
244
|
+
// Show accepted vs rejected
|
|
245
|
+
const accepted = result.items.filter((i) => i.status === 'completed' && i.result?.intent === 'accepted');
|
|
246
|
+
const rejected = result.items.filter((i) => i.status === 'completed' && i.result?.intent === 'rejected');
|
|
247
|
+
console.log(` Accepted: ${accepted.length}, Rejected: ${rejected.length}`);
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
'load-to-warehouse': async () => {
|
|
251
|
+
console.log(' Loading accepted records to data warehouse');
|
|
252
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
'validate-integrity': async () => {
|
|
256
|
+
console.log(' Running integrity checks on loaded data');
|
|
257
|
+
await new Promise((r) => setTimeout(r, 15));
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
'finalize': async () => {
|
|
261
|
+
console.log(' Generating ETL summary report');
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// ============================================================================
|
|
266
|
+
// 6. Drive outer graph
|
|
267
|
+
// ============================================================================
|
|
268
|
+
|
|
269
|
+
async function main() {
|
|
270
|
+
console.log('=== Multi-Stage ETL Pipeline (Graph-of-Graphs) ===');
|
|
271
|
+
console.log('Outer: event-graph | Extract sub: event-graph | Transform sub: step-machine\n');
|
|
272
|
+
|
|
273
|
+
let state = createInitialExecutionState(outerGraph, 'etl-run-1');
|
|
274
|
+
const now = () => new Date().toISOString();
|
|
275
|
+
|
|
276
|
+
while (true) {
|
|
277
|
+
const { eligibleTasks, isComplete } = next(outerGraph, state);
|
|
278
|
+
if (isComplete) break;
|
|
279
|
+
if (eligibleTasks.length === 0) {
|
|
280
|
+
console.log('\nPipeline stuck!');
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Note: load-to-warehouse and validate-integrity will run in PARALLEL
|
|
285
|
+
// because they both only require transform-complete
|
|
286
|
+
if (eligibleTasks.length > 1) {
|
|
287
|
+
console.log(`\n▶ [parallel] ${eligibleTasks.join(' + ')}`);
|
|
288
|
+
}
|
|
289
|
+
await Promise.all(
|
|
290
|
+
eligibleTasks.map(async (taskName) => {
|
|
291
|
+
if (eligibleTasks.length === 1) console.log(`\n▶ ${taskName}`);
|
|
292
|
+
state = apply(state, { type: 'task-started', taskName, timestamp: now() }, outerGraph);
|
|
293
|
+
try {
|
|
294
|
+
await outerHandlers[taskName]();
|
|
295
|
+
state = apply(state, { type: 'task-completed', taskName, timestamp: now() }, outerGraph);
|
|
296
|
+
} catch (err: any) {
|
|
297
|
+
state = apply(state, { type: 'task-failed', taskName, error: err.message, timestamp: now() }, outerGraph);
|
|
298
|
+
}
|
|
299
|
+
}),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
console.log('\n=== ETL Pipeline Complete ===');
|
|
304
|
+
console.log('Final tokens:', state.availableOutputs);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
main().catch(console.error);
|