@bian-womp/spark-graph 0.3.46 → 0.3.48

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.
Files changed (163) hide show
  1. package/lib/cjs/index.cjs +20 -17
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/core/types.d.ts +2 -1
  4. package/lib/cjs/src/core/types.d.ts.map +1 -1
  5. package/lib/cjs/src/runtime/GraphRuntime.d.ts.map +1 -1
  6. package/lib/cjs/src/runtime/components/NodeExecutor.d.ts.map +1 -1
  7. package/lib/cjs/src/runtime/components/RunContextManager.d.ts +3 -2
  8. package/lib/cjs/src/runtime/components/RunContextManager.d.ts.map +1 -1
  9. package/lib/cjs/src/runtime/utils.d.ts.map +1 -1
  10. package/lib/esm/index.js +20 -17
  11. package/lib/esm/index.js.map +1 -1
  12. package/lib/esm/src/core/types.d.ts +2 -1
  13. package/lib/esm/src/core/types.d.ts.map +1 -1
  14. package/lib/esm/src/runtime/GraphRuntime.d.ts.map +1 -1
  15. package/lib/esm/src/runtime/components/NodeExecutor.d.ts.map +1 -1
  16. package/lib/esm/src/runtime/components/RunContextManager.d.ts +3 -2
  17. package/lib/esm/src/runtime/components/RunContextManager.d.ts.map +1 -1
  18. package/lib/esm/src/runtime/utils.d.ts.map +1 -1
  19. package/lib/src/builder/GraphBuilder.d.ts +43 -0
  20. package/lib/src/builder/GraphBuilder.d.ts.map +1 -0
  21. package/lib/src/builder/GraphBuilder.js +284 -0
  22. package/lib/src/builder/GraphBuilder.js.map +1 -0
  23. package/lib/src/builder/Registry.d.ts +93 -0
  24. package/lib/src/builder/Registry.d.ts.map +1 -0
  25. package/lib/src/builder/Registry.js +393 -0
  26. package/lib/src/builder/Registry.js.map +1 -0
  27. package/lib/src/core/categories.d.ts +22 -0
  28. package/lib/src/core/categories.d.ts.map +1 -0
  29. package/lib/src/core/categories.js +2 -0
  30. package/lib/src/core/categories.js.map +1 -0
  31. package/lib/src/core/order.d.ts +7 -0
  32. package/lib/src/core/order.d.ts.map +1 -0
  33. package/lib/src/core/order.js +66 -0
  34. package/lib/src/core/order.js.map +1 -0
  35. package/lib/src/core/type-utils.d.ts +29 -0
  36. package/lib/src/core/type-utils.d.ts.map +1 -0
  37. package/lib/src/core/type-utils.js +97 -0
  38. package/lib/src/core/type-utils.js.map +1 -0
  39. package/lib/src/core/types.d.ts +92 -0
  40. package/lib/src/core/types.d.ts.map +1 -0
  41. package/lib/src/core/types.js +2 -0
  42. package/lib/src/core/types.js.map +1 -0
  43. package/lib/src/examples/arrays.d.ts +5 -0
  44. package/lib/src/examples/arrays.d.ts.map +1 -0
  45. package/lib/src/examples/arrays.js +49 -0
  46. package/lib/src/examples/arrays.js.map +1 -0
  47. package/lib/src/examples/async.d.ts +5 -0
  48. package/lib/src/examples/async.d.ts.map +1 -0
  49. package/lib/src/examples/async.js +91 -0
  50. package/lib/src/examples/async.js.map +1 -0
  51. package/lib/src/examples/progress.d.ts +5 -0
  52. package/lib/src/examples/progress.d.ts.map +1 -0
  53. package/lib/src/examples/progress.js +51 -0
  54. package/lib/src/examples/progress.js.map +1 -0
  55. package/lib/src/examples/run.d.ts +2 -0
  56. package/lib/src/examples/run.d.ts.map +1 -0
  57. package/lib/src/examples/run.js +32 -0
  58. package/lib/src/examples/run.js.map +1 -0
  59. package/lib/src/examples/runMode.d.ts +2 -0
  60. package/lib/src/examples/runMode.d.ts.map +1 -0
  61. package/lib/src/examples/runMode.js +223 -0
  62. package/lib/src/examples/runMode.js.map +1 -0
  63. package/lib/src/examples/shared.d.ts +5 -0
  64. package/lib/src/examples/shared.d.ts.map +1 -0
  65. package/lib/src/examples/shared.js +49 -0
  66. package/lib/src/examples/shared.js.map +1 -0
  67. package/lib/src/examples/simple.d.ts +5 -0
  68. package/lib/src/examples/simple.d.ts.map +1 -0
  69. package/lib/src/examples/simple.js +79 -0
  70. package/lib/src/examples/simple.js.map +1 -0
  71. package/lib/src/examples/snapshot.d.ts +4 -0
  72. package/lib/src/examples/snapshot.d.ts.map +1 -0
  73. package/lib/src/examples/snapshot.js +58 -0
  74. package/lib/src/examples/snapshot.js.map +1 -0
  75. package/lib/src/examples/validation.d.ts +5 -0
  76. package/lib/src/examples/validation.d.ts.map +1 -0
  77. package/lib/src/examples/validation.js +105 -0
  78. package/lib/src/examples/validation.js.map +1 -0
  79. package/lib/src/index.d.ts +27 -0
  80. package/lib/src/index.d.ts.map +1 -0
  81. package/lib/src/index.js +19 -0
  82. package/lib/src/index.js.map +1 -0
  83. package/lib/src/misc/base.d.ts +51 -0
  84. package/lib/src/misc/base.d.ts.map +1 -0
  85. package/lib/src/misc/base.js +1122 -0
  86. package/lib/src/misc/base.js.map +1 -0
  87. package/lib/src/misc/utils/json.d.ts +22 -0
  88. package/lib/src/misc/utils/json.d.ts.map +1 -0
  89. package/lib/src/misc/utils/json.js +239 -0
  90. package/lib/src/misc/utils/json.js.map +1 -0
  91. package/lib/src/misc/utils/merge.d.ts +51 -0
  92. package/lib/src/misc/utils/merge.d.ts.map +1 -0
  93. package/lib/src/misc/utils/merge.js +600 -0
  94. package/lib/src/misc/utils/merge.js.map +1 -0
  95. package/lib/src/plugins/composite.d.ts +22 -0
  96. package/lib/src/plugins/composite.d.ts.map +1 -0
  97. package/lib/src/plugins/composite.js +59 -0
  98. package/lib/src/plugins/composite.js.map +1 -0
  99. package/lib/src/plugins/compute.d.ts +5 -0
  100. package/lib/src/plugins/compute.d.ts.map +1 -0
  101. package/lib/src/plugins/compute.js +39 -0
  102. package/lib/src/plugins/compute.js.map +1 -0
  103. package/lib/src/runtime/Engine.d.ts +28 -0
  104. package/lib/src/runtime/Engine.d.ts.map +1 -0
  105. package/lib/src/runtime/Engine.js +2 -0
  106. package/lib/src/runtime/Engine.js.map +1 -0
  107. package/lib/src/runtime/GraphLifecycleApi.d.ts +46 -0
  108. package/lib/src/runtime/GraphLifecycleApi.d.ts.map +1 -0
  109. package/lib/src/runtime/GraphLifecycleApi.js +2 -0
  110. package/lib/src/runtime/GraphLifecycleApi.js.map +1 -0
  111. package/lib/src/runtime/GraphRuntime.d.ts +94 -0
  112. package/lib/src/runtime/GraphRuntime.d.ts.map +1 -0
  113. package/lib/src/runtime/GraphRuntime.js +729 -0
  114. package/lib/src/runtime/GraphRuntime.js.map +1 -0
  115. package/lib/src/runtime/LocalEngine.d.ts +45 -0
  116. package/lib/src/runtime/LocalEngine.d.ts.map +1 -0
  117. package/lib/src/runtime/LocalEngine.js +89 -0
  118. package/lib/src/runtime/LocalEngine.js.map +1 -0
  119. package/lib/src/runtime/components/EdgePropagator.d.ts +101 -0
  120. package/lib/src/runtime/components/EdgePropagator.d.ts.map +1 -0
  121. package/lib/src/runtime/components/EdgePropagator.js +372 -0
  122. package/lib/src/runtime/components/EdgePropagator.js.map +1 -0
  123. package/lib/src/runtime/components/EventEmitter.d.ts +12 -0
  124. package/lib/src/runtime/components/EventEmitter.d.ts.map +1 -0
  125. package/lib/src/runtime/components/EventEmitter.js +33 -0
  126. package/lib/src/runtime/components/EventEmitter.js.map +1 -0
  127. package/lib/src/runtime/components/Graph.d.ts +211 -0
  128. package/lib/src/runtime/components/Graph.d.ts.map +1 -0
  129. package/lib/src/runtime/components/Graph.js +468 -0
  130. package/lib/src/runtime/components/Graph.js.map +1 -0
  131. package/lib/src/runtime/components/HandleResolver.d.ts +36 -0
  132. package/lib/src/runtime/components/HandleResolver.d.ts.map +1 -0
  133. package/lib/src/runtime/components/HandleResolver.js +231 -0
  134. package/lib/src/runtime/components/HandleResolver.js.map +1 -0
  135. package/lib/src/runtime/components/NodeExecutor.d.ts +110 -0
  136. package/lib/src/runtime/components/NodeExecutor.d.ts.map +1 -0
  137. package/lib/src/runtime/components/NodeExecutor.js +659 -0
  138. package/lib/src/runtime/components/NodeExecutor.js.map +1 -0
  139. package/lib/src/runtime/components/RunContextManager.d.ts +86 -0
  140. package/lib/src/runtime/components/RunContextManager.d.ts.map +1 -0
  141. package/lib/src/runtime/components/RunContextManager.js +302 -0
  142. package/lib/src/runtime/components/RunContextManager.js.map +1 -0
  143. package/lib/src/runtime/components/RuntimeValidatorManager.d.ts +31 -0
  144. package/lib/src/runtime/components/RuntimeValidatorManager.d.ts.map +1 -0
  145. package/lib/src/runtime/components/RuntimeValidatorManager.js +55 -0
  146. package/lib/src/runtime/components/RuntimeValidatorManager.js.map +1 -0
  147. package/lib/src/runtime/components/graph-utils.d.ts +33 -0
  148. package/lib/src/runtime/components/graph-utils.d.ts.map +1 -0
  149. package/lib/src/runtime/components/graph-utils.js +292 -0
  150. package/lib/src/runtime/components/graph-utils.js.map +1 -0
  151. package/lib/src/runtime/components/interfaces.d.ts +54 -0
  152. package/lib/src/runtime/components/interfaces.d.ts.map +1 -0
  153. package/lib/src/runtime/components/interfaces.js +2 -0
  154. package/lib/src/runtime/components/interfaces.js.map +1 -0
  155. package/lib/src/runtime/components/types.d.ts +55 -0
  156. package/lib/src/runtime/components/types.d.ts.map +1 -0
  157. package/lib/src/runtime/components/types.js +2 -0
  158. package/lib/src/runtime/components/types.js.map +1 -0
  159. package/lib/src/runtime/utils.d.ts +67 -0
  160. package/lib/src/runtime/utils.d.ts.map +1 -0
  161. package/lib/src/runtime/utils.js +137 -0
  162. package/lib/src/runtime/utils.js.map +1 -0
  163. 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