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,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portfolio Tracker — Correct Model
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates externally-driven tasks, engine-stored data, data-changed
|
|
5
|
+
* cascade, and retrigger via task-restart.
|
|
6
|
+
*
|
|
7
|
+
* Graph topology:
|
|
8
|
+
* portfolio-form → price-fetch → holdings-table → portfolio-value
|
|
9
|
+
* (external) (handler) (handler) (handler)
|
|
10
|
+
*
|
|
11
|
+
* Key concepts:
|
|
12
|
+
* • portfolio-form has NO handler — it's user-editable.
|
|
13
|
+
* The caller pushes `task-completed` with data externally.
|
|
14
|
+
* • Engine persists `data` on GraphEngineStore (per-task state).
|
|
15
|
+
* • Downstream handlers read upstream data from engine state:
|
|
16
|
+
* ctx.live.state.tasks['portfolio-form'].data
|
|
17
|
+
* • data-changed refresh: reactive layer auto-hashes the data payload
|
|
18
|
+
* on external push. When the hash differs from previous, downstream
|
|
19
|
+
* tasks become re-eligible.
|
|
20
|
+
* • retrigger('price-fetch') sends a task-restart event through the
|
|
21
|
+
* engine, which resets the task and re-triggers the cascade.
|
|
22
|
+
*
|
|
23
|
+
* Timeline:
|
|
24
|
+
* T0: Empty board — graph wired, standing by
|
|
25
|
+
* T1: User submits 2 holdings → push task-completed for portfolio-form
|
|
26
|
+
* data-changed cascade: price-fetch → holdings-table → portfolio-value
|
|
27
|
+
* T2: User adds 3rd holding → push task-completed with new data
|
|
28
|
+
* new hash → identical cascade, fresh prices
|
|
29
|
+
* T3: Force price refresh without form change → retrigger('price-fetch')
|
|
30
|
+
* T4: Quiescent
|
|
31
|
+
*
|
|
32
|
+
* Run with: npx tsx examples/continuous-event-graph/portfolio-tracker.ts
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { GraphConfig } from '../../src/event-graph/types.js';
|
|
36
|
+
import { createReactiveGraph } from '../../src/continuous-event-graph/reactive.js';
|
|
37
|
+
import { validateReactiveGraph } from '../../src/continuous-event-graph/validate.js';
|
|
38
|
+
import { createCallbackHandler } from '../../src/continuous-event-graph/handlers.js';
|
|
39
|
+
import type { TaskHandlerFn, ReactiveGraph } from '../../src/continuous-event-graph/reactive.js';
|
|
40
|
+
import type { ResolveCallbackFn } from '../../src/continuous-event-graph/handlers.js';
|
|
41
|
+
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// Simulated market data
|
|
44
|
+
// ============================================================================
|
|
45
|
+
|
|
46
|
+
const marketPrices: Record<string, number> = {
|
|
47
|
+
AAPL: 198.50,
|
|
48
|
+
MSFT: 425.30,
|
|
49
|
+
GOOG: 178.90,
|
|
50
|
+
AMZN: 192.40,
|
|
51
|
+
TSLA: 168.75,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function fetchPrices(symbols: string[]): Record<string, number> {
|
|
55
|
+
const prices: Record<string, number> = {};
|
|
56
|
+
for (const sym of symbols) {
|
|
57
|
+
prices[sym] = marketPrices[sym] ?? 0;
|
|
58
|
+
}
|
|
59
|
+
return prices;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// 1. Define the graph config
|
|
64
|
+
// ============================================================================
|
|
65
|
+
|
|
66
|
+
const config: GraphConfig = {
|
|
67
|
+
id: 'portfolio-tracker',
|
|
68
|
+
settings: {
|
|
69
|
+
completion: 'manual',
|
|
70
|
+
execution_mode: 'eligibility-mode',
|
|
71
|
+
refreshStrategy: 'data-changed',
|
|
72
|
+
},
|
|
73
|
+
tasks: {
|
|
74
|
+
'portfolio-form': {
|
|
75
|
+
// No requires — root node, externally driven
|
|
76
|
+
provides: ['portfolio-form'],
|
|
77
|
+
description: 'Editable portfolio holdings (no handler — external push)',
|
|
78
|
+
},
|
|
79
|
+
'price-fetch': {
|
|
80
|
+
requires: ['portfolio-form'],
|
|
81
|
+
provides: ['price-fetch'],
|
|
82
|
+
taskHandlers: ['price-fetch'],
|
|
83
|
+
description: 'Fetch market prices for portfolio symbols',
|
|
84
|
+
},
|
|
85
|
+
'holdings-table': {
|
|
86
|
+
requires: ['portfolio-form', 'price-fetch'],
|
|
87
|
+
provides: ['holdings-table'],
|
|
88
|
+
taskHandlers: ['holdings-table'],
|
|
89
|
+
description: 'Join holdings × prices → table rows',
|
|
90
|
+
},
|
|
91
|
+
'portfolio-value': {
|
|
92
|
+
requires: ['holdings-table'],
|
|
93
|
+
provides: ['portfolio-value'],
|
|
94
|
+
taskHandlers: ['portfolio-value'],
|
|
95
|
+
description: 'Sum all holding values → total portfolio value',
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// 2. Define handlers (portfolio-form has NONE — it's externally driven)
|
|
102
|
+
// ============================================================================
|
|
103
|
+
|
|
104
|
+
// Lazy resolver — graph doesn't exist at handler-creation time
|
|
105
|
+
let graphRef: ReactiveGraph;
|
|
106
|
+
const getResolve = (): ResolveCallbackFn => graphRef.resolveCallback.bind(graphRef);
|
|
107
|
+
|
|
108
|
+
const handlers: Record<string, TaskHandlerFn> = {
|
|
109
|
+
// price-fetch: reads holdings from upstream state, fetches market prices
|
|
110
|
+
'price-fetch': createCallbackHandler(async ({ state }) => {
|
|
111
|
+
const formData = state['portfolio-form'] as
|
|
112
|
+
{ holdings?: Array<{ symbol: string; qty: number }> } | undefined;
|
|
113
|
+
const symbols = (formData?.holdings ?? []).map(h => h.symbol);
|
|
114
|
+
console.log(` [price-fetch] Fetching prices for: ${symbols.join(', ') || '(none)'}`);
|
|
115
|
+
const prices = fetchPrices(symbols);
|
|
116
|
+
return { prices };
|
|
117
|
+
}, getResolve),
|
|
118
|
+
|
|
119
|
+
// holdings-table: reads form data + prices from upstream state, computes rows
|
|
120
|
+
'holdings-table': createCallbackHandler(async ({ state }) => {
|
|
121
|
+
const formData = state['portfolio-form'] as
|
|
122
|
+
{ holdings?: Array<{ symbol: string; qty: number }> } | undefined;
|
|
123
|
+
const priceData = state['price-fetch'] as
|
|
124
|
+
{ prices?: Record<string, number> } | undefined;
|
|
125
|
+
|
|
126
|
+
const holdings = formData?.holdings ?? [];
|
|
127
|
+
const prices = priceData?.prices ?? {};
|
|
128
|
+
|
|
129
|
+
const rows = holdings.map(h => ({
|
|
130
|
+
symbol: h.symbol,
|
|
131
|
+
qty: h.qty,
|
|
132
|
+
price: prices[h.symbol] ?? 0,
|
|
133
|
+
value: h.qty * (prices[h.symbol] ?? 0),
|
|
134
|
+
}));
|
|
135
|
+
console.log(` [holdings-table] ${rows.length} rows computed`);
|
|
136
|
+
rows.forEach(r =>
|
|
137
|
+
console.log(` ${r.symbol}: ${r.qty} × $${r.price.toFixed(2)} = $${r.value.toFixed(2)}`),
|
|
138
|
+
);
|
|
139
|
+
return { rows };
|
|
140
|
+
}, getResolve),
|
|
141
|
+
|
|
142
|
+
// portfolio-value: reads table rows from upstream state, sums values
|
|
143
|
+
'portfolio-value': createCallbackHandler(async ({ state }) => {
|
|
144
|
+
const tableData = state['holdings-table'] as
|
|
145
|
+
{ rows?: Array<{ value: number }> } | undefined;
|
|
146
|
+
const rows = tableData?.rows ?? [];
|
|
147
|
+
const totalValue = rows.reduce((sum, r) => sum + r.value, 0);
|
|
148
|
+
console.log(` [portfolio-value] Total: $${totalValue.toFixed(2)}`);
|
|
149
|
+
return { totalValue };
|
|
150
|
+
}, getResolve),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// 3. Create the reactive graph
|
|
155
|
+
// ============================================================================
|
|
156
|
+
|
|
157
|
+
const graph = createReactiveGraph(config, {
|
|
158
|
+
handlers,
|
|
159
|
+
onDrain: (events, live, result) => {
|
|
160
|
+
const done = Object.values(live.state.tasks).filter(t => t.status === 'completed').length;
|
|
161
|
+
const total = Object.keys(live.config.tasks).length;
|
|
162
|
+
if (events.length > 0) {
|
|
163
|
+
console.log(` [drain] ${events.length} events, ${done}/${total} done, eligible: [${result.eligible.join(', ')}]`);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
graphRef = graph;
|
|
168
|
+
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// 4. T0: Empty board
|
|
171
|
+
// ============================================================================
|
|
172
|
+
|
|
173
|
+
console.log('=== Portfolio Tracker ===');
|
|
174
|
+
console.log(`Tasks: ${Object.keys(config.tasks).join(' → ')}`);
|
|
175
|
+
console.log(`portfolio-form has NO handler — externally driven\n`);
|
|
176
|
+
|
|
177
|
+
console.log('--- T0: Empty board ---');
|
|
178
|
+
console.log(' No holdings yet. Standing by.\n');
|
|
179
|
+
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// 5. T1: User submits 2 holdings → external push for portfolio-form
|
|
182
|
+
// ============================================================================
|
|
183
|
+
|
|
184
|
+
console.log('--- T1: User adds AAPL (50 shares) and MSFT (30 shares) ---\n');
|
|
185
|
+
|
|
186
|
+
// External push — portfolio-form completes with data.
|
|
187
|
+
// The reactive layer auto-hashes the data payload.
|
|
188
|
+
// data-changed cascade triggers: price-fetch → holdings-table → portfolio-value
|
|
189
|
+
graph.push({
|
|
190
|
+
type: 'task-completed',
|
|
191
|
+
taskName: 'portfolio-form',
|
|
192
|
+
data: {
|
|
193
|
+
holdings: [
|
|
194
|
+
{ symbol: 'AAPL', qty: 50 },
|
|
195
|
+
{ symbol: 'MSFT', qty: 30 },
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
timestamp: new Date().toISOString(),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await sleep(2000);
|
|
202
|
+
printState('T1');
|
|
203
|
+
|
|
204
|
+
// ============================================================================
|
|
205
|
+
// 6. T2: User adds a 3rd holding → new data, new hash → cascade
|
|
206
|
+
// ============================================================================
|
|
207
|
+
|
|
208
|
+
console.log('\n--- T2: User adds GOOG (100 shares) ---\n');
|
|
209
|
+
|
|
210
|
+
// Same task, new data. Auto-hash differs → data-changed re-triggers downstream.
|
|
211
|
+
graph.push({
|
|
212
|
+
type: 'task-completed',
|
|
213
|
+
taskName: 'portfolio-form',
|
|
214
|
+
data: {
|
|
215
|
+
holdings: [
|
|
216
|
+
{ symbol: 'AAPL', qty: 50 },
|
|
217
|
+
{ symbol: 'MSFT', qty: 30 },
|
|
218
|
+
{ symbol: 'GOOG', qty: 100 },
|
|
219
|
+
],
|
|
220
|
+
},
|
|
221
|
+
timestamp: new Date().toISOString(),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await sleep(2000);
|
|
225
|
+
printState('T2');
|
|
226
|
+
|
|
227
|
+
// ============================================================================
|
|
228
|
+
// 7. T3: Force price refresh — retrigger without form change
|
|
229
|
+
// ============================================================================
|
|
230
|
+
|
|
231
|
+
console.log('\n--- T3: Force price refresh via retrigger ---\n');
|
|
232
|
+
|
|
233
|
+
// Simulate a price change
|
|
234
|
+
marketPrices.AAPL = 205.00;
|
|
235
|
+
console.log(' [simulated] AAPL price changed to $205.00');
|
|
236
|
+
|
|
237
|
+
// retrigger pushes a task-restart event through the engine
|
|
238
|
+
graph.retrigger('price-fetch');
|
|
239
|
+
|
|
240
|
+
await sleep(2000);
|
|
241
|
+
printState('T3');
|
|
242
|
+
|
|
243
|
+
// ============================================================================
|
|
244
|
+
// 8. T4: Quiescent
|
|
245
|
+
// ============================================================================
|
|
246
|
+
|
|
247
|
+
console.log('\n--- T4: No changes — board quiescent ---');
|
|
248
|
+
const sched = graph.getSchedule();
|
|
249
|
+
console.log(` Eligible tasks: ${sched.eligible.length === 0 ? 'none' : sched.eligible.join(', ')}`);
|
|
250
|
+
|
|
251
|
+
// ============================================================================
|
|
252
|
+
// 9. Validate
|
|
253
|
+
// ============================================================================
|
|
254
|
+
|
|
255
|
+
console.log('\n--- Validation ---');
|
|
256
|
+
const validation = validateReactiveGraph({ graph, handlers });
|
|
257
|
+
console.log(` Valid: ${validation.valid} (${validation.issues.length} issues)`);
|
|
258
|
+
for (const issue of validation.issues) {
|
|
259
|
+
console.log(` [${issue.severity}] ${issue.code}: ${issue.message}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
graph.dispose();
|
|
263
|
+
console.log('\nDone.');
|
|
264
|
+
|
|
265
|
+
// ============================================================================
|
|
266
|
+
// Helpers
|
|
267
|
+
// ============================================================================
|
|
268
|
+
|
|
269
|
+
function printState(label: string): void {
|
|
270
|
+
console.log(`\n--- ${label} Result ---`);
|
|
271
|
+
const state = graph.getState();
|
|
272
|
+
for (const [name, task] of Object.entries(state.state.tasks)) {
|
|
273
|
+
const hash = task.lastDataHash ? ` (hash: ${task.lastDataHash.slice(0, 8)}…)` : '';
|
|
274
|
+
console.log(` ${name}: ${task.status} (${task.executionCount}x)${hash}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Read portfolio value directly from engine state — no sharedState needed
|
|
278
|
+
const valueData = state.state.tasks['portfolio-value']?.data as
|
|
279
|
+
{ totalValue?: number } | undefined;
|
|
280
|
+
if (valueData?.totalValue != null) {
|
|
281
|
+
console.log(`\n Portfolio Value: $${valueData.totalValue.toFixed(2)}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function sleep(ms: number): Promise<void> {
|
|
286
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
287
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive Graph Example: Live Monitoring Dashboard
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates the reactive graph's advanced features:
|
|
5
|
+
* - Self-sustaining execution (no loops, no daemon)
|
|
6
|
+
* - createCallbackHandler + createFireAndForgetHandler
|
|
7
|
+
* - Adding nodes at runtime with handlers
|
|
8
|
+
* - Conditional routing (on)
|
|
9
|
+
* - Handler failure → core engine on_failure tokens
|
|
10
|
+
* - Auto dataHash (no explicit hash in handlers)
|
|
11
|
+
* - validateReactiveGraph for handler/dispatch checks
|
|
12
|
+
* - Observability via onDrain
|
|
13
|
+
* - Journal-based event batching
|
|
14
|
+
*
|
|
15
|
+
* Scenario: A monitoring system that collects metrics, evaluates alerts,
|
|
16
|
+
* and dynamically adds notification channels at runtime.
|
|
17
|
+
*
|
|
18
|
+
* Run with: npx tsx examples/continuous-event-graph/reactive-monitoring.ts
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
createReactiveGraph,
|
|
23
|
+
MemoryJournal,
|
|
24
|
+
createCallbackHandler,
|
|
25
|
+
createFireAndForgetHandler,
|
|
26
|
+
validateReactiveGraph,
|
|
27
|
+
} from '../../src/continuous-event-graph/index.js';
|
|
28
|
+
import type { GraphConfig, TaskConfig } from '../../src/continuous-event-graph/types.js';
|
|
29
|
+
import type { TaskHandlerFn, ReactiveGraph } from '../../src/continuous-event-graph/reactive.js';
|
|
30
|
+
import type { ResolveCallbackFn } from '../../src/continuous-event-graph/handlers.js';
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// 1. Define the initial graph
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
const config: GraphConfig = {
|
|
37
|
+
id: 'monitoring-dashboard',
|
|
38
|
+
settings: {
|
|
39
|
+
completion: 'manual', // continuous — never auto-completes
|
|
40
|
+
execution_mode: 'eligibility-mode',
|
|
41
|
+
},
|
|
42
|
+
tasks: {
|
|
43
|
+
collect_cpu: {
|
|
44
|
+
provides: ['cpu-metrics'],
|
|
45
|
+
taskHandlers: ['collect_cpu'],
|
|
46
|
+
description: 'Collect CPU utilization metrics',
|
|
47
|
+
},
|
|
48
|
+
collect_memory: {
|
|
49
|
+
provides: ['memory-metrics'],
|
|
50
|
+
taskHandlers: ['collect_memory'],
|
|
51
|
+
description: 'Collect memory usage metrics',
|
|
52
|
+
},
|
|
53
|
+
evaluate_health: {
|
|
54
|
+
requires: ['cpu-metrics', 'memory-metrics'],
|
|
55
|
+
provides: ['health-status'],
|
|
56
|
+
taskHandlers: ['evaluate_health'],
|
|
57
|
+
on: {
|
|
58
|
+
healthy: ['system-ok'],
|
|
59
|
+
degraded: ['system-degraded'],
|
|
60
|
+
critical: ['system-critical'],
|
|
61
|
+
},
|
|
62
|
+
description: 'Evaluate system health from all metrics',
|
|
63
|
+
},
|
|
64
|
+
alert_oncall: {
|
|
65
|
+
requires: ['system-critical'],
|
|
66
|
+
provides: ['oncall-notified'],
|
|
67
|
+
taskHandlers: ['alert_oncall'],
|
|
68
|
+
description: 'Page the on-call engineer',
|
|
69
|
+
},
|
|
70
|
+
log_status: {
|
|
71
|
+
requires: ['system-ok'],
|
|
72
|
+
provides: ['status-logged'],
|
|
73
|
+
taskHandlers: ['log_status'],
|
|
74
|
+
description: 'Log healthy status for audit',
|
|
75
|
+
},
|
|
76
|
+
scale_up: {
|
|
77
|
+
requires: ['system-degraded'],
|
|
78
|
+
provides: ['scaled-up'],
|
|
79
|
+
taskHandlers: ['scale_up'],
|
|
80
|
+
on_failure: ['scale-failed'],
|
|
81
|
+
description: 'Auto-scale infrastructure',
|
|
82
|
+
},
|
|
83
|
+
escalate: {
|
|
84
|
+
requires: ['scale-failed'],
|
|
85
|
+
provides: ['escalated'],
|
|
86
|
+
taskHandlers: ['escalate'],
|
|
87
|
+
description: 'Escalate to platform team when auto-scale fails',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// 2. Simulated metrics
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
let cpuLoad = 45; // start normal
|
|
97
|
+
let memoryUsage = 60;
|
|
98
|
+
|
|
99
|
+
function simulateMetrics(): { cpu: number; memory: number } {
|
|
100
|
+
// Gradually increase load to trigger different paths
|
|
101
|
+
cpuLoad = Math.min(99, cpuLoad + Math.floor(Math.random() * 15));
|
|
102
|
+
memoryUsage = Math.min(95, memoryUsage + Math.floor(Math.random() * 10));
|
|
103
|
+
return { cpu: cpuLoad, memory: memoryUsage };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// 3. Create reactive graph
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
console.log('=== Reactive Monitoring Dashboard ===\n');
|
|
111
|
+
|
|
112
|
+
// Lazy resolver — graph doesn't exist at handler-creation time
|
|
113
|
+
let graphRef: ReactiveGraph;
|
|
114
|
+
const getResolve = (): ResolveCallbackFn => graphRef.resolveCallback.bind(graphRef);
|
|
115
|
+
|
|
116
|
+
// Handlers use createCallbackHandler for data-producing tasks and
|
|
117
|
+
// createFireAndForgetHandler for side-effect-only tasks.
|
|
118
|
+
// No explicit dataHash — the reactive layer auto-computes from data.
|
|
119
|
+
|
|
120
|
+
const handlers: Record<string, TaskHandlerFn> = {
|
|
121
|
+
// Data-producing handlers — return data and let auto-hash do the rest
|
|
122
|
+
collect_cpu: createCallbackHandler(async ({ nodeId }) => {
|
|
123
|
+
const { cpu } = simulateMetrics();
|
|
124
|
+
console.log(` [${nodeId}] CPU: ${cpu}%`);
|
|
125
|
+
return { cpu };
|
|
126
|
+
}, getResolve),
|
|
127
|
+
|
|
128
|
+
collect_memory: createCallbackHandler(async ({ nodeId }) => {
|
|
129
|
+
const { memory } = simulateMetrics();
|
|
130
|
+
console.log(` [${nodeId}] Memory: ${memory}%`);
|
|
131
|
+
return { memory };
|
|
132
|
+
}, getResolve),
|
|
133
|
+
|
|
134
|
+
// evaluate_health uses conditional routing (on: { healthy, degraded, critical })
|
|
135
|
+
// resolveCallback doesn't support `result`, so we push directly to the graph
|
|
136
|
+
evaluate_health: async ({ nodeId, callbackToken }) => {
|
|
137
|
+
setTimeout(() => {
|
|
138
|
+
let status: string;
|
|
139
|
+
if (cpuLoad > 90 || memoryUsage > 90) {
|
|
140
|
+
status = 'critical';
|
|
141
|
+
} else if (cpuLoad > 70 || memoryUsage > 75) {
|
|
142
|
+
status = 'degraded';
|
|
143
|
+
} else {
|
|
144
|
+
status = 'healthy';
|
|
145
|
+
}
|
|
146
|
+
console.log(` [${nodeId}] Health: ${status.toUpperCase()} (CPU=${cpuLoad}%, Mem=${memoryUsage}%)`);
|
|
147
|
+
// Use graph.push directly for conditional routing (result field)
|
|
148
|
+
graphRef.push({
|
|
149
|
+
type: 'task-completed',
|
|
150
|
+
taskName: nodeId,
|
|
151
|
+
result: status,
|
|
152
|
+
data: { status, cpu: cpuLoad, memory: memoryUsage },
|
|
153
|
+
timestamp: new Date().toISOString(),
|
|
154
|
+
});
|
|
155
|
+
}, 50);
|
|
156
|
+
return 'task-initiated';
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
// Side-effect-only handlers — fire and forget (logging, alerting)
|
|
160
|
+
alert_oncall: createFireAndForgetHandler(async ({ nodeId }) => {
|
|
161
|
+
console.log(` [${nodeId}] 🚨 PAGING ON-CALL: System critical!`);
|
|
162
|
+
await sleep(100);
|
|
163
|
+
}, getResolve),
|
|
164
|
+
|
|
165
|
+
log_status: createFireAndForgetHandler(({ nodeId }) => {
|
|
166
|
+
console.log(` [${nodeId}] ✅ System healthy — logged.`);
|
|
167
|
+
}, getResolve),
|
|
168
|
+
|
|
169
|
+
scale_up: createCallbackHandler(async ({ nodeId }) => {
|
|
170
|
+
console.log(` [${nodeId}] ⚡ Scaling up infrastructure...`);
|
|
171
|
+
await sleep(100);
|
|
172
|
+
// Simulate scale failure 50% of the time
|
|
173
|
+
if (Math.random() > 0.5) {
|
|
174
|
+
throw new Error('Auto-scale service unavailable');
|
|
175
|
+
}
|
|
176
|
+
console.log(` [${nodeId}] Scaled up successfully.`);
|
|
177
|
+
return {};
|
|
178
|
+
}, getResolve),
|
|
179
|
+
|
|
180
|
+
escalate: createFireAndForgetHandler(({ nodeId }) => {
|
|
181
|
+
console.log(` [${nodeId}] 📢 Escalating to platform team — auto-scale failed.`);
|
|
182
|
+
}, getResolve),
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const rg = createReactiveGraph(config, {
|
|
186
|
+
handlers,
|
|
187
|
+
|
|
188
|
+
onDrain: (events, live, result) => {
|
|
189
|
+
const statuses = Object.entries(live.state.tasks)
|
|
190
|
+
.map(([n, s]) => `${n}=${s.status}`)
|
|
191
|
+
.join(', ');
|
|
192
|
+
console.log(` [drain] ${events.length} events | eligible: [${result.eligible.join(', ')}]`);
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
graphRef = rg;
|
|
196
|
+
|
|
197
|
+
// ============================================================================
|
|
198
|
+
// 4. Kick it off — one push, the graph drives itself
|
|
199
|
+
// ============================================================================
|
|
200
|
+
|
|
201
|
+
console.log('Phase 1: Initial metrics collection\n');
|
|
202
|
+
rg.push({ type: 'inject-tokens', tokens: [], timestamp: new Date().toISOString() });
|
|
203
|
+
|
|
204
|
+
await sleep(1000);
|
|
205
|
+
|
|
206
|
+
// ============================================================================
|
|
207
|
+
// 5. Add a Slack notification node at runtime
|
|
208
|
+
// ============================================================================
|
|
209
|
+
|
|
210
|
+
console.log('\n--- Adding Slack notification node at runtime ---\n');
|
|
211
|
+
|
|
212
|
+
const slackConfig: TaskConfig = {
|
|
213
|
+
requires: ['system-degraded'],
|
|
214
|
+
provides: ['slack-notified'],
|
|
215
|
+
taskHandlers: ['notify_slack'],
|
|
216
|
+
description: 'Send alert to #incidents Slack channel',
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// Use createFireAndForgetHandler — Slack notification is a side-effect
|
|
220
|
+
const slackHandler: TaskHandlerFn = createFireAndForgetHandler(({ nodeId }) => {
|
|
221
|
+
console.log(` [${nodeId}] 💬 Slack → #incidents: System degraded, auto-scaling...`);
|
|
222
|
+
}, getResolve);
|
|
223
|
+
|
|
224
|
+
rg.registerHandler('notify_slack', slackHandler);
|
|
225
|
+
rg.addNode('notify_slack', slackConfig);
|
|
226
|
+
|
|
227
|
+
await sleep(500);
|
|
228
|
+
|
|
229
|
+
// ============================================================================
|
|
230
|
+
// 6. Show final state
|
|
231
|
+
// ============================================================================
|
|
232
|
+
|
|
233
|
+
console.log('\n=== Final State ===\n');
|
|
234
|
+
|
|
235
|
+
const state = rg.getState();
|
|
236
|
+
for (const [name, task] of Object.entries(state.state.tasks)) {
|
|
237
|
+
const hash = task.lastDataHash ? ` (hash: ${task.lastDataHash})` : '';
|
|
238
|
+
console.log(` ${name}: ${task.status} [${task.executionCount}x]${hash}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.log(`\n Outputs: [${state.state.availableOutputs.join(', ')}]`);
|
|
242
|
+
|
|
243
|
+
// ============================================================================
|
|
244
|
+
// 7. Validate reactive graph consistency
|
|
245
|
+
// ============================================================================
|
|
246
|
+
|
|
247
|
+
console.log('\n=== Validation ===');
|
|
248
|
+
const validation = validateReactiveGraph({
|
|
249
|
+
graph: rg,
|
|
250
|
+
handlers: { ...handlers, notify_slack: slackHandler },
|
|
251
|
+
});
|
|
252
|
+
console.log(` Valid: ${validation.valid} (${validation.issues.length} issues)`);
|
|
253
|
+
for (const issue of validation.issues) {
|
|
254
|
+
console.log(` [${issue.severity}] ${issue.code}: ${issue.message}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
rg.dispose();
|
|
258
|
+
|
|
259
|
+
// ============================================================================
|
|
260
|
+
// Util
|
|
261
|
+
// ============================================================================
|
|
262
|
+
|
|
263
|
+
function sleep(ms: number): Promise<void> {
|
|
264
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
265
|
+
}
|