@bian-womp/spark-graph 0.3.45 → 0.3.47
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/lib/cjs/index.cjs +14 -15
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/core/types.d.ts +2 -1
- package/lib/cjs/src/core/types.d.ts.map +1 -1
- package/lib/cjs/src/runtime/GraphRuntime.d.ts +1 -1
- package/lib/cjs/src/runtime/GraphRuntime.d.ts.map +1 -1
- package/lib/cjs/src/runtime/components/NodeExecutor.d.ts.map +1 -1
- package/lib/cjs/src/runtime/components/RunContextManager.d.ts +2 -1
- package/lib/cjs/src/runtime/components/RunContextManager.d.ts.map +1 -1
- package/lib/esm/index.js +14 -15
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/core/types.d.ts +2 -1
- package/lib/esm/src/core/types.d.ts.map +1 -1
- package/lib/esm/src/runtime/GraphRuntime.d.ts +1 -1
- package/lib/esm/src/runtime/GraphRuntime.d.ts.map +1 -1
- package/lib/esm/src/runtime/components/NodeExecutor.d.ts.map +1 -1
- package/lib/esm/src/runtime/components/RunContextManager.d.ts +2 -1
- package/lib/esm/src/runtime/components/RunContextManager.d.ts.map +1 -1
- package/lib/src/builder/GraphBuilder.d.ts +43 -0
- package/lib/src/builder/GraphBuilder.d.ts.map +1 -0
- package/lib/src/builder/GraphBuilder.js +284 -0
- package/lib/src/builder/GraphBuilder.js.map +1 -0
- package/lib/src/builder/Registry.d.ts +93 -0
- package/lib/src/builder/Registry.d.ts.map +1 -0
- package/lib/src/builder/Registry.js +393 -0
- package/lib/src/builder/Registry.js.map +1 -0
- package/lib/src/core/categories.d.ts +22 -0
- package/lib/src/core/categories.d.ts.map +1 -0
- package/lib/src/core/categories.js +2 -0
- package/lib/src/core/categories.js.map +1 -0
- package/lib/src/core/order.d.ts +7 -0
- package/lib/src/core/order.d.ts.map +1 -0
- package/lib/src/core/order.js +66 -0
- package/lib/src/core/order.js.map +1 -0
- package/lib/src/core/type-utils.d.ts +29 -0
- package/lib/src/core/type-utils.d.ts.map +1 -0
- package/lib/src/core/type-utils.js +97 -0
- package/lib/src/core/type-utils.js.map +1 -0
- package/lib/src/core/types.d.ts +92 -0
- package/lib/src/core/types.d.ts.map +1 -0
- package/lib/src/core/types.js +2 -0
- package/lib/src/core/types.js.map +1 -0
- package/lib/src/examples/arrays.d.ts +5 -0
- package/lib/src/examples/arrays.d.ts.map +1 -0
- package/lib/src/examples/arrays.js +49 -0
- package/lib/src/examples/arrays.js.map +1 -0
- package/lib/src/examples/async.d.ts +5 -0
- package/lib/src/examples/async.d.ts.map +1 -0
- package/lib/src/examples/async.js +91 -0
- package/lib/src/examples/async.js.map +1 -0
- package/lib/src/examples/progress.d.ts +5 -0
- package/lib/src/examples/progress.d.ts.map +1 -0
- package/lib/src/examples/progress.js +51 -0
- package/lib/src/examples/progress.js.map +1 -0
- package/lib/src/examples/run.d.ts +2 -0
- package/lib/src/examples/run.d.ts.map +1 -0
- package/lib/src/examples/run.js +32 -0
- package/lib/src/examples/run.js.map +1 -0
- package/lib/src/examples/runMode.d.ts +2 -0
- package/lib/src/examples/runMode.d.ts.map +1 -0
- package/lib/src/examples/runMode.js +223 -0
- package/lib/src/examples/runMode.js.map +1 -0
- package/lib/src/examples/shared.d.ts +5 -0
- package/lib/src/examples/shared.d.ts.map +1 -0
- package/lib/src/examples/shared.js +49 -0
- package/lib/src/examples/shared.js.map +1 -0
- package/lib/src/examples/simple.d.ts +5 -0
- package/lib/src/examples/simple.d.ts.map +1 -0
- package/lib/src/examples/simple.js +79 -0
- package/lib/src/examples/simple.js.map +1 -0
- package/lib/src/examples/snapshot.d.ts +4 -0
- package/lib/src/examples/snapshot.d.ts.map +1 -0
- package/lib/src/examples/snapshot.js +58 -0
- package/lib/src/examples/snapshot.js.map +1 -0
- package/lib/src/examples/validation.d.ts +5 -0
- package/lib/src/examples/validation.d.ts.map +1 -0
- package/lib/src/examples/validation.js +105 -0
- package/lib/src/examples/validation.js.map +1 -0
- package/lib/src/index.d.ts +27 -0
- package/lib/src/index.d.ts.map +1 -0
- package/lib/src/index.js +19 -0
- package/lib/src/index.js.map +1 -0
- package/lib/src/misc/base.d.ts +51 -0
- package/lib/src/misc/base.d.ts.map +1 -0
- package/lib/src/misc/base.js +1122 -0
- package/lib/src/misc/base.js.map +1 -0
- package/lib/src/misc/utils/json.d.ts +22 -0
- package/lib/src/misc/utils/json.d.ts.map +1 -0
- package/lib/src/misc/utils/json.js +239 -0
- package/lib/src/misc/utils/json.js.map +1 -0
- package/lib/src/misc/utils/merge.d.ts +51 -0
- package/lib/src/misc/utils/merge.d.ts.map +1 -0
- package/lib/src/misc/utils/merge.js +600 -0
- package/lib/src/misc/utils/merge.js.map +1 -0
- package/lib/src/plugins/composite.d.ts +22 -0
- package/lib/src/plugins/composite.d.ts.map +1 -0
- package/lib/src/plugins/composite.js +59 -0
- package/lib/src/plugins/composite.js.map +1 -0
- package/lib/src/plugins/compute.d.ts +5 -0
- package/lib/src/plugins/compute.d.ts.map +1 -0
- package/lib/src/plugins/compute.js +39 -0
- package/lib/src/plugins/compute.js.map +1 -0
- package/lib/src/runtime/Engine.d.ts +28 -0
- package/lib/src/runtime/Engine.d.ts.map +1 -0
- package/lib/src/runtime/Engine.js +2 -0
- package/lib/src/runtime/Engine.js.map +1 -0
- package/lib/src/runtime/GraphLifecycleApi.d.ts +46 -0
- package/lib/src/runtime/GraphLifecycleApi.d.ts.map +1 -0
- package/lib/src/runtime/GraphLifecycleApi.js +2 -0
- package/lib/src/runtime/GraphLifecycleApi.js.map +1 -0
- package/lib/src/runtime/GraphRuntime.d.ts +94 -0
- package/lib/src/runtime/GraphRuntime.d.ts.map +1 -0
- package/lib/src/runtime/GraphRuntime.js +729 -0
- package/lib/src/runtime/GraphRuntime.js.map +1 -0
- package/lib/src/runtime/LocalEngine.d.ts +45 -0
- package/lib/src/runtime/LocalEngine.d.ts.map +1 -0
- package/lib/src/runtime/LocalEngine.js +89 -0
- package/lib/src/runtime/LocalEngine.js.map +1 -0
- package/lib/src/runtime/components/EdgePropagator.d.ts +101 -0
- package/lib/src/runtime/components/EdgePropagator.d.ts.map +1 -0
- package/lib/src/runtime/components/EdgePropagator.js +372 -0
- package/lib/src/runtime/components/EdgePropagator.js.map +1 -0
- package/lib/src/runtime/components/EventEmitter.d.ts +12 -0
- package/lib/src/runtime/components/EventEmitter.d.ts.map +1 -0
- package/lib/src/runtime/components/EventEmitter.js +33 -0
- package/lib/src/runtime/components/EventEmitter.js.map +1 -0
- package/lib/src/runtime/components/Graph.d.ts +211 -0
- package/lib/src/runtime/components/Graph.d.ts.map +1 -0
- package/lib/src/runtime/components/Graph.js +468 -0
- package/lib/src/runtime/components/Graph.js.map +1 -0
- package/lib/src/runtime/components/HandleResolver.d.ts +36 -0
- package/lib/src/runtime/components/HandleResolver.d.ts.map +1 -0
- package/lib/src/runtime/components/HandleResolver.js +231 -0
- package/lib/src/runtime/components/HandleResolver.js.map +1 -0
- package/lib/src/runtime/components/NodeExecutor.d.ts +110 -0
- package/lib/src/runtime/components/NodeExecutor.d.ts.map +1 -0
- package/lib/src/runtime/components/NodeExecutor.js +659 -0
- package/lib/src/runtime/components/NodeExecutor.js.map +1 -0
- package/lib/src/runtime/components/RunContextManager.d.ts +86 -0
- package/lib/src/runtime/components/RunContextManager.d.ts.map +1 -0
- package/lib/src/runtime/components/RunContextManager.js +302 -0
- package/lib/src/runtime/components/RunContextManager.js.map +1 -0
- package/lib/src/runtime/components/RuntimeValidatorManager.d.ts +31 -0
- package/lib/src/runtime/components/RuntimeValidatorManager.d.ts.map +1 -0
- package/lib/src/runtime/components/RuntimeValidatorManager.js +55 -0
- package/lib/src/runtime/components/RuntimeValidatorManager.js.map +1 -0
- package/lib/src/runtime/components/graph-utils.d.ts +33 -0
- package/lib/src/runtime/components/graph-utils.d.ts.map +1 -0
- package/lib/src/runtime/components/graph-utils.js +292 -0
- package/lib/src/runtime/components/graph-utils.js.map +1 -0
- package/lib/src/runtime/components/interfaces.d.ts +54 -0
- package/lib/src/runtime/components/interfaces.d.ts.map +1 -0
- package/lib/src/runtime/components/interfaces.js +2 -0
- package/lib/src/runtime/components/interfaces.js.map +1 -0
- package/lib/src/runtime/components/types.d.ts +55 -0
- package/lib/src/runtime/components/types.d.ts.map +1 -0
- package/lib/src/runtime/components/types.js +2 -0
- package/lib/src/runtime/components/types.js.map +1 -0
- package/lib/src/runtime/utils.d.ts +67 -0
- package/lib/src/runtime/utils.d.ts.map +1 -0
- package/lib/src/runtime/utils.js +137 -0
- package/lib/src/runtime/utils.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
import { getEffectiveInputs } from "./graph-utils";
|
|
2
|
+
import { LevelLogger } from "../utils";
|
|
3
|
+
/**
|
|
4
|
+
* NodeExecutor component - handles node execution scheduling and lifecycle
|
|
5
|
+
*/
|
|
6
|
+
export class NodeExecutor {
|
|
7
|
+
constructor(graph, eventEmitter, runContextManager, handleResolver, edgePropagator, runtime, environment) {
|
|
8
|
+
this.graph = graph;
|
|
9
|
+
this.eventEmitter = eventEmitter;
|
|
10
|
+
this.runContextManager = runContextManager;
|
|
11
|
+
this.handleResolver = handleResolver;
|
|
12
|
+
this.edgePropagator = edgePropagator;
|
|
13
|
+
this.runtime = runtime;
|
|
14
|
+
this.environment = {};
|
|
15
|
+
this.environment = environment ?? {};
|
|
16
|
+
}
|
|
17
|
+
setEnvironment(environment) {
|
|
18
|
+
this.environment = environment;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Compute effective inputs for a node by merging real inputs with defaults
|
|
22
|
+
*/
|
|
23
|
+
getEffectiveInputs(nodeId) {
|
|
24
|
+
const registry = this.graph.getRegistry();
|
|
25
|
+
if (!registry)
|
|
26
|
+
return {};
|
|
27
|
+
return getEffectiveInputs(nodeId, this.graph, registry);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create an execution context for a node
|
|
31
|
+
*/
|
|
32
|
+
createExecutionContext(nodeId, node, inputs, runId, abortSignal, runContextIds, options) {
|
|
33
|
+
const emitHandler = options?.emitHandler ??
|
|
34
|
+
((handle, value) => {
|
|
35
|
+
this.edgePropagator.propagate(nodeId, handle, value, runContextIds);
|
|
36
|
+
});
|
|
37
|
+
const reportProgress = options?.reportProgress ??
|
|
38
|
+
((p) => {
|
|
39
|
+
this.graph.updateNodeStats(nodeId, {
|
|
40
|
+
progress: Math.max(0, Math.min(1, Number(p) || 0)),
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
// Create log function that respects node's logLevel using LevelLogger
|
|
44
|
+
const nodeLogLevel = node.logLevel ?? "info";
|
|
45
|
+
const logger = new LevelLogger(nodeLogLevel, `[node:${runId || nodeId}:${node.typeId}]`);
|
|
46
|
+
const log = (level, message, context) => {
|
|
47
|
+
switch (level) {
|
|
48
|
+
case "debug":
|
|
49
|
+
logger.debug(message, context);
|
|
50
|
+
break;
|
|
51
|
+
case "info":
|
|
52
|
+
logger.info(message, context);
|
|
53
|
+
break;
|
|
54
|
+
case "warn":
|
|
55
|
+
logger.warn(message, context);
|
|
56
|
+
break;
|
|
57
|
+
case "error":
|
|
58
|
+
logger.error(message, context);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
nodeId,
|
|
64
|
+
state: node.state,
|
|
65
|
+
setState: (next) => this.graph.updateNodeState(nodeId, next),
|
|
66
|
+
emit: emitHandler,
|
|
67
|
+
invalidateDownstream: () => {
|
|
68
|
+
this.edgePropagator.invalidateDownstream(nodeId);
|
|
69
|
+
},
|
|
70
|
+
execute: (opts) => {
|
|
71
|
+
if (this.graph.allInboundHaveValue(nodeId)) {
|
|
72
|
+
let runContextIdsToUse = this.runtime.getRunMode() === "auto" ? undefined : runContextIds;
|
|
73
|
+
if (this.runtime.getRunMode() === "manual" &&
|
|
74
|
+
(!runContextIds || runContextIds.size === 0)) {
|
|
75
|
+
runContextIdsToUse = new Set([
|
|
76
|
+
this.runContextManager.createRunContext(nodeId, opts),
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
this.execute(nodeId, runContextIdsToUse);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
getInput: (handle) => inputs[handle],
|
|
83
|
+
environment: this.environment,
|
|
84
|
+
runId,
|
|
85
|
+
abortSignal,
|
|
86
|
+
reportProgress,
|
|
87
|
+
setCustomData: (data) => {
|
|
88
|
+
this.eventEmitter.emit("stats", {
|
|
89
|
+
kind: "node-custom-data",
|
|
90
|
+
nodeId,
|
|
91
|
+
typeId: node.typeId,
|
|
92
|
+
runId,
|
|
93
|
+
data,
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
log,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Internal method for executing inputs changed (also used by GraphRuntime)
|
|
101
|
+
*/
|
|
102
|
+
execute(nodeId, runContextIds, canSkipHandleResolution) {
|
|
103
|
+
const node = this.graph.getNode(nodeId);
|
|
104
|
+
if (!node)
|
|
105
|
+
return;
|
|
106
|
+
const runMode = this.runtime.getRunMode();
|
|
107
|
+
if (!runMode) {
|
|
108
|
+
console.trace("NodeExecutor.execute: no runMode, skipping execution");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// In manual mode, require runContextIds unless autoRun policy is set
|
|
112
|
+
if (runMode === "manual" && (!runContextIds || runContextIds.size === 0)) {
|
|
113
|
+
// If autoRun is true, auto-generate a run context (similar to createExecutionContext pattern)
|
|
114
|
+
if (node.policy?.autoRun === true) {
|
|
115
|
+
runContextIds = new Set([
|
|
116
|
+
this.runContextManager.createRunContext(nodeId, { propagate: false }),
|
|
117
|
+
]);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
console.trace("NodeExecutor.execute: no runContextIds provided in manual mode, skipping execution");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (runMode === "auto" && runContextIds && runContextIds.size > 0) {
|
|
125
|
+
console.trace("NodeExecutor.execute: runContextIds provided in auto mode, ignoring");
|
|
126
|
+
runContextIds = undefined;
|
|
127
|
+
}
|
|
128
|
+
// Early validation for auto-mode paused state
|
|
129
|
+
if (this.runtime.isPaused())
|
|
130
|
+
return;
|
|
131
|
+
// Check runtime validators (check current state, not just graph definition)
|
|
132
|
+
const runtimeValidationError = this.runtime.hasRuntimeValidationBlock(nodeId);
|
|
133
|
+
if (runtimeValidationError) {
|
|
134
|
+
this.eventEmitter.emit("error", {
|
|
135
|
+
kind: "system",
|
|
136
|
+
message: runtimeValidationError.message,
|
|
137
|
+
code: runtimeValidationError.code || "RUNTIME_VALIDATION_BLOCKED",
|
|
138
|
+
details: {
|
|
139
|
+
nodeId,
|
|
140
|
+
...runtimeValidationError.details,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Attach run-context IDs if provided - do this BEFORE checking for pending resolution
|
|
146
|
+
// so that handle resolution can track these run contexts
|
|
147
|
+
if (runContextIds) {
|
|
148
|
+
this.graph.addNodeRunContextIds(nodeId, runContextIds);
|
|
149
|
+
}
|
|
150
|
+
if (!canSkipHandleResolution &&
|
|
151
|
+
!this.handleResolver.getPendingResolution(nodeId)) {
|
|
152
|
+
this.handleResolver.scheduleRecomputeHandles(nodeId);
|
|
153
|
+
}
|
|
154
|
+
// Check if handles are being resolved - wait for resolution before executing
|
|
155
|
+
// Do this AFTER setting up run contexts so handle resolution can track them
|
|
156
|
+
const pendingResolution = this.handleResolver.getPendingResolution(nodeId);
|
|
157
|
+
if (pendingResolution) {
|
|
158
|
+
if (runContextIds && runContextIds.size > 0) {
|
|
159
|
+
for (const id of runContextIds) {
|
|
160
|
+
this.runContextManager.startHandleResolution(id, nodeId);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Wait for resolution to complete, then re-execute
|
|
164
|
+
pendingResolution.then(() => {
|
|
165
|
+
// Re-check node still exists and conditions
|
|
166
|
+
const nodeAfter = this.graph.getNode(nodeId);
|
|
167
|
+
if (nodeAfter) {
|
|
168
|
+
this.execute(nodeId, runContextIds, true);
|
|
169
|
+
}
|
|
170
|
+
if (runContextIds && runContextIds.size > 0) {
|
|
171
|
+
for (const id of runContextIds) {
|
|
172
|
+
this.runContextManager.finishHandleResolution(id, nodeId);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// Handle debouncing
|
|
179
|
+
const now = Date.now();
|
|
180
|
+
if (this.shouldDebounce(node, now)) {
|
|
181
|
+
this.handleDebouncedSchedule(node, nodeId, now, runContextIds);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// Prepare execution plan
|
|
185
|
+
const executionPlan = this.prepareExecutionPlan(node, nodeId, runContextIds, now);
|
|
186
|
+
// Route to appropriate concurrency handler
|
|
187
|
+
this.routeToConcurrencyHandler(node, nodeId, executionPlan);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Check if execution should be debounced
|
|
191
|
+
*/
|
|
192
|
+
shouldDebounce(node, now) {
|
|
193
|
+
const policy = node.policy ?? {};
|
|
194
|
+
const lastScheduledAt = node.lastScheduledAt;
|
|
195
|
+
return !!(policy.debounceMs &&
|
|
196
|
+
lastScheduledAt &&
|
|
197
|
+
now - lastScheduledAt < policy.debounceMs);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Handle debounced scheduling by replacing the latest queued item
|
|
201
|
+
*/
|
|
202
|
+
handleDebouncedSchedule(node, nodeId, now, runContextIds) {
|
|
203
|
+
// Decrement pendingQueued for any existing queued items before replacing
|
|
204
|
+
if (node.queue.length > 0) {
|
|
205
|
+
this.decrementQueuedForPlans(node.queue, nodeId);
|
|
206
|
+
}
|
|
207
|
+
const effectiveInputs = this.getEffectiveInputs(nodeId);
|
|
208
|
+
const runSeq = this.graph.incrementNodeRunSeq(nodeId);
|
|
209
|
+
const runId = `${nodeId}:${runSeq}:${now}`;
|
|
210
|
+
const policySnapshot = node.policy ? { ...node.policy } : undefined;
|
|
211
|
+
const plan = {
|
|
212
|
+
runId,
|
|
213
|
+
effectiveInputs,
|
|
214
|
+
runContextIdsForRun: runContextIds,
|
|
215
|
+
timestamp: now,
|
|
216
|
+
policy: policySnapshot,
|
|
217
|
+
};
|
|
218
|
+
this.graph.replaceNodeQueue(nodeId, [plan]);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Prepare execution plan with all necessary information
|
|
222
|
+
*/
|
|
223
|
+
prepareExecutionPlan(node, nodeId, runContextIds, now) {
|
|
224
|
+
this.graph.setNodeLastScheduledAt(nodeId, now);
|
|
225
|
+
const runSeq = this.graph.incrementNodeRunSeq(nodeId);
|
|
226
|
+
const runId = `${nodeId}:${runSeq}:${now}`;
|
|
227
|
+
this.graph.setNodeLatestRunId(nodeId, runId);
|
|
228
|
+
const effectiveInputs = this.getEffectiveInputs(nodeId);
|
|
229
|
+
// Take a shallow snapshot of the current policy for this run
|
|
230
|
+
const policySnapshot = node.policy ? { ...node.policy } : undefined;
|
|
231
|
+
return {
|
|
232
|
+
runId,
|
|
233
|
+
effectiveInputs,
|
|
234
|
+
runContextIdsForRun: runContextIds,
|
|
235
|
+
timestamp: now,
|
|
236
|
+
policy: policySnapshot,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Route execution to appropriate concurrency handler
|
|
241
|
+
*/
|
|
242
|
+
routeToConcurrencyHandler(node, nodeId, plan) {
|
|
243
|
+
const mode = plan.policy?.asyncConcurrency ?? "switch";
|
|
244
|
+
switch (mode) {
|
|
245
|
+
case "drop":
|
|
246
|
+
this.handleDropMode(node, nodeId, plan);
|
|
247
|
+
break;
|
|
248
|
+
case "queue":
|
|
249
|
+
this.handleQueueMode(node, nodeId, plan);
|
|
250
|
+
break;
|
|
251
|
+
case "switch":
|
|
252
|
+
case "merge":
|
|
253
|
+
default:
|
|
254
|
+
this.startRun(node, nodeId, plan);
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Handle drop mode - drop execution if node is already running, otherwise start run
|
|
260
|
+
*/
|
|
261
|
+
handleDropMode(node, nodeId, plan) {
|
|
262
|
+
// Drop if node is already running
|
|
263
|
+
if (node.activeControllers.size > 0) {
|
|
264
|
+
return; // Don't increment pendingCount if we're dropping this run
|
|
265
|
+
}
|
|
266
|
+
// Start run if node is not running
|
|
267
|
+
this.startRun(node, nodeId, plan);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Handle queue mode - add to queue and process sequentially
|
|
271
|
+
*/
|
|
272
|
+
handleQueueMode(node, nodeId, plan) {
|
|
273
|
+
const maxQ = plan.policy?.maxQueue ?? 8;
|
|
274
|
+
const currentNode = this.graph.getNode(nodeId);
|
|
275
|
+
if (!currentNode)
|
|
276
|
+
return;
|
|
277
|
+
// Keep the originating run-context alive while work is queued by
|
|
278
|
+
// incrementing queued counters for the plan's run-contexts.
|
|
279
|
+
if (plan.runContextIdsForRun) {
|
|
280
|
+
for (const rcId of plan.runContextIdsForRun) {
|
|
281
|
+
this.runContextManager.incrementQueued(rcId, nodeId);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
this.graph.addToNodeQueue(nodeId, plan);
|
|
285
|
+
if (currentNode.queue.length > maxQ) {
|
|
286
|
+
const dropped = this.graph.shiftNodeQueue(nodeId);
|
|
287
|
+
if (dropped) {
|
|
288
|
+
this.decrementQueuedForPlans(dropped, nodeId);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
this.processQueue(node, nodeId);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Process queued executions sequentially
|
|
295
|
+
*/
|
|
296
|
+
processQueue(node, nodeId) {
|
|
297
|
+
const processNext = () => {
|
|
298
|
+
const node = this.graph.getNode(nodeId);
|
|
299
|
+
if (!node)
|
|
300
|
+
return;
|
|
301
|
+
if (node.activeControllers.size > 0)
|
|
302
|
+
return;
|
|
303
|
+
const next = this.graph.shiftNodeQueue(nodeId);
|
|
304
|
+
if (!next)
|
|
305
|
+
return;
|
|
306
|
+
this.graph.setNodeLatestRunId(nodeId, next.runId);
|
|
307
|
+
// Start the run first (which increments pendingNodes), then decrement
|
|
308
|
+
// pendingQueued to ensure the run context stays alive.
|
|
309
|
+
this.startRun(node, nodeId, next, () => {
|
|
310
|
+
setTimeout(processNext, 0);
|
|
311
|
+
});
|
|
312
|
+
this.decrementQueuedForPlans(next, nodeId);
|
|
313
|
+
};
|
|
314
|
+
processNext();
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Start a node execution run
|
|
318
|
+
*/
|
|
319
|
+
startRun(node, nodeId, plan, onDone) {
|
|
320
|
+
// Hard guarantee: in queue mode, never run concurrently.
|
|
321
|
+
// If we somehow get here while already running (e.g. re-entrancy), re-queue instead.
|
|
322
|
+
if (plan.policy?.asyncConcurrency === "queue") {
|
|
323
|
+
const currentNode = this.graph.getNode(nodeId);
|
|
324
|
+
if (!currentNode)
|
|
325
|
+
return;
|
|
326
|
+
if (currentNode.activeControllers.size > 0) {
|
|
327
|
+
const maxQ = plan.policy?.maxQueue ?? 8;
|
|
328
|
+
// Keep the originating run-context alive while queued.
|
|
329
|
+
if (plan.runContextIdsForRun) {
|
|
330
|
+
for (const rcId of plan.runContextIdsForRun) {
|
|
331
|
+
this.runContextManager.incrementQueued(rcId, nodeId);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
this.graph.addToNodeQueue(nodeId, plan);
|
|
335
|
+
if (currentNode.queue.length > maxQ) {
|
|
336
|
+
const dropped = this.graph.shiftNodeQueue(nodeId);
|
|
337
|
+
if (dropped) {
|
|
338
|
+
this.decrementQueuedForPlans(dropped, nodeId);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Track run-contexts
|
|
345
|
+
this.trackRunContextStart(nodeId, plan.runContextIdsForRun);
|
|
346
|
+
// Setup execution controller
|
|
347
|
+
const controller = this.createExecutionController(nodeId, node, plan.runId);
|
|
348
|
+
// Handle concurrency mode
|
|
349
|
+
this.applyConcurrencyMode(nodeId, node, controller, plan);
|
|
350
|
+
// Setup timeout if needed
|
|
351
|
+
const timeoutId = this.setupTimeout(node, controller, plan);
|
|
352
|
+
// Create execution context
|
|
353
|
+
const execCtx = this.createExecutionContext(nodeId, node, plan.effectiveInputs, plan.runId, controller.signal, plan.runContextIdsForRun, this.createEmitAndProgressHandlers(node, nodeId, plan));
|
|
354
|
+
// Execute
|
|
355
|
+
this.executeNode(node, nodeId, execCtx, plan, controller, timeoutId, onDone);
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Track run-context start for pending nodes
|
|
359
|
+
*/
|
|
360
|
+
trackRunContextStart(nodeId, runContextIdsForRun) {
|
|
361
|
+
if (runContextIdsForRun && runContextIdsForRun.size > 0) {
|
|
362
|
+
for (const id of runContextIdsForRun) {
|
|
363
|
+
this.runContextManager.startNodeRun(id, nodeId);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Decrement pendingQueued counters for plans' run-contexts.
|
|
369
|
+
* Used when queued work is being started, dropped, or cancelled.
|
|
370
|
+
*/
|
|
371
|
+
decrementQueuedForPlans(plans, nodeId) {
|
|
372
|
+
const plansArray = Array.isArray(plans) ? plans : [plans];
|
|
373
|
+
for (const plan of plansArray) {
|
|
374
|
+
if (plan.runContextIdsForRun) {
|
|
375
|
+
for (const rcId of plan.runContextIdsForRun) {
|
|
376
|
+
this.runContextManager.decrementQueued(rcId, nodeId);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Create execution controller and update node stats
|
|
383
|
+
*/
|
|
384
|
+
createExecutionController(nodeId, node, runId) {
|
|
385
|
+
const controller = new AbortController();
|
|
386
|
+
const now = Date.now();
|
|
387
|
+
this.graph.updateNodeStats(nodeId, {
|
|
388
|
+
runs: node.stats.runs + 1,
|
|
389
|
+
active: node.stats.active + 1,
|
|
390
|
+
lastStartAt: now,
|
|
391
|
+
progress: 0,
|
|
392
|
+
});
|
|
393
|
+
this.graph.addNodeController(nodeId, controller, runId);
|
|
394
|
+
return controller;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Apply concurrency mode (switch mode aborts other controllers)
|
|
398
|
+
*/
|
|
399
|
+
applyConcurrencyMode(nodeId, node, controller, plan) {
|
|
400
|
+
const mode = plan.policy?.asyncConcurrency ?? "switch";
|
|
401
|
+
if (mode === "switch") {
|
|
402
|
+
const controllers = this.graph.getNodeControllers(nodeId);
|
|
403
|
+
for (const c of controllers) {
|
|
404
|
+
if (c !== controller)
|
|
405
|
+
c.abort("switch");
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Setup timeout for execution if configured
|
|
411
|
+
*/
|
|
412
|
+
setupTimeout(node, controller, plan) {
|
|
413
|
+
const policy = plan.policy ?? {};
|
|
414
|
+
if (policy.timeoutMs && policy.timeoutMs > 0) {
|
|
415
|
+
return setTimeout(() => controller.abort("timeout"), policy.timeoutMs);
|
|
416
|
+
}
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Create emit and progress handlers for execution context
|
|
421
|
+
*/
|
|
422
|
+
createEmitAndProgressHandlers(node, nodeId, plan) {
|
|
423
|
+
const policy = plan.policy ?? {};
|
|
424
|
+
return {
|
|
425
|
+
emitHandler: (handle, value) => {
|
|
426
|
+
const node = this.graph.getNode(nodeId);
|
|
427
|
+
if (!node)
|
|
428
|
+
return;
|
|
429
|
+
const m = policy.asyncConcurrency ?? "switch";
|
|
430
|
+
// Drop emits from runs that were explicitly cancelled due to a
|
|
431
|
+
// snapshot/undo/redo operation, regardless of asyncConcurrency.
|
|
432
|
+
if (node.snapshotCancelledRunIds?.has(plan.runId))
|
|
433
|
+
return;
|
|
434
|
+
if (m !== "merge" && plan.runId !== node.latestRunId)
|
|
435
|
+
return;
|
|
436
|
+
this.edgePropagator.propagate(nodeId, handle, value, plan.runContextIdsForRun);
|
|
437
|
+
},
|
|
438
|
+
reportProgress: (p) => {
|
|
439
|
+
const progress = Math.max(0, Math.min(1, Number(p) || 0));
|
|
440
|
+
this.graph.updateNodeStats(nodeId, { progress });
|
|
441
|
+
const node = this.graph.getNode(nodeId);
|
|
442
|
+
if (!node)
|
|
443
|
+
return;
|
|
444
|
+
this.eventEmitter.emit("stats", {
|
|
445
|
+
kind: "node-progress",
|
|
446
|
+
nodeId,
|
|
447
|
+
typeId: node.typeId,
|
|
448
|
+
runId: plan.runId,
|
|
449
|
+
progress,
|
|
450
|
+
});
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Execute the node with retry logic and cleanup
|
|
456
|
+
*/
|
|
457
|
+
executeNode(node, nodeId, ctx, plan, controller, timeoutId, onDone) {
|
|
458
|
+
// Fire node-start event
|
|
459
|
+
this.eventEmitter.emit("stats", {
|
|
460
|
+
kind: "node-start",
|
|
461
|
+
nodeId,
|
|
462
|
+
typeId: node.typeId,
|
|
463
|
+
runId: plan.runId,
|
|
464
|
+
});
|
|
465
|
+
ctx.log("debug", "node-start");
|
|
466
|
+
const exec = async (attempt) => {
|
|
467
|
+
let hadError = false;
|
|
468
|
+
try {
|
|
469
|
+
if (node.lifecycle?.prepare) {
|
|
470
|
+
ctx.log("debug", "prepare-start");
|
|
471
|
+
node.lifecycle.prepare(node.params ?? {}, ctx);
|
|
472
|
+
ctx.log("debug", "prepare-done");
|
|
473
|
+
}
|
|
474
|
+
await node.runtime.onInputsChanged?.(plan.effectiveInputs, ctx);
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
// Suppress errors caused by expected cancellations
|
|
478
|
+
if (controller.signal.aborted) {
|
|
479
|
+
const reason = controller.signal.reason;
|
|
480
|
+
if (reason === "switch" ||
|
|
481
|
+
reason === "snapshot" ||
|
|
482
|
+
reason === "node-deleted" ||
|
|
483
|
+
reason === "user-cancelled") {
|
|
484
|
+
return; // Cancellation events are emitted separately, skip error handling
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
hadError = true;
|
|
488
|
+
this.graph.updateNodeStats(nodeId, { lastError: err });
|
|
489
|
+
const retry = plan.policy?.retry;
|
|
490
|
+
if (retry && attempt < (retry.attempts ?? 0)) {
|
|
491
|
+
const delay = retry.backoffMs ? retry.backoffMs(attempt) : 0;
|
|
492
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
493
|
+
return exec(attempt + 1);
|
|
494
|
+
}
|
|
495
|
+
this.eventEmitter.emit("error", {
|
|
496
|
+
kind: "node-run",
|
|
497
|
+
nodeId,
|
|
498
|
+
runId: plan.runId,
|
|
499
|
+
err,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
finally {
|
|
503
|
+
this.cleanupExecution(node, nodeId, ctx, plan, controller, timeoutId, hadError, onDone);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
exec(0);
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Cleanup after execution completes
|
|
510
|
+
*/
|
|
511
|
+
cleanupExecution(node, nodeId, ctx, plan, controller, timeoutId, hadError, onDone) {
|
|
512
|
+
// Decrement pendingNodes count for all relevant run-contexts
|
|
513
|
+
if (plan.runContextIdsForRun && plan.runContextIdsForRun.size > 0) {
|
|
514
|
+
for (const id of plan.runContextIdsForRun) {
|
|
515
|
+
this.runContextManager.finishNodeRun(id, nodeId);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// Skip cleanup if node was deleted (cleanup already handled)
|
|
519
|
+
if (!this.graph.hasNode(nodeId)) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (timeoutId)
|
|
523
|
+
clearTimeout(timeoutId);
|
|
524
|
+
this.graph.removeNodeController(nodeId, controller);
|
|
525
|
+
const currentNode = this.graph.getNode(nodeId);
|
|
526
|
+
if (!currentNode)
|
|
527
|
+
return;
|
|
528
|
+
const controllers = this.graph.getNodeControllers(nodeId);
|
|
529
|
+
const lastEndAt = Date.now();
|
|
530
|
+
const lastDurationMs = currentNode.stats.lastStartAt && lastEndAt
|
|
531
|
+
? lastEndAt - currentNode.stats.lastStartAt
|
|
532
|
+
: undefined;
|
|
533
|
+
this.graph.updateNodeStats(nodeId, {
|
|
534
|
+
active: Math.max(0, controllers.size),
|
|
535
|
+
lastEndAt,
|
|
536
|
+
lastDurationMs,
|
|
537
|
+
lastError: hadError ? currentNode.stats.lastError : undefined,
|
|
538
|
+
});
|
|
539
|
+
// Track successful completion time (for detecting stale inputs)
|
|
540
|
+
const isCancelled = controller.signal.aborted &&
|
|
541
|
+
(controller.signal.reason === "snapshot" ||
|
|
542
|
+
controller.signal.reason === "node-deleted" ||
|
|
543
|
+
controller.signal.reason === "user-cancelled");
|
|
544
|
+
if (!hadError && !isCancelled) {
|
|
545
|
+
this.graph.setNodeLastSuccessAt(nodeId, Date.now());
|
|
546
|
+
}
|
|
547
|
+
// Only emit node-done if not cancelled (cancellation events emitted separately)
|
|
548
|
+
if (!isCancelled) {
|
|
549
|
+
if (currentNode) {
|
|
550
|
+
this.eventEmitter.emit("stats", {
|
|
551
|
+
kind: "node-done",
|
|
552
|
+
nodeId,
|
|
553
|
+
typeId: currentNode.typeId,
|
|
554
|
+
runId: plan.runId,
|
|
555
|
+
durationMs: currentNode.stats.lastDurationMs,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (currentNode) {
|
|
560
|
+
ctx.log("debug", "node-done", {
|
|
561
|
+
durationMs: currentNode.stats.lastDurationMs,
|
|
562
|
+
hadError,
|
|
563
|
+
inputs: currentNode.inputs
|
|
564
|
+
? structuredClone(currentNode.inputs)
|
|
565
|
+
: undefined,
|
|
566
|
+
outputs: currentNode.outputs
|
|
567
|
+
? structuredClone(currentNode.outputs)
|
|
568
|
+
: undefined,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
if (onDone)
|
|
572
|
+
onDone();
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Cancel all active runs for a node
|
|
576
|
+
*/
|
|
577
|
+
cancelNodeActiveRuns(node, reason) {
|
|
578
|
+
const nodeId = node.nodeId;
|
|
579
|
+
const controllers = this.graph.getNodeControllers(nodeId);
|
|
580
|
+
for (const controller of controllers) {
|
|
581
|
+
const currentNode = this.graph.getNode(nodeId);
|
|
582
|
+
if (!currentNode)
|
|
583
|
+
continue;
|
|
584
|
+
const runId = currentNode.controllerRunIds.get(controller);
|
|
585
|
+
if (runId) {
|
|
586
|
+
// Track cancelled runIds for snapshot and user-cancelled operations
|
|
587
|
+
// (to drop emits from cancelled runs)
|
|
588
|
+
if (reason === "snapshot" || reason === "user-cancelled") {
|
|
589
|
+
this.graph.addSnapshotCancelledRunId(nodeId, runId);
|
|
590
|
+
}
|
|
591
|
+
// Emit cancellation event
|
|
592
|
+
this.eventEmitter.emit("stats", {
|
|
593
|
+
kind: "node-done",
|
|
594
|
+
nodeId: currentNode.nodeId,
|
|
595
|
+
typeId: currentNode.typeId,
|
|
596
|
+
runId,
|
|
597
|
+
cancelled: true,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
try {
|
|
601
|
+
controller.abort(reason);
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
// ignore abort errors
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
this.graph.clearNodeControllers(nodeId);
|
|
608
|
+
this.graph.updateNodeStats(nodeId, { active: 0 });
|
|
609
|
+
// Decrement pendingQueued for any queued items before clearing the queue
|
|
610
|
+
if (node.queue.length > 0) {
|
|
611
|
+
this.decrementQueuedForPlans(node.queue, nodeId);
|
|
612
|
+
}
|
|
613
|
+
this.graph.clearNodeQueue(nodeId);
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Cancel runs for multiple nodes.
|
|
617
|
+
* Can be called for snapshot/undo/redo operations or user-initiated cancellation.
|
|
618
|
+
*/
|
|
619
|
+
cancelNodeRuns(nodeIds, reason = "user-cancelled") {
|
|
620
|
+
if (nodeIds.length === 0)
|
|
621
|
+
return;
|
|
622
|
+
const toCancel = new Set(nodeIds);
|
|
623
|
+
const visited = new Set();
|
|
624
|
+
const queue = [...nodeIds];
|
|
625
|
+
// Collect all downstream nodes to cancel
|
|
626
|
+
for (let i = 0; i < queue.length; i++) {
|
|
627
|
+
const nodeId = queue[i];
|
|
628
|
+
if (visited.has(nodeId))
|
|
629
|
+
continue;
|
|
630
|
+
visited.add(nodeId);
|
|
631
|
+
this.graph.forEachEdge((edge) => {
|
|
632
|
+
if (edge.source.nodeId === nodeId) {
|
|
633
|
+
const targetId = edge.target.nodeId;
|
|
634
|
+
if (!visited.has(targetId)) {
|
|
635
|
+
toCancel.add(targetId);
|
|
636
|
+
queue.push(targetId);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
// Cancel runs for all affected nodes
|
|
642
|
+
for (const nodeId of toCancel) {
|
|
643
|
+
const node = this.graph.getNode(nodeId);
|
|
644
|
+
if (!node)
|
|
645
|
+
continue;
|
|
646
|
+
this.cancelNodeActiveRuns(node, reason);
|
|
647
|
+
const runSeq = this.graph.incrementNodeRunSeq(nodeId);
|
|
648
|
+
const now = Date.now();
|
|
649
|
+
const suffix = reason === "snapshot" ? "snapshot" : "cancelled";
|
|
650
|
+
this.graph.setNodeLatestRunId(nodeId, `${nodeId}:${runSeq}:${now}:${suffix}`);
|
|
651
|
+
}
|
|
652
|
+
// Cancel nodes in run-contexts (exclude them from active run-contexts)
|
|
653
|
+
for (const nodeId of toCancel) {
|
|
654
|
+
// includeDownstream = false (already collected above)
|
|
655
|
+
this.runContextManager.cancelNodeInRunContexts(nodeId, false);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
//# sourceMappingURL=NodeExecutor.js.map
|