@devilsdev/rag-pipeline-utils 2.1.12 → 2.2.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/.ragrc.schema.json +218 -0
- package/README.md +178 -550
- package/bin/cli.js +8 -14
- package/contracts/reranker-contract.json +69 -0
- package/dist/index.d.ts +237 -0
- package/dist/index.mjs +22 -7
- package/package.json +18 -13
- package/src/ai/adaptive-retrieval.js +191 -0
- package/src/ai/federated-learning.js +214 -0
- package/src/ai/federation/federated-learning-coordinator.js +345 -0
- package/src/ai/federation.js +76 -0
- package/src/ai/index-fixed.js +509 -0
- package/src/ai/index.js +33 -0
- package/src/ai/model-trainer.js +217 -0
- package/src/ai/model-training.js +205 -0
- package/src/ai/module-template.js +37 -0
- package/src/ai/multimodal/multi-modal-processor.js +345 -0
- package/src/ai/multimodal-processing.js +268 -0
- package/src/ai/multimodal.js +71 -0
- package/src/ai/orchestrator.js +70 -0
- package/src/ai/retrieval/adaptive-retrieval-engine.js +398 -0
- package/src/ai/retrieval-engine.js +69 -0
- package/src/ai/training/model-training-orchestrator.js +228 -0
- package/src/ci/workflow-analyzer.js +71 -0
- package/src/cli/commands/ai-ml.js +967 -0
- package/src/cli/commands/docs.js +228 -0
- package/src/cli/commands/dx.js +538 -0
- package/src/cli/commands/plugin-hub.js +875 -0
- package/src/cli/doctor-command.js +639 -0
- package/src/cli/enhanced-cli-commands.js +945 -0
- package/src/cli/interactive-wizard.js +997 -0
- package/src/cli/plugin-marketplace-commands.js +869 -0
- package/src/cli.js +17 -0
- package/src/config/enhanced-ragrc-schema.js +632 -0
- package/src/config/load-config.js +54 -0
- package/src/config/load-plugin-config.js +69 -0
- package/src/config/normalize-config.js +105 -0
- package/src/config/schema-validator.js +315 -0
- package/src/config/validate-plugin-schema.js +35 -0
- package/src/config/validate-schema.js +113 -0
- package/src/contracts/plugin-contracts.js +33 -0
- package/src/core/create-pipeline.js +99 -0
- package/src/core/dependency-injection.js +242 -0
- package/src/core/observability/event-logger.js +241 -0
- package/src/core/observability/instrumented-pipeline.js +609 -0
- package/src/core/observability/metrics.js +497 -0
- package/src/core/observability/tracing.js +781 -0
- package/src/core/performance/benchmark.js +516 -0
- package/src/core/performance/parallel-processor.js +240 -0
- package/src/core/performance/streaming-safeguards.js +620 -0
- package/src/core/pipeline-factory.js +60 -0
- package/src/core/plugin-contracts.js +89 -0
- package/src/core/plugin-marketplace/plugin-metadata.js +485 -0
- package/src/core/plugin-marketplace/plugin-publisher.js +574 -0
- package/src/core/plugin-marketplace/plugin-registry-format.js +419 -0
- package/src/core/plugin-marketplace/version-resolver.js +61 -0
- package/src/core/plugin-registry.js +294 -0
- package/src/dag/dag-engine.js +954 -0
- package/src/dag/dag-node.js +76 -0
- package/src/dag/enhanced-dag-engine.js +822 -0
- package/src/dx/index.js +40 -0
- package/src/dx/integration-templates.js +665 -0
- package/src/dx/performance-profiler.js +968 -0
- package/src/dx/realtime-debugger.js +755 -0
- package/src/dx/visual-pipeline-builder.js +578 -0
- package/src/ecosystem/plugin-analytics-dashboard.js +778 -0
- package/src/ecosystem/plugin-certification.js +580 -0
- package/src/ecosystem/plugin-hub.js +821 -0
- package/src/enterprise/audit-logging.js +967 -0
- package/src/enterprise/data-governance.js +408 -0
- package/src/enterprise/multi-tenancy.js +496 -0
- package/src/enterprise/sso-integration.js +1030 -0
- package/src/evaluate/evaluator.js +52 -0
- package/src/evaluate/scoring.js +91 -0
- package/src/index.js +67 -0
- package/src/ingest.js +59 -0
- package/src/loader/csv-loader.js +38 -0
- package/src/loader/directory-loader.js +48 -0
- package/src/loader/html-loader.js +62 -0
- package/src/loader/markdown-loader.js +67 -0
- package/src/mocks/openai-embedder.js +119 -0
- package/src/mocks/openai-llm.js +77 -0
- package/src/mocks/pdf-loader.js +21 -0
- package/src/mocks/pinecone-retriever.js +106 -0
- package/src/observability/audit-logger.js +758 -0
- package/src/observability/dashboard-config.json +675 -0
- package/src/observability/event-logger.js +310 -0
- package/src/observability/metrics.js +691 -0
- package/src/observability/security-monitor.js +830 -0
- package/src/observability/slo-monitor.js +319 -0
- package/src/observability/telemetry.js +686 -0
- package/src/pipeline/pipeline.js +18 -0
- package/src/plugins/sample-embedder.js +53 -0
- package/src/plugins/sample-llm.js +30 -0
- package/src/plugins/sample-loader.js +24 -0
- package/src/plugins/sample-retriever.js +84 -0
- package/src/query.js +117 -0
- package/src/reranker/llm-reranker.js +111 -0
- package/src/scripts/script-utilities.js +36 -0
- package/src/security/plugin-sandbox.js +586 -0
- package/src/security/plugin-signature-verifier.js +414 -0
- package/src/security/validator.js +36 -0
- package/src/utils/ci/diagnostic-reporter.js +57 -0
- package/src/utils/logger.js +211 -0
- package/src/utils/performance-monitor.js +561 -0
- package/src/utils/plugin-scaffolder.js +447 -0
- package/src/utils/rate-limiter.js +244 -0
- package/src/utils/retry-policy.js +443 -0
- package/src/utils/retry.js +107 -0
- package/src/utils/safe-expression-evaluator.js +579 -0
- package/src/utils/secure-logging.js +338 -0
- package/src/utils/structured-logger.js +388 -0
- package/src/utils/validate-plugin-contract.js +42 -0
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version: 2.0.2
|
|
3
|
+
* Path: /src/dag/dag-engine.js
|
|
4
|
+
* Description: DAG executor for chained RAG pipelines with cycle detection, error handling, and optional concurrency
|
|
5
|
+
* Author: Ali Kahwaji
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const DAGNode = require('./dag-node.js');
|
|
9
|
+
|
|
10
|
+
class DAG {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.nodes = new Map();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
addNode(id, _fn, options = {}) {
|
|
16
|
+
if (this.nodes.has(id)) {
|
|
17
|
+
throw new Error(`Node with ID "${id}" already exists`);
|
|
18
|
+
}
|
|
19
|
+
const node = new DAGNode(id, _fn, options);
|
|
20
|
+
this.nodes.set(id, node);
|
|
21
|
+
return node;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
connect(fromId, toId) {
|
|
25
|
+
const from = this.nodes.get(fromId);
|
|
26
|
+
const to = this.nodes.get(toId);
|
|
27
|
+
if (!from || !to) throw new Error(`Invalid edge: ${fromId} → ${toId}`);
|
|
28
|
+
from.outputs.push(to);
|
|
29
|
+
to.inputs.push(from);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse and normalize execute parameters
|
|
34
|
+
* @param {any} seedOrOptions - Seed data or options object
|
|
35
|
+
* @param {Object} _options - Additional options (if first param is seed)
|
|
36
|
+
* @returns {Object} - Normalized {seed, options} object
|
|
37
|
+
*/
|
|
38
|
+
_parseExecuteParams(seedOrOptions, _options = {}) {
|
|
39
|
+
// Handle parameter overloading
|
|
40
|
+
let seed, options;
|
|
41
|
+
if (
|
|
42
|
+
seedOrOptions &&
|
|
43
|
+
typeof seedOrOptions === 'object' &&
|
|
44
|
+
!Array.isArray(seedOrOptions) &&
|
|
45
|
+
('concurrency' in seedOrOptions ||
|
|
46
|
+
'timeout' in seedOrOptions ||
|
|
47
|
+
'continueOnError' in seedOrOptions ||
|
|
48
|
+
'enableCheckpoints' in seedOrOptions ||
|
|
49
|
+
'checkpointId' in seedOrOptions ||
|
|
50
|
+
'requiredNodes' in seedOrOptions ||
|
|
51
|
+
'returnFormat' in seedOrOptions ||
|
|
52
|
+
'retryFailedNodes' in seedOrOptions ||
|
|
53
|
+
'maxRetries' in seedOrOptions ||
|
|
54
|
+
'gracefulDegradation' in seedOrOptions)
|
|
55
|
+
) {
|
|
56
|
+
// First parameter is options
|
|
57
|
+
options = seedOrOptions;
|
|
58
|
+
seed = options.seed;
|
|
59
|
+
} else {
|
|
60
|
+
// First parameter is seed
|
|
61
|
+
seed = seedOrOptions;
|
|
62
|
+
options = _options;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Extract options with defaults
|
|
66
|
+
const normalizedOptions = {
|
|
67
|
+
concurrency: parseInt(process.env.RAG_MAX_CONCURRENCY, 10) || null,
|
|
68
|
+
timeout: null,
|
|
69
|
+
continueOnError: false,
|
|
70
|
+
gracefulDegradation: false,
|
|
71
|
+
enableCheckpoints: false,
|
|
72
|
+
checkpointId: null,
|
|
73
|
+
requiredNodes: [],
|
|
74
|
+
returnFormat: 'auto',
|
|
75
|
+
retryFailedNodes: false,
|
|
76
|
+
maxRetries: 3,
|
|
77
|
+
...options,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return { seed, options: normalizedOptions };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Execute all nodes in topological order with concurrency control
|
|
85
|
+
* @param {Array} order - Topologically sorted nodes
|
|
86
|
+
* @param {Object} context - Execution context
|
|
87
|
+
* @returns {Promise<void>}
|
|
88
|
+
*/
|
|
89
|
+
async _runAllNodes(order, context) {
|
|
90
|
+
const {
|
|
91
|
+
concurrency,
|
|
92
|
+
results,
|
|
93
|
+
errors,
|
|
94
|
+
fwd,
|
|
95
|
+
continueOnError,
|
|
96
|
+
requiredIds,
|
|
97
|
+
seed,
|
|
98
|
+
gracefulDegradation,
|
|
99
|
+
requiredNodes,
|
|
100
|
+
retryFailedNodes,
|
|
101
|
+
maxRetries,
|
|
102
|
+
} = context;
|
|
103
|
+
|
|
104
|
+
const semaphore = concurrency ? new Array(concurrency).fill(null) : null;
|
|
105
|
+
let semaphoreIndex = 0;
|
|
106
|
+
|
|
107
|
+
const executeWithSemaphore = async (node) => {
|
|
108
|
+
if (semaphore) {
|
|
109
|
+
// Wait for available slot
|
|
110
|
+
while (semaphore[semaphoreIndex % semaphore.length] !== null) {
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
112
|
+
}
|
|
113
|
+
const slotIndex = semaphoreIndex % semaphore.length;
|
|
114
|
+
semaphoreIndex++;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const result = await this.executeNode(node, {
|
|
118
|
+
results,
|
|
119
|
+
errors,
|
|
120
|
+
fwd,
|
|
121
|
+
continueOnError,
|
|
122
|
+
requiredIds,
|
|
123
|
+
seed,
|
|
124
|
+
gracefulDegradation,
|
|
125
|
+
requiredNodes,
|
|
126
|
+
retryFailedNodes,
|
|
127
|
+
maxRetries,
|
|
128
|
+
});
|
|
129
|
+
// Only set result if it's not undefined (failed optional nodes return undefined)
|
|
130
|
+
if (result !== undefined) {
|
|
131
|
+
results.set(node.id, result);
|
|
132
|
+
}
|
|
133
|
+
} finally {
|
|
134
|
+
semaphore[slotIndex] = null;
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
const result = await this.executeNode(node, {
|
|
138
|
+
results,
|
|
139
|
+
errors,
|
|
140
|
+
fwd,
|
|
141
|
+
continueOnError,
|
|
142
|
+
requiredIds,
|
|
143
|
+
seed,
|
|
144
|
+
gracefulDegradation,
|
|
145
|
+
requiredNodes,
|
|
146
|
+
retryFailedNodes,
|
|
147
|
+
maxRetries,
|
|
148
|
+
});
|
|
149
|
+
// Only set result if it's not the special failed-optional-node symbol
|
|
150
|
+
if (
|
|
151
|
+
typeof result !== 'symbol' ||
|
|
152
|
+
result.description !== 'failed-optional-node'
|
|
153
|
+
) {
|
|
154
|
+
results.set(node.id, result);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Execute nodes respecting dependencies
|
|
160
|
+
for (const node of order) {
|
|
161
|
+
// Check if all dependencies are satisfied
|
|
162
|
+
const canExecute = this._canExecuteNode(node, {
|
|
163
|
+
results,
|
|
164
|
+
errors,
|
|
165
|
+
gracefulDegradation,
|
|
166
|
+
requiredNodes,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (canExecute) {
|
|
170
|
+
await executeWithSemaphore(node);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Check if a node can be executed based on dependency satisfaction
|
|
177
|
+
* @param {DAGNode} node - Node to check
|
|
178
|
+
* @param {Object} context - Execution context
|
|
179
|
+
* @returns {boolean}
|
|
180
|
+
*/
|
|
181
|
+
_canExecuteNode(node, context) {
|
|
182
|
+
const { results, errors, gracefulDegradation, requiredNodes } = context;
|
|
183
|
+
|
|
184
|
+
return node.inputs.every((dep) => {
|
|
185
|
+
// If dependency has result or error, it's satisfied
|
|
186
|
+
if (results.has(dep.id) || errors.has(dep.id)) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
// If graceful degradation is enabled and dependency is not required, it's satisfied
|
|
190
|
+
if (
|
|
191
|
+
gracefulDegradation &&
|
|
192
|
+
requiredNodes &&
|
|
193
|
+
!requiredNodes.includes(dep.id)
|
|
194
|
+
) {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Initialize execution state and validate DAG
|
|
203
|
+
* @returns {Object} - Execution state object
|
|
204
|
+
*/
|
|
205
|
+
_initializeExecutionState() {
|
|
206
|
+
this.validate();
|
|
207
|
+
|
|
208
|
+
// Initialize execution state
|
|
209
|
+
const results = new Map();
|
|
210
|
+
const errors = new Map();
|
|
211
|
+
const order = this.topoSort();
|
|
212
|
+
const { fwd, rev } = this._buildAdjacency();
|
|
213
|
+
const requiredIds = this._getRequiredIds(fwd, rev);
|
|
214
|
+
|
|
215
|
+
return { results, errors, order, fwd, rev, requiredIds };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Handle execution errors with appropriate wrapping
|
|
220
|
+
* @param {Error} error - Error to handle
|
|
221
|
+
* @throws {Error} - Appropriately wrapped error
|
|
222
|
+
*/
|
|
223
|
+
_handleExecutionError(error) {
|
|
224
|
+
// Error policy: wrap everything in try/catch
|
|
225
|
+
// If caught error is a node error (message starts with "Node " OR error.nodeId) → rethrow unchanged
|
|
226
|
+
if (error.message.startsWith('Node ') || error.nodeId) {
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// If caught error is validation or aggregate → wrap once
|
|
231
|
+
if (
|
|
232
|
+
error.cycle ||
|
|
233
|
+
/^DAG validation failed/.test(error.message) ||
|
|
234
|
+
error.errors
|
|
235
|
+
) {
|
|
236
|
+
const wrappedError = new Error(`DAG execution failed: ${error.message}`);
|
|
237
|
+
if (error.cycle) wrappedError.cycle = error.cycle;
|
|
238
|
+
if (error.errors) wrappedError.errors = error.errors;
|
|
239
|
+
if (error.nodeId) wrappedError.nodeId = error.nodeId;
|
|
240
|
+
if (error.timestamp) wrappedError.timestamp = error.timestamp;
|
|
241
|
+
throw wrappedError;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Do NOT wrap timeout errors or 'no sink nodes' errors
|
|
245
|
+
if (
|
|
246
|
+
error.message === 'Execution timeout' ||
|
|
247
|
+
error.message === 'DAG has no sink nodes - no final output available'
|
|
248
|
+
) {
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Otherwise wrap with 'DAG execution failed: ...'
|
|
253
|
+
const wrappedError = new Error(`DAG execution failed: ${error.message}`);
|
|
254
|
+
if (error.errors) wrappedError.errors = error.errors;
|
|
255
|
+
if (error.nodeId) wrappedError.nodeId = error.nodeId;
|
|
256
|
+
if (error.timestamp) wrappedError.timestamp = error.timestamp;
|
|
257
|
+
if (error.cycle) wrappedError.cycle = error.cycle;
|
|
258
|
+
throw wrappedError;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Execute function with optional timeout
|
|
263
|
+
* @param {Function} runFn - Function to execute
|
|
264
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
265
|
+
* @returns {Promise<void>}
|
|
266
|
+
*/
|
|
267
|
+
async _executeWithTimeout(runFn, timeout) {
|
|
268
|
+
if (timeout && timeout > 0) {
|
|
269
|
+
await Promise.race([
|
|
270
|
+
runFn(),
|
|
271
|
+
new Promise((_, reject) => {
|
|
272
|
+
setTimeout(() => reject(new Error('Execution timeout')), timeout);
|
|
273
|
+
}),
|
|
274
|
+
]);
|
|
275
|
+
} else {
|
|
276
|
+
await runFn();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Process execution results and handle checkpointing
|
|
282
|
+
* @param {Map} results - Node execution results
|
|
283
|
+
* @param {Map} errors - Node execution errors
|
|
284
|
+
* @param {Map} fwd - Forward adjacency map
|
|
285
|
+
* @param {Object} options - Processing options
|
|
286
|
+
* @returns {any} - Processed results
|
|
287
|
+
*/
|
|
288
|
+
_processResults(results, errors, fwd, options) {
|
|
289
|
+
const {
|
|
290
|
+
gracefulDegradation,
|
|
291
|
+
retryFailedNodes,
|
|
292
|
+
enableCheckpoints,
|
|
293
|
+
checkpointId,
|
|
294
|
+
} = options;
|
|
295
|
+
|
|
296
|
+
// Check for multiple errors first
|
|
297
|
+
if (errors.size >= 2) {
|
|
298
|
+
const agg = new Error('Multiple execution errors');
|
|
299
|
+
agg.errors = [...errors.values()].map((x) => x.cause || x);
|
|
300
|
+
throw agg;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Determine sink nodes and successful sinks
|
|
304
|
+
const sinkIds = this._getSinkIds(fwd);
|
|
305
|
+
|
|
306
|
+
if (sinkIds.length === 0) {
|
|
307
|
+
// Check for single error first before generic "no sink nodes"
|
|
308
|
+
if (errors.size === 1) {
|
|
309
|
+
throw [...errors.values()][0];
|
|
310
|
+
}
|
|
311
|
+
throw new Error('DAG has no sink nodes - no final output available');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const successfulSinks = sinkIds.filter((id) => results.has(id));
|
|
315
|
+
|
|
316
|
+
if (successfulSinks.length === 0) {
|
|
317
|
+
// Check for single error first before generic "no sink nodes"
|
|
318
|
+
if (errors.size === 1) {
|
|
319
|
+
throw [...errors.values()][0];
|
|
320
|
+
}
|
|
321
|
+
throw new Error('DAG has no sink nodes - no final output available');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Save checkpoint if enabled
|
|
325
|
+
if (enableCheckpoints && checkpointId) {
|
|
326
|
+
this.saveCheckpoint(checkpointId, { results, errors });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// For graceful degradation or retry scenarios, always return object with Map-like methods
|
|
330
|
+
if (gracefulDegradation || retryFailedNodes || successfulSinks.length > 1) {
|
|
331
|
+
// Multiple successful sinks OR graceful degradation - return POJO with sink keys enumerable and non-enumerable helpers
|
|
332
|
+
const sinkEntries = successfulSinks.map((id) => [id, results.get(id)]);
|
|
333
|
+
const obj = Object.fromEntries(sinkEntries);
|
|
334
|
+
|
|
335
|
+
// Attach non-enumerable helpers backed by ALL results
|
|
336
|
+
Object.defineProperty(obj, '__allResults', {
|
|
337
|
+
value: new Map(results),
|
|
338
|
+
enumerable: false,
|
|
339
|
+
});
|
|
340
|
+
Object.defineProperty(obj, 'get', {
|
|
341
|
+
value: (k) => obj.__allResults.get(k),
|
|
342
|
+
enumerable: false,
|
|
343
|
+
});
|
|
344
|
+
Object.defineProperty(obj, 'has', {
|
|
345
|
+
value: (k) => obj.__allResults.has(k),
|
|
346
|
+
enumerable: false,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
return obj;
|
|
350
|
+
} else if (successfulSinks.length === 1) {
|
|
351
|
+
// Single successful sink without graceful degradation - return its value directly
|
|
352
|
+
return results.get(successfulSinks[0]);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Executes the DAG with optional seed data and configuration
|
|
358
|
+
* @param {any} seedOrOptions - Seed data or options object
|
|
359
|
+
* @param {Object} _options - Additional options (if first param is seed)
|
|
360
|
+
* @returns {Promise<any>} - Execution results
|
|
361
|
+
*/
|
|
362
|
+
async execute(seedOrOptions, _options = {}) {
|
|
363
|
+
const { seed, options } = this._parseExecuteParams(seedOrOptions, _options);
|
|
364
|
+
const {
|
|
365
|
+
concurrency,
|
|
366
|
+
timeout,
|
|
367
|
+
continueOnError,
|
|
368
|
+
gracefulDegradation,
|
|
369
|
+
enableCheckpoints,
|
|
370
|
+
checkpointId,
|
|
371
|
+
requiredNodes,
|
|
372
|
+
returnFormat,
|
|
373
|
+
retryFailedNodes,
|
|
374
|
+
maxRetries,
|
|
375
|
+
} = options;
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
const executionState = this._initializeExecutionState();
|
|
379
|
+
|
|
380
|
+
const { results, errors, order, fwd, requiredIds } = executionState;
|
|
381
|
+
|
|
382
|
+
// Execute nodes with optional timeout
|
|
383
|
+
await this._executeWithTimeout(
|
|
384
|
+
() =>
|
|
385
|
+
this._runAllNodes(order, {
|
|
386
|
+
concurrency,
|
|
387
|
+
results,
|
|
388
|
+
errors,
|
|
389
|
+
fwd,
|
|
390
|
+
continueOnError,
|
|
391
|
+
requiredIds,
|
|
392
|
+
seed,
|
|
393
|
+
gracefulDegradation,
|
|
394
|
+
requiredNodes,
|
|
395
|
+
retryFailedNodes,
|
|
396
|
+
maxRetries,
|
|
397
|
+
}),
|
|
398
|
+
timeout,
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
return this._processResults(results, errors, fwd, {
|
|
402
|
+
gracefulDegradation,
|
|
403
|
+
retryFailedNodes,
|
|
404
|
+
enableCheckpoints,
|
|
405
|
+
checkpointId,
|
|
406
|
+
});
|
|
407
|
+
} catch (error) {
|
|
408
|
+
this._handleExecutionError(error);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Helper for running with optional global timeout
|
|
414
|
+
* @param {Function} runFn - Function to run
|
|
415
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
416
|
+
* @returns {Promise} - Promise that resolves/rejects with timeout handling
|
|
417
|
+
*/
|
|
418
|
+
async _runWithOptionalGlobalTimeout(runFn, timeout) {
|
|
419
|
+
const run = Promise.resolve().then(runFn);
|
|
420
|
+
if (!timeout || timeout <= 0) return run;
|
|
421
|
+
return Promise.race([
|
|
422
|
+
run,
|
|
423
|
+
new Promise((_, reject) => {
|
|
424
|
+
const t = setTimeout(
|
|
425
|
+
() => reject(new Error('Execution timeout')),
|
|
426
|
+
timeout,
|
|
427
|
+
);
|
|
428
|
+
run.finally(() => clearTimeout(t));
|
|
429
|
+
}),
|
|
430
|
+
]);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Build adjacency lists for the DAG
|
|
435
|
+
* @returns {Object} - Object with fwd and rev adjacency maps
|
|
436
|
+
*/
|
|
437
|
+
_buildAdjacency() {
|
|
438
|
+
const fwd = new Map(); // node -> Set of children
|
|
439
|
+
const rev = new Map(); // node -> Set of parents
|
|
440
|
+
|
|
441
|
+
// Initialize adjacency lists for all nodes
|
|
442
|
+
for (const node of this.nodes.values()) {
|
|
443
|
+
fwd.set(node.id, new Set());
|
|
444
|
+
rev.set(node.id, new Set());
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Populate adjacency lists based on node connections
|
|
448
|
+
for (const node of this.nodes.values()) {
|
|
449
|
+
for (const output of node.outputs) {
|
|
450
|
+
fwd.get(node.id).add(output.id);
|
|
451
|
+
// Only add reverse edge if the output node exists in the DAG
|
|
452
|
+
if (rev.has(output.id)) {
|
|
453
|
+
rev.get(output.id).add(node.id);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return { fwd, rev };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Get sink node IDs (nodes with no outgoing edges)
|
|
463
|
+
* @param {Map} fwd - Forward adjacency map
|
|
464
|
+
* @returns {Array} - Array of sink node IDs
|
|
465
|
+
*/
|
|
466
|
+
_getSinkIds(fwd) {
|
|
467
|
+
const sinkIds = [];
|
|
468
|
+
for (const [nodeId, children] of fwd) {
|
|
469
|
+
if (children.size === 0) {
|
|
470
|
+
sinkIds.push(nodeId);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return sinkIds;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Get required node IDs (all sinks + all their ancestors)
|
|
478
|
+
* @param {Map} fwd - Forward adjacency map
|
|
479
|
+
* @param {Map} rev - Reverse adjacency map
|
|
480
|
+
* @returns {Set} - Set of required node IDs
|
|
481
|
+
*/
|
|
482
|
+
_getRequiredIds(fwd, rev) {
|
|
483
|
+
const sinkIds = this._getSinkIds(fwd);
|
|
484
|
+
const required = new Set();
|
|
485
|
+
const visited = new Set();
|
|
486
|
+
|
|
487
|
+
const addAncestors = (nodeId) => {
|
|
488
|
+
if (visited.has(nodeId)) return;
|
|
489
|
+
visited.add(nodeId);
|
|
490
|
+
required.add(nodeId);
|
|
491
|
+
|
|
492
|
+
const parents = rev.get(nodeId) || new Set();
|
|
493
|
+
for (const parentId of parents) {
|
|
494
|
+
addAncestors(parentId);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
for (const sinkId of sinkIds) {
|
|
499
|
+
addAncestors(sinkId);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return required;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Build adjacency list for the DAG (legacy method for compatibility)
|
|
507
|
+
* @returns {Map} - Adjacency list mapping node IDs to arrays of dependent node IDs
|
|
508
|
+
*/
|
|
509
|
+
buildAdjacencyList() {
|
|
510
|
+
const { fwd } = this._buildAdjacency();
|
|
511
|
+
const adjacency = new Map();
|
|
512
|
+
|
|
513
|
+
for (const [nodeId, children] of fwd) {
|
|
514
|
+
adjacency.set(nodeId, Array.from(children));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return adjacency;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Validates the DAG structure
|
|
522
|
+
* @returns {boolean} - True if valid
|
|
523
|
+
* @throws {Error} - If validation fails
|
|
524
|
+
*/
|
|
525
|
+
validate() {
|
|
526
|
+
if (this.nodes.size === 0) {
|
|
527
|
+
throw new Error('DAG is empty - no nodes to execute');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Check for cycles using topological sort
|
|
531
|
+
try {
|
|
532
|
+
this.topoSort();
|
|
533
|
+
} catch (error) {
|
|
534
|
+
if (error.message.includes('Cycle detected')) {
|
|
535
|
+
// Extract cycle information from error message or use error.cycle if available
|
|
536
|
+
let nodes = error.cycle;
|
|
537
|
+
if (!nodes && error.message.includes('involving node:')) {
|
|
538
|
+
// Parse cycle from message if cycle array not available
|
|
539
|
+
const match = error.message.match(/involving node: (.+)/);
|
|
540
|
+
if (match) {
|
|
541
|
+
nodes = match[1].split(' -> ');
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (!nodes) {
|
|
545
|
+
nodes = ['unknown'];
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const pretty = nodes.join(' -> ');
|
|
549
|
+
const err = new Error(
|
|
550
|
+
`DAG validation failed: DAG topological sort failed: Cycle detected involving node: ${pretty}`,
|
|
551
|
+
);
|
|
552
|
+
err.cycle = nodes;
|
|
553
|
+
throw err;
|
|
554
|
+
}
|
|
555
|
+
throw new Error(`DAG validation failed: ${error.message}`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Check for orphaned nodes (nodes with no path to any sink)
|
|
559
|
+
const { fwd } = this._buildAdjacency();
|
|
560
|
+
const sinkIds = this._getSinkIds(fwd);
|
|
561
|
+
if (sinkIds.length === 0) {
|
|
562
|
+
throw new Error('DAG has no sink nodes - no final output available');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Execute a single node with input data
|
|
570
|
+
* @param {DAGNode} node - Node to execute
|
|
571
|
+
* @param {Object} context - Execution context
|
|
572
|
+
* @returns {Promise<any>} Node execution result
|
|
573
|
+
*/
|
|
574
|
+
async executeNode(node, context) {
|
|
575
|
+
const {
|
|
576
|
+
results,
|
|
577
|
+
errors,
|
|
578
|
+
fwd,
|
|
579
|
+
continueOnError,
|
|
580
|
+
requiredIds,
|
|
581
|
+
seed,
|
|
582
|
+
gracefulDegradation,
|
|
583
|
+
requiredNodes,
|
|
584
|
+
} = context;
|
|
585
|
+
|
|
586
|
+
// Handle undefined node.run function
|
|
587
|
+
if (typeof node.run !== 'function') {
|
|
588
|
+
throw new Error(`Node ${node.id} has no run function`);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Determine criticality - default is critical unless explicitly marked optional
|
|
592
|
+
// OR if gracefulDegradation is enabled and node is not in requiredNodes
|
|
593
|
+
// OR if there are multiple sink nodes (individual sink failures should be non-critical)
|
|
594
|
+
const sinkIds = this._getSinkIds(fwd);
|
|
595
|
+
const isMultipleSinks = sinkIds.length > 1;
|
|
596
|
+
const isSinkNode = sinkIds.includes(node.id);
|
|
597
|
+
|
|
598
|
+
const isNonCritical =
|
|
599
|
+
node?.optional === true ||
|
|
600
|
+
node?.isOptional === true ||
|
|
601
|
+
node?.critical === false ||
|
|
602
|
+
(gracefulDegradation &&
|
|
603
|
+
requiredNodes &&
|
|
604
|
+
!requiredNodes.includes(node.id)) ||
|
|
605
|
+
(isMultipleSinks && isSinkNode); // Sink nodes are non-critical when there are multiple sinks
|
|
606
|
+
|
|
607
|
+
// Prepare input based on dependencies
|
|
608
|
+
let input;
|
|
609
|
+
if (node.inputs.length === 0) {
|
|
610
|
+
input = seed;
|
|
611
|
+
} else if (node.inputs.length === 1) {
|
|
612
|
+
input = results.get(node.inputs[0].id);
|
|
613
|
+
} else {
|
|
614
|
+
input = node.inputs.map((inputNode) => {
|
|
615
|
+
// For graceful degradation, return undefined for failed optional dependencies
|
|
616
|
+
if (
|
|
617
|
+
!results.has(inputNode.id) &&
|
|
618
|
+
gracefulDegradation &&
|
|
619
|
+
requiredNodes &&
|
|
620
|
+
!requiredNodes.includes(inputNode.id)
|
|
621
|
+
) {
|
|
622
|
+
return undefined;
|
|
623
|
+
}
|
|
624
|
+
return results.get(inputNode.id);
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Retry logic - use global options if available, otherwise node-specific
|
|
629
|
+
const { retryFailedNodes, maxRetries: globalMaxRetries } = context;
|
|
630
|
+
const nodeMaxRetries = retryFailedNodes
|
|
631
|
+
? Math.max(0, globalMaxRetries ?? 3)
|
|
632
|
+
: Math.max(0, node?.retry?.retries ?? 0);
|
|
633
|
+
const delayMs = Math.max(0, node?.retry?.delayMs ?? 0);
|
|
634
|
+
let attempt = 0;
|
|
635
|
+
let keepRetrying = true;
|
|
636
|
+
|
|
637
|
+
while (keepRetrying) {
|
|
638
|
+
try {
|
|
639
|
+
// Execute the node
|
|
640
|
+
const output = await node.run(input);
|
|
641
|
+
if (errors.has(node.id)) errors.delete(node.id);
|
|
642
|
+
return output;
|
|
643
|
+
} catch (e) {
|
|
644
|
+
attempt += 1;
|
|
645
|
+
if (attempt <= nodeMaxRetries) {
|
|
646
|
+
if (delayMs) await new Promise((r) => setTimeout(r, delayMs));
|
|
647
|
+
continue; // no error recorded yet
|
|
648
|
+
}
|
|
649
|
+
keepRetrying = false;
|
|
650
|
+
// FINAL failure path
|
|
651
|
+
const deps = [...(fwd.get(node.id) || [])];
|
|
652
|
+
const suffix = deps.length
|
|
653
|
+
? `. This affects downstream nodes: ${deps.join(', ')}`
|
|
654
|
+
: '';
|
|
655
|
+
const nodeErr = new Error(
|
|
656
|
+
`Node ${node.id} execution failed: ${e.message}${suffix}`,
|
|
657
|
+
);
|
|
658
|
+
nodeErr.nodeId = node.id;
|
|
659
|
+
nodeErr.timestamp = Date.now();
|
|
660
|
+
nodeErr.cause = e;
|
|
661
|
+
|
|
662
|
+
// Handle non-critical failures with warnings and no result nullification
|
|
663
|
+
if (isNonCritical || (continueOnError && !requiredIds.has(node.id))) {
|
|
664
|
+
console.warn(`Non-critical node failure: ${nodeErr.message}`);
|
|
665
|
+
errors.set(node.id, nodeErr);
|
|
666
|
+
// Don't set result for failed optional nodes - return special symbol so they don't appear in results
|
|
667
|
+
return Symbol('failed-optional-node');
|
|
668
|
+
}
|
|
669
|
+
errors.set(node.id, nodeErr);
|
|
670
|
+
throw nodeErr;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Validate topology with advanced options
|
|
677
|
+
* @param {Object} _options - Validation options
|
|
678
|
+
* @returns {Array} - Array of warnings (empty if no issues)
|
|
679
|
+
*/
|
|
680
|
+
validateTopology(_options = {}) {
|
|
681
|
+
const { strict = true } = _options;
|
|
682
|
+
const warnings = [];
|
|
683
|
+
|
|
684
|
+
// Check for empty DAG
|
|
685
|
+
if (this.nodes.size === 0) {
|
|
686
|
+
throw new Error('DAG cannot be empty');
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Check for self-loops
|
|
690
|
+
for (const node of this.nodes.values()) {
|
|
691
|
+
if (node.outputs.includes(node)) {
|
|
692
|
+
throw new Error('Self-loop detected');
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Check for cycles by attempting topological sort
|
|
697
|
+
try {
|
|
698
|
+
this.topoSort();
|
|
699
|
+
} catch (error) {
|
|
700
|
+
if (error.message.includes('Cycle detected')) {
|
|
701
|
+
// Extract cycle information from error message or use error.cycle if available
|
|
702
|
+
let nodes = error.cycle;
|
|
703
|
+
if (!nodes && error.message.includes('involving node:')) {
|
|
704
|
+
// Parse cycle from message if cycle array not available
|
|
705
|
+
const match = error.message.match(/involving node: (.+)/);
|
|
706
|
+
if (match) {
|
|
707
|
+
nodes = match[1].split(' -> ');
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
if (!nodes) {
|
|
711
|
+
nodes = ['unknown'];
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const err = new Error('Cycle detected in DAG');
|
|
715
|
+
err.cycle = nodes;
|
|
716
|
+
throw err;
|
|
717
|
+
}
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Check for isolated nodes
|
|
722
|
+
for (const node of this.nodes.values()) {
|
|
723
|
+
if (node.inputs.length === 0 && node.outputs.length === 0) {
|
|
724
|
+
const message = `Orphaned node detected: ${node.id}`;
|
|
725
|
+
if (strict) {
|
|
726
|
+
throw new Error(message);
|
|
727
|
+
} else {
|
|
728
|
+
warnings.push(message);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return warnings;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Performs topological sort with cycle detection
|
|
738
|
+
* @returns {DAGNode[]} Topologically sorted nodes
|
|
739
|
+
* @throws {Error} If cycles are detected
|
|
740
|
+
*/
|
|
741
|
+
topoSort() {
|
|
742
|
+
const order = [];
|
|
743
|
+
const visited = new Set();
|
|
744
|
+
const visiting = new Set(); // Track nodes currently being visited
|
|
745
|
+
|
|
746
|
+
const visit = (node, path = []) => {
|
|
747
|
+
if (visiting.has(node.id)) {
|
|
748
|
+
const cycleStart = path.indexOf(node.id);
|
|
749
|
+
let cyclePath =
|
|
750
|
+
cycleStart >= 0
|
|
751
|
+
? path.slice(cycleStart).concat([node.id])
|
|
752
|
+
: [node.id];
|
|
753
|
+
// Reverse interior to show forward traversal order (A->B->C->A)
|
|
754
|
+
if (cyclePath.length > 2) {
|
|
755
|
+
const startNode = cyclePath[0];
|
|
756
|
+
const restNodes = cyclePath.slice(1, -1).reverse();
|
|
757
|
+
cyclePath = [startNode, ...restNodes, startNode];
|
|
758
|
+
}
|
|
759
|
+
const cyclePathString = cyclePath.join(' -> ');
|
|
760
|
+
const error = new Error(
|
|
761
|
+
`Cycle detected involving node: ${cyclePathString}`,
|
|
762
|
+
);
|
|
763
|
+
error.cycle = cyclePath; // Array format for test assertions
|
|
764
|
+
throw error;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (visited.has(node.id)) return;
|
|
768
|
+
|
|
769
|
+
visiting.add(node.id);
|
|
770
|
+
const newPath = [...path, node.id];
|
|
771
|
+
|
|
772
|
+
try {
|
|
773
|
+
node.inputs.forEach((input) => visit(input, newPath));
|
|
774
|
+
} catch (error) {
|
|
775
|
+
if (error.message.includes('Cycle detected')) {
|
|
776
|
+
throw error; // Preserve original message and cycle property
|
|
777
|
+
}
|
|
778
|
+
throw error;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
visiting.delete(node.id);
|
|
782
|
+
visited.add(node.id);
|
|
783
|
+
order.push(node);
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
try {
|
|
787
|
+
for (const node of this.nodes.values()) {
|
|
788
|
+
visit(node);
|
|
789
|
+
}
|
|
790
|
+
} catch (error) {
|
|
791
|
+
if (error.message.includes('Cycle detected')) {
|
|
792
|
+
throw error;
|
|
793
|
+
}
|
|
794
|
+
throw new Error(`DAG topological sort failed: ${error.message}`);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return order;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Resume execution from checkpoint data
|
|
802
|
+
* @param {Object} checkpointData - Checkpoint data to resume from
|
|
803
|
+
* @returns {Promise<Map>} - Results Map from resumed execution
|
|
804
|
+
*/
|
|
805
|
+
async resume(checkpointData = {}) {
|
|
806
|
+
const { completed = [], results: priorResults = {} } = checkpointData;
|
|
807
|
+
|
|
808
|
+
// Start with prior results as they were
|
|
809
|
+
const results = new Map(Object.entries(priorResults));
|
|
810
|
+
|
|
811
|
+
// Validate topology first
|
|
812
|
+
this.validateTopology();
|
|
813
|
+
|
|
814
|
+
// Get topological order and adjacency
|
|
815
|
+
const order = this.topoSort();
|
|
816
|
+
const { fwd } = this._buildAdjacency();
|
|
817
|
+
|
|
818
|
+
// Execute remaining nodes
|
|
819
|
+
for (const node of order) {
|
|
820
|
+
if (completed.includes(node.id)) {
|
|
821
|
+
continue; // Skip already completed nodes
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
// Handle undefined node.run function
|
|
826
|
+
if (typeof node.run !== 'function') {
|
|
827
|
+
throw new Error(`Node ${node.id} has no run function`);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Prepare input based on dependencies
|
|
831
|
+
let input;
|
|
832
|
+
if (node.inputs.length === 0) {
|
|
833
|
+
input = null; // No seed in resume
|
|
834
|
+
} else if (node.inputs.length === 1) {
|
|
835
|
+
input = results.get(node.inputs[0].id);
|
|
836
|
+
} else {
|
|
837
|
+
input = node.inputs.map((inputNode) => results.get(inputNode.id));
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Retry logic
|
|
841
|
+
const maxRetries = Math.max(0, node?.retry?.retries ?? 0);
|
|
842
|
+
const delayMs = Math.max(0, node?.retry?.delayMs ?? 0);
|
|
843
|
+
let attempt = 0;
|
|
844
|
+
let keepRetrying = true;
|
|
845
|
+
|
|
846
|
+
while (keepRetrying) {
|
|
847
|
+
try {
|
|
848
|
+
// Execute the node
|
|
849
|
+
const output = await node.run(input);
|
|
850
|
+
results.set(node.id, output);
|
|
851
|
+
keepRetrying = false;
|
|
852
|
+
} catch (e) {
|
|
853
|
+
attempt += 1;
|
|
854
|
+
if (attempt <= maxRetries) {
|
|
855
|
+
if (delayMs) await new Promise((r) => setTimeout(r, delayMs));
|
|
856
|
+
continue; // no error recorded yet
|
|
857
|
+
}
|
|
858
|
+
// FINAL failure path - warn EXACT string and keep going
|
|
859
|
+
// eslint-disable-next-line no-console
|
|
860
|
+
console.warn(`Node ${node.id} failed during resume: ${e.message}`);
|
|
861
|
+
keepRetrying = false; // Don't throw, just continue
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
} catch (error) {
|
|
865
|
+
// eslint-disable-next-line no-console
|
|
866
|
+
console.warn(`Node ${node.id} failed during resume: ${error.message}`);
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
return results;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Save execution checkpoint
|
|
876
|
+
* @param {string} checkpointId - Unique checkpoint identifier
|
|
877
|
+
* @param {Object} state - Execution state to save
|
|
878
|
+
*/
|
|
879
|
+
saveCheckpoint(checkpointId, state) {
|
|
880
|
+
if (!this.checkpoints) {
|
|
881
|
+
this.checkpoints = new Map();
|
|
882
|
+
}
|
|
883
|
+
this.checkpoints.set(checkpointId, {
|
|
884
|
+
timestamp: Date.now(),
|
|
885
|
+
results: new Map(state.results),
|
|
886
|
+
errors: new Map(state.errors),
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Load execution checkpoint
|
|
892
|
+
* @param {string} checkpointId - Unique checkpoint identifier
|
|
893
|
+
* @returns {Object|null} - Saved execution state or null if not found
|
|
894
|
+
*/
|
|
895
|
+
loadCheckpoint(checkpointId) {
|
|
896
|
+
if (!this.checkpoints) {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
return this.checkpoints.get(checkpointId) || null;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* List all available checkpoints
|
|
904
|
+
* @returns {Array} - Array of checkpoint IDs with metadata
|
|
905
|
+
*/
|
|
906
|
+
listCheckpoints() {
|
|
907
|
+
if (!this.checkpoints) {
|
|
908
|
+
return [];
|
|
909
|
+
}
|
|
910
|
+
return Array.from(this.checkpoints.entries()).map(([id, data]) => ({
|
|
911
|
+
id,
|
|
912
|
+
timestamp: data.timestamp,
|
|
913
|
+
resultCount: data.results.size,
|
|
914
|
+
errorCount: data.errors.size,
|
|
915
|
+
}));
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Clear execution checkpoint
|
|
920
|
+
* @param {string} checkpointId - Unique checkpoint identifier
|
|
921
|
+
*/
|
|
922
|
+
clearCheckpoint(checkpointId) {
|
|
923
|
+
if (this.checkpoints) {
|
|
924
|
+
this.checkpoints.delete(checkpointId);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Get all downstream nodes that depend on the given node
|
|
930
|
+
* @param {DAGNode} node - Node to find downstream dependencies for
|
|
931
|
+
* @returns {DAGNode[]} Array of downstream nodes
|
|
932
|
+
*/
|
|
933
|
+
getDownstreamNodes(node) {
|
|
934
|
+
const downstream = new Set();
|
|
935
|
+
const visited = new Set();
|
|
936
|
+
|
|
937
|
+
const traverse = (currentNode) => {
|
|
938
|
+
if (visited.has(currentNode.id)) return;
|
|
939
|
+
visited.add(currentNode.id);
|
|
940
|
+
|
|
941
|
+
for (const output of currentNode.outputs) {
|
|
942
|
+
downstream.add(output);
|
|
943
|
+
traverse(output);
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
traverse(node);
|
|
948
|
+
return Array.from(downstream);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Export classes
|
|
953
|
+
module.exports = { DAG, DAGNode };
|
|
954
|
+
module.exports.default = module.exports;
|