@bian-womp/spark-graph 0.3.15 → 0.3.17

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 (35) hide show
  1. package/lib/cjs/index.cjs +415 -92
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/builder/GraphBuilder.d.ts.map +1 -1
  4. package/lib/cjs/src/index.d.ts +1 -0
  5. package/lib/cjs/src/index.d.ts.map +1 -1
  6. package/lib/cjs/src/runtime/GraphRuntime.d.ts.map +1 -1
  7. package/lib/cjs/src/runtime/components/EdgePropagator.d.ts.map +1 -1
  8. package/lib/cjs/src/runtime/components/HandleResolver.d.ts +15 -1
  9. package/lib/cjs/src/runtime/components/HandleResolver.d.ts.map +1 -1
  10. package/lib/cjs/src/runtime/components/NodeExecutor.d.ts +3 -2
  11. package/lib/cjs/src/runtime/components/NodeExecutor.d.ts.map +1 -1
  12. package/lib/cjs/src/runtime/components/RunContextManager.d.ts +7 -1
  13. package/lib/cjs/src/runtime/components/RunContextManager.d.ts.map +1 -1
  14. package/lib/cjs/src/runtime/components/interfaces.d.ts +3 -0
  15. package/lib/cjs/src/runtime/components/interfaces.d.ts.map +1 -1
  16. package/lib/cjs/src/runtime/utils.d.ts +51 -0
  17. package/lib/cjs/src/runtime/utils.d.ts.map +1 -1
  18. package/lib/esm/index.js +415 -93
  19. package/lib/esm/index.js.map +1 -1
  20. package/lib/esm/src/builder/GraphBuilder.d.ts.map +1 -1
  21. package/lib/esm/src/index.d.ts +1 -0
  22. package/lib/esm/src/index.d.ts.map +1 -1
  23. package/lib/esm/src/runtime/GraphRuntime.d.ts.map +1 -1
  24. package/lib/esm/src/runtime/components/EdgePropagator.d.ts.map +1 -1
  25. package/lib/esm/src/runtime/components/HandleResolver.d.ts +15 -1
  26. package/lib/esm/src/runtime/components/HandleResolver.d.ts.map +1 -1
  27. package/lib/esm/src/runtime/components/NodeExecutor.d.ts +3 -2
  28. package/lib/esm/src/runtime/components/NodeExecutor.d.ts.map +1 -1
  29. package/lib/esm/src/runtime/components/RunContextManager.d.ts +7 -1
  30. package/lib/esm/src/runtime/components/RunContextManager.d.ts.map +1 -1
  31. package/lib/esm/src/runtime/components/interfaces.d.ts +3 -0
  32. package/lib/esm/src/runtime/components/interfaces.d.ts.map +1 -1
  33. package/lib/esm/src/runtime/utils.d.ts +51 -0
  34. package/lib/esm/src/runtime/utils.d.ts.map +1 -1
  35. package/package.json +2 -2
package/lib/cjs/index.cjs CHANGED
@@ -932,15 +932,166 @@ class EventEmitter {
932
932
  }
933
933
  }
934
934
 
935
+ const LOG_LEVEL_VALUES = {
936
+ debug: 0,
937
+ info: 1,
938
+ warn: 2,
939
+ error: 3,
940
+ silent: 4,
941
+ };
942
+
943
+ /**
944
+ * Shared utility functions for runtime components
945
+ */
946
+ /**
947
+ * Type guard to check if a value is a Promise
948
+ */
949
+ function isPromise(value) {
950
+ return !!value && typeof value.then === "function";
951
+ }
952
+ /**
953
+ * Unwrap a value that might be a Promise
954
+ */
955
+ async function unwrapMaybePromise(value) {
956
+ return isPromise(value) ? await value : value;
957
+ }
958
+ /**
959
+ * Shallow/deep-ish equality check to avoid unnecessary runs on identical values
960
+ */
961
+ function valuesEqual(a, b) {
962
+ if (a === b)
963
+ return true;
964
+ if (typeof a !== typeof b)
965
+ return false;
966
+ if (a && b && typeof a === "object") {
967
+ try {
968
+ return JSON.stringify(a) === JSON.stringify(b);
969
+ }
970
+ catch {
971
+ return false;
972
+ }
973
+ }
974
+ return false;
975
+ }
976
+ /**
977
+ * A reusable logger class that supports configurable log levels and prefixes.
978
+ * Can be instantiated with a default log level and optionally override per call.
979
+ */
980
+ class LevelLogger {
981
+ constructor(defaultLevel = "info", prefix = "") {
982
+ this.defaultLevel = defaultLevel;
983
+ this.prefix = prefix;
984
+ }
985
+ /**
986
+ * Sets the prefix for log messages
987
+ */
988
+ setPrefix(prefix) {
989
+ this.prefix = prefix;
990
+ }
991
+ /**
992
+ * Gets the current prefix
993
+ */
994
+ getPrefix() {
995
+ return this.prefix;
996
+ }
997
+ /**
998
+ * Sets the default log level for this logger instance
999
+ */
1000
+ setLevel(level) {
1001
+ this.defaultLevel = level;
1002
+ }
1003
+ /**
1004
+ * Gets the current default log level
1005
+ */
1006
+ getLevel() {
1007
+ return this.defaultLevel;
1008
+ }
1009
+ getLevelValue() {
1010
+ return LevelLogger.levelValues[this.defaultLevel];
1011
+ }
1012
+ /**
1013
+ * Logs a debug message
1014
+ */
1015
+ debug(message, context, overrideLevel) {
1016
+ this.log("debug", message, context, overrideLevel);
1017
+ }
1018
+ /**
1019
+ * Logs an info message
1020
+ */
1021
+ info(message, context, overrideLevel) {
1022
+ this.log("info", message, context, overrideLevel);
1023
+ }
1024
+ /**
1025
+ * Logs a warning message
1026
+ */
1027
+ warn(message, context, overrideLevel) {
1028
+ this.log("warn", message, context, overrideLevel);
1029
+ }
1030
+ /**
1031
+ * Logs an error message
1032
+ */
1033
+ error(message, context, overrideLevel) {
1034
+ this.log("error", message, context, overrideLevel);
1035
+ }
1036
+ /**
1037
+ * Core logging method that respects the log level and applies prefix
1038
+ */
1039
+ log(requestedLevel, message, context, overrideLevel) {
1040
+ const effectiveLevel = overrideLevel ?? this.defaultLevel;
1041
+ // Silent level suppresses all logs
1042
+ if (effectiveLevel === "silent") {
1043
+ return;
1044
+ }
1045
+ const requestedValue = LevelLogger.levelValues[requestedLevel] ?? 1;
1046
+ const effectiveValue = LevelLogger.levelValues[effectiveLevel] ?? 1;
1047
+ // Only log if the requested level is >= effective level
1048
+ if (requestedValue >= effectiveValue) {
1049
+ const contextStr = context
1050
+ ? ` ${Object.entries(context)
1051
+ .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
1052
+ .join(" ")}`
1053
+ : "";
1054
+ const prefixedMessage = this.prefix
1055
+ ? `${this.prefix} ${message}${contextStr}`
1056
+ : `${message}${contextStr}`;
1057
+ switch (requestedLevel) {
1058
+ case "debug":
1059
+ console.info(prefixedMessage);
1060
+ break;
1061
+ case "info":
1062
+ console.info(prefixedMessage);
1063
+ break;
1064
+ case "warn":
1065
+ console.warn(prefixedMessage);
1066
+ break;
1067
+ case "error":
1068
+ console.error(prefixedMessage);
1069
+ break;
1070
+ }
1071
+ }
1072
+ }
1073
+ }
1074
+ /**
1075
+ * Maps log levels to numeric values for comparison
1076
+ */
1077
+ LevelLogger.levelValues = LOG_LEVEL_VALUES;
1078
+
935
1079
  /**
936
1080
  * RunContextManager component - manages run-context lifecycle
937
1081
  */
938
1082
  class RunContextManager {
939
- constructor(graph) {
1083
+ constructor(graph, logLevel) {
940
1084
  this.graph = graph;
941
1085
  this.runContexts = new Map();
942
1086
  this.runContextCounter = 0;
943
1087
  this.graph = graph;
1088
+ this.logger = new LevelLogger(logLevel ?? "info", "[RunContextManager]");
1089
+ }
1090
+ /**
1091
+ * Set the log level for this manager
1092
+ */
1093
+ setLogLevel(logLevel) {
1094
+ this.logger.setLevel(logLevel);
944
1095
  }
945
1096
  /**
946
1097
  * Create a new run-context for runFromHere
@@ -959,6 +1110,12 @@ class RunContextManager {
959
1110
  resolve,
960
1111
  };
961
1112
  this.runContexts.set(id, ctx);
1113
+ this.logger.info("create-run-context", {
1114
+ id,
1115
+ startNodeId,
1116
+ skipPropagateValues: ctx.skipPropagateValues,
1117
+ propagate: ctx.propagate,
1118
+ });
962
1119
  return id;
963
1120
  }
964
1121
  /**
@@ -981,41 +1138,101 @@ class RunContextManager {
981
1138
  }
982
1139
  startNodeRun(id, nodeId) {
983
1140
  const ctx = this.runContexts.get(id);
984
- if (!ctx)
1141
+ if (!ctx) {
1142
+ this.logger.debug("start-node-run-context-not-found", {
1143
+ runContextId: id,
1144
+ nodeId,
1145
+ });
985
1146
  return;
1147
+ }
986
1148
  ctx.pendingNodes++;
1149
+ this.logger.debug("start-node-run", {
1150
+ runContextId: id,
1151
+ nodeId,
1152
+ pendingNodes: ctx.pendingNodes,
1153
+ });
987
1154
  }
988
1155
  finishNodeRun(id, nodeId) {
989
1156
  const ctx = this.runContexts.get(id);
990
- if (!ctx)
1157
+ if (!ctx) {
1158
+ this.logger.debug("finish-node-run-context-not-found", {
1159
+ runContextId: id,
1160
+ nodeId,
1161
+ });
991
1162
  return;
1163
+ }
992
1164
  ctx.pendingNodes--;
1165
+ this.logger.debug("finish-node-run", {
1166
+ runContextId: id,
1167
+ nodeId,
1168
+ pendingNodes: ctx.pendingNodes,
1169
+ });
993
1170
  this.finishRunContextIfPossible(id);
994
1171
  }
995
1172
  startEdgeConversion(id, edgeId) {
996
1173
  const ctx = this.runContexts.get(id);
997
- if (!ctx)
1174
+ if (!ctx) {
1175
+ this.logger.debug("start-edge-conversion-context-not-found", {
1176
+ runContextId: id,
1177
+ edgeId,
1178
+ });
998
1179
  return;
1180
+ }
999
1181
  ctx.pendingEdges++;
1182
+ this.logger.debug("start-edge-conversion", {
1183
+ runContextId: id,
1184
+ edgeId,
1185
+ pendingEdges: ctx.pendingEdges,
1186
+ });
1000
1187
  }
1001
1188
  finishEdgeConversion(id, edgeId) {
1002
1189
  const ctx = this.runContexts.get(id);
1003
- if (!ctx)
1190
+ if (!ctx) {
1191
+ this.logger.debug("finish-edge-conversion-context-not-found", {
1192
+ runContextId: id,
1193
+ edgeId,
1194
+ });
1004
1195
  return;
1196
+ }
1005
1197
  ctx.pendingEdges--;
1198
+ this.logger.debug("finish-edge-conversion", {
1199
+ runContextId: id,
1200
+ edgeId,
1201
+ pendingEdges: ctx.pendingEdges,
1202
+ });
1006
1203
  this.finishRunContextIfPossible(id);
1007
1204
  }
1008
1205
  startHandleResolution(id, nodeId) {
1009
1206
  const ctx = this.runContexts.get(id);
1010
- if (!ctx)
1207
+ if (!ctx) {
1208
+ this.logger.debug("start-handle-resolution-context-not-found", {
1209
+ runContextId: id,
1210
+ nodeId,
1211
+ });
1011
1212
  return;
1213
+ }
1012
1214
  ctx.pendingResolvers++;
1215
+ this.logger.debug("start-handle-resolution", {
1216
+ runContextId: id,
1217
+ nodeId,
1218
+ pendingResolvers: ctx.pendingResolvers,
1219
+ });
1013
1220
  }
1014
1221
  finishHandleResolution(id, nodeId) {
1015
1222
  const ctx = this.runContexts.get(id);
1016
- if (!ctx)
1223
+ if (!ctx) {
1224
+ this.logger.debug("finish-handle-resolution-context-not-found", {
1225
+ runContextId: id,
1226
+ nodeId,
1227
+ });
1017
1228
  return;
1229
+ }
1018
1230
  ctx.pendingResolvers--;
1231
+ this.logger.debug("finish-handle-resolution", {
1232
+ runContextId: id,
1233
+ nodeId,
1234
+ pendingResolvers: ctx.pendingResolvers,
1235
+ });
1019
1236
  this.finishRunContextIfPossible(id);
1020
1237
  }
1021
1238
  /**
@@ -1023,13 +1240,22 @@ class RunContextManager {
1023
1240
  */
1024
1241
  finishRunContextIfPossible(id) {
1025
1242
  const ctx = this.runContexts.get(id);
1026
- if (!ctx)
1243
+ if (!ctx) {
1244
+ this.logger.debug("finish-run-context-not-found", {
1245
+ runContextId: id,
1246
+ });
1027
1247
  return;
1248
+ }
1028
1249
  if (ctx.pendingNodes > 0 ||
1029
1250
  ctx.pendingEdges > 0 ||
1030
1251
  ctx.pendingResolvers > 0) {
1031
1252
  return; // Still has pending work
1032
1253
  }
1254
+ this.logger.info("finish-run-context", {
1255
+ runContextId: id,
1256
+ startNodes: Array.from(ctx.startNodes),
1257
+ cancelledNodes: Array.from(ctx.cancelledNodes),
1258
+ });
1033
1259
  // Clean up activeRunContexts from all nodes
1034
1260
  this.graph.forEachNode((node) => {
1035
1261
  this.graph.removeNodeRunContextId(node.nodeId, id);
@@ -1067,6 +1293,12 @@ class RunContextManager {
1067
1293
  });
1068
1294
  }
1069
1295
  }
1296
+ this.logger.debug("cancel-node-in-run-contexts", {
1297
+ nodeId,
1298
+ includeDownstream,
1299
+ cancelledNodes: Array.from(toCancel),
1300
+ affectedRunContexts: Array.from(this.runContexts.keys()),
1301
+ });
1070
1302
  // Mark nodes as cancelled in all run-contexts
1071
1303
  for (const ctx of this.runContexts.values()) {
1072
1304
  for (const id of toCancel) {
@@ -1082,6 +1314,11 @@ class RunContextManager {
1082
1314
  * Resolve all pending run-context promises (for cleanup)
1083
1315
  */
1084
1316
  resolveAll() {
1317
+ const count = this.runContexts.size;
1318
+ this.logger.info("resolve-all-run-contexts", {
1319
+ count,
1320
+ runContextIds: Array.from(this.runContexts.keys()),
1321
+ });
1085
1322
  for (const ctx of this.runContexts.values()) {
1086
1323
  if (ctx.resolve)
1087
1324
  ctx.resolve();
@@ -1091,52 +1328,12 @@ class RunContextManager {
1091
1328
  * Clear all run-contexts
1092
1329
  */
1093
1330
  clear() {
1331
+ const count = this.runContexts.size;
1332
+ this.logger.info("clear-all-run-contexts", { count });
1094
1333
  this.runContexts.clear();
1095
1334
  }
1096
1335
  }
1097
1336
 
1098
- const LOG_LEVEL_VALUES = {
1099
- debug: 0,
1100
- info: 1,
1101
- warn: 2,
1102
- error: 3,
1103
- silent: 4,
1104
- };
1105
-
1106
- /**
1107
- * Shared utility functions for runtime components
1108
- */
1109
- /**
1110
- * Type guard to check if a value is a Promise
1111
- */
1112
- function isPromise(value) {
1113
- return !!value && typeof value.then === "function";
1114
- }
1115
- /**
1116
- * Unwrap a value that might be a Promise
1117
- */
1118
- async function unwrapMaybePromise(value) {
1119
- return isPromise(value) ? await value : value;
1120
- }
1121
- /**
1122
- * Shallow/deep-ish equality check to avoid unnecessary runs on identical values
1123
- */
1124
- function valuesEqual(a, b) {
1125
- if (a === b)
1126
- return true;
1127
- if (typeof a !== typeof b)
1128
- return false;
1129
- if (a && b && typeof a === "object") {
1130
- try {
1131
- return JSON.stringify(a) === JSON.stringify(b);
1132
- }
1133
- catch {
1134
- return false;
1135
- }
1136
- }
1137
- return false;
1138
- }
1139
-
1140
1337
  function tryHandleResolving(def, registry, environment) {
1141
1338
  const out = new Map();
1142
1339
  const pending = new Set();
@@ -1334,6 +1531,8 @@ class HandleResolver {
1334
1531
  this.registry = registry;
1335
1532
  this.recomputeTokenByNode = new Map();
1336
1533
  this.environment = {};
1534
+ this.pendingResolutions = new Map();
1535
+ this.pendingResolutionRunContexts = new Map();
1337
1536
  this.environment = environment ?? {};
1338
1537
  }
1339
1538
  setRegistry(registry) {
@@ -1342,6 +1541,33 @@ class HandleResolver {
1342
1541
  setEnvironment(environment) {
1343
1542
  this.environment = environment;
1344
1543
  }
1544
+ /**
1545
+ * Check if handle resolution is pending for a node
1546
+ */
1547
+ isResolvingHandles(nodeId) {
1548
+ return this.pendingResolutions.has(nodeId);
1549
+ }
1550
+ /**
1551
+ * Get the promise for pending handle resolution, or null if none
1552
+ */
1553
+ getPendingResolution(nodeId) {
1554
+ return this.pendingResolutions.get(nodeId) || null;
1555
+ }
1556
+ /**
1557
+ * Track additional run contexts for a pending resolution
1558
+ */
1559
+ trackRunContextsForPendingResolution(nodeId, runContextIds) {
1560
+ if (!this.pendingResolutions.has(nodeId))
1561
+ return;
1562
+ const tracked = this.pendingResolutionRunContexts.get(nodeId) ?? new Set();
1563
+ for (const runContextId of runContextIds) {
1564
+ if (!tracked.has(runContextId)) {
1565
+ this.runContextManager.startHandleResolution(runContextId, nodeId);
1566
+ tracked.add(runContextId);
1567
+ }
1568
+ }
1569
+ this.pendingResolutionRunContexts.set(nodeId, tracked);
1570
+ }
1345
1571
  /**
1346
1572
  * Schedule async recomputation of handles for a node
1347
1573
  */
@@ -1354,14 +1580,25 @@ class HandleResolver {
1354
1580
  return;
1355
1581
  // Track resolver start for all active run-contexts
1356
1582
  const activeRunContextIds = this.graph.getNodeRunContextIds(nodeId);
1583
+ const trackedRunContextIds = new Set(activeRunContextIds);
1357
1584
  if (activeRunContextIds.size > 0) {
1358
1585
  for (const runContextId of activeRunContextIds) {
1359
1586
  this.runContextManager.startHandleResolution(runContextId, nodeId);
1360
1587
  }
1361
1588
  }
1362
- setTimeout(() => {
1363
- void this.recomputeHandlesForNode(nodeId, activeRunContextIds.size > 0 ? activeRunContextIds : undefined);
1364
- }, 0);
1589
+ // Create and track the resolution promise
1590
+ const resolutionPromise = new Promise((resolve) => {
1591
+ setTimeout(async () => {
1592
+ // Get all tracked run contexts (including any added during pending state)
1593
+ const allTracked = this.pendingResolutionRunContexts.get(nodeId) ?? trackedRunContextIds;
1594
+ await this.recomputeHandlesForNode(nodeId, allTracked.size > 0 ? allTracked : undefined);
1595
+ this.pendingResolutions.delete(nodeId);
1596
+ this.pendingResolutionRunContexts.delete(nodeId);
1597
+ resolve();
1598
+ }, 0);
1599
+ });
1600
+ this.pendingResolutions.set(nodeId, resolutionPromise);
1601
+ this.pendingResolutionRunContexts.set(nodeId, trackedRunContextIds);
1365
1602
  }
1366
1603
  // Update resolved handles for a single node and refresh edge converters/types that touch it
1367
1604
  updateNodeHandles(nodeId, handles) {
@@ -1906,10 +2143,11 @@ class EdgePropagator {
1906
2143
  * NodeExecutor component - handles node execution scheduling and lifecycle
1907
2144
  */
1908
2145
  class NodeExecutor {
1909
- constructor(graph, eventEmitter, runContextManager, edgePropagator, runtime, environment) {
2146
+ constructor(graph, eventEmitter, runContextManager, handleResolver, edgePropagator, runtime, environment) {
1910
2147
  this.graph = graph;
1911
2148
  this.eventEmitter = eventEmitter;
1912
2149
  this.runContextManager = runContextManager;
2150
+ this.handleResolver = handleResolver;
1913
2151
  this.edgePropagator = edgePropagator;
1914
2152
  this.runtime = runtime;
1915
2153
  this.environment = {};
@@ -1978,33 +2216,23 @@ class NodeExecutor {
1978
2216
  progress: Math.max(0, Math.min(1, Number(p) || 0)),
1979
2217
  });
1980
2218
  });
1981
- // Create log function that respects node's logLevel
2219
+ // Create log function that respects node's logLevel using LevelLogger
2220
+ const nodeLogLevel = node.logLevel ?? "info";
2221
+ const logger = new LevelLogger(nodeLogLevel, `[node:${runId || nodeId}:${node.typeId}]`);
1982
2222
  const log = (level, message, context) => {
1983
- const nodeLogLevel = node.logLevel ?? "info";
1984
- const nodeLogValue = LOG_LEVEL_VALUES[nodeLogLevel] ?? 1;
1985
- const requestedValue = LOG_LEVEL_VALUES[level] ?? 1;
1986
- // Only log if requested level >= node's logLevel
1987
- if (requestedValue >= nodeLogValue && nodeLogLevel !== "silent") {
1988
- const contextStr = context
1989
- ? ` ${Object.entries(context)
1990
- .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
1991
- .join(" ")}`
1992
- : "";
1993
- const fullMessage = `[node:${runId || nodeId}:${node.typeId}] ${message}${contextStr}`;
1994
- switch (level) {
1995
- case "debug":
1996
- console.info(fullMessage);
1997
- break;
1998
- case "info":
1999
- console.info(fullMessage);
2000
- break;
2001
- case "warn":
2002
- console.warn(fullMessage);
2003
- break;
2004
- case "error":
2005
- console.error(fullMessage);
2006
- break;
2007
- }
2223
+ switch (level) {
2224
+ case "debug":
2225
+ logger.debug(message, context);
2226
+ break;
2227
+ case "info":
2228
+ logger.info(message, context);
2229
+ break;
2230
+ case "warn":
2231
+ logger.warn(message, context);
2232
+ break;
2233
+ case "error":
2234
+ logger.error(message, context);
2235
+ break;
2008
2236
  }
2009
2237
  };
2010
2238
  return {
@@ -2069,10 +2297,31 @@ class NodeExecutor {
2069
2297
  // Early validation for auto-mode paused state
2070
2298
  if (this.runtime.isPaused())
2071
2299
  return;
2072
- // Attach run-context IDs if provided
2300
+ // Attach run-context IDs if provided - do this BEFORE checking for pending resolution
2301
+ // so that handle resolution can track these run contexts
2073
2302
  if (runContextIds) {
2074
2303
  this.graph.addNodeRunContextIds(nodeId, runContextIds);
2075
2304
  }
2305
+ // Check if handles are being resolved - wait for resolution before executing
2306
+ // Do this AFTER setting up run contexts so handle resolution can track them
2307
+ if (this.handleResolver && this.handleResolver.isResolvingHandles(nodeId)) {
2308
+ // Track run contexts for the pending resolution
2309
+ if (runContextIds && runContextIds.size > 0) {
2310
+ this.handleResolver.trackRunContextsForPendingResolution(nodeId, runContextIds);
2311
+ }
2312
+ const pendingResolution = this.handleResolver.getPendingResolution(nodeId);
2313
+ if (pendingResolution) {
2314
+ // Wait for resolution to complete, then re-execute
2315
+ pendingResolution.then(() => {
2316
+ // Re-check node still exists and conditions
2317
+ const nodeAfter = this.graph.getNode(nodeId);
2318
+ if (nodeAfter) {
2319
+ this.execute(nodeId, runContextIds);
2320
+ }
2321
+ });
2322
+ return;
2323
+ }
2324
+ }
2076
2325
  // Handle debouncing
2077
2326
  const now = Date.now();
2078
2327
  if (this.shouldDebounce(nodeId, node, now)) {
@@ -2512,11 +2761,11 @@ class GraphRuntime {
2512
2761
  // Initialize components
2513
2762
  this.graph = new Graph();
2514
2763
  this.eventEmitter = new EventEmitter();
2515
- this.runContextManager = new RunContextManager(this.graph);
2764
+ this.runContextManager = new RunContextManager(this.graph, "debug");
2516
2765
  this.handleResolver = new HandleResolver(this.graph, this.eventEmitter, this.runContextManager, this);
2517
2766
  this.edgePropagator = new EdgePropagator(this.graph, this.eventEmitter, this.runContextManager, this.handleResolver, this);
2518
- // Create NodeExecutor with EdgePropagator
2519
- this.nodeExecutor = new NodeExecutor(this.graph, this.eventEmitter, this.runContextManager, this, this);
2767
+ // Create NodeExecutor with EdgePropagator and HandleResolver
2768
+ this.nodeExecutor = new NodeExecutor(this.graph, this.eventEmitter, this.runContextManager, this.handleResolver, this, this);
2520
2769
  }
2521
2770
  static create(def, registry, opts) {
2522
2771
  const gr = new GraphRuntime();
@@ -2829,19 +3078,30 @@ class GraphRuntime {
2829
3078
  const releasePause = this.requestPause();
2830
3079
  try {
2831
3080
  const ins = payload?.inputs || {};
3081
+ const nodesWithChangedInputs = new Set();
2832
3082
  for (const [nodeId, map] of Object.entries(ins)) {
2833
3083
  if (!this.graph.hasNode(nodeId))
2834
3084
  continue;
3085
+ let nodeChanged = false;
2835
3086
  for (const [h, v] of Object.entries(map || {})) {
3087
+ const node = this.graph.getNode(nodeId);
3088
+ const prev = node?.inputs[h];
2836
3089
  const clonedValue = structuredClone(v);
2837
- this.graph.updateNodeInput(nodeId, h, clonedValue);
2838
- this.eventEmitter.emit("value", {
2839
- nodeId,
2840
- handle: h,
2841
- value: clonedValue,
2842
- io: "input",
2843
- runtimeTypeId: getTypedOutputTypeId(clonedValue),
2844
- });
3090
+ const same = valuesEqual(prev, clonedValue);
3091
+ if (!same) {
3092
+ this.graph.updateNodeInput(nodeId, h, clonedValue);
3093
+ this.eventEmitter.emit("value", {
3094
+ nodeId,
3095
+ handle: h,
3096
+ value: clonedValue,
3097
+ io: "input",
3098
+ runtimeTypeId: getTypedOutputTypeId(clonedValue),
3099
+ });
3100
+ nodeChanged = true;
3101
+ }
3102
+ }
3103
+ if (nodeChanged) {
3104
+ nodesWithChangedInputs.add(nodeId);
2845
3105
  }
2846
3106
  }
2847
3107
  const outs = payload?.outputs || {};
@@ -2860,6 +3120,10 @@ class GraphRuntime {
2860
3120
  });
2861
3121
  }
2862
3122
  }
3123
+ // Trigger handle resolution for nodes with changed inputs
3124
+ for (const nodeId of nodesWithChangedInputs) {
3125
+ this.handleResolver.scheduleRecomputeHandles(nodeId);
3126
+ }
2863
3127
  if (opts?.invalidate) {
2864
3128
  for (const nodeId of this.graph.getNodeIds()) {
2865
3129
  this.invalidateDownstream(nodeId);
@@ -3042,6 +3306,8 @@ class GraphRuntime {
3042
3306
  }
3043
3307
  if (changed) {
3044
3308
  this.edgePropagator.clearArrayBuckets(nodeId);
3309
+ // Trigger handle resolution when inputs are removed
3310
+ this.handleResolver.scheduleRecomputeHandles(nodeId);
3045
3311
  if (this.runMode === "auto" &&
3046
3312
  this.graph.allInboundHaveValue(nodeId)) {
3047
3313
  this.execute(nodeId);
@@ -3162,6 +3428,17 @@ class GraphBuilder {
3162
3428
  const arr = Array.isArray(from) ? from : [from];
3163
3429
  return arr.every((s) => s === to || !!this.registry.canCoerce(s, to));
3164
3430
  };
3431
+ // Helper to validate enum value
3432
+ const validateEnumValue = (typeId, value, nodeId, handle) => {
3433
+ if (!typeId.startsWith("enum:"))
3434
+ return true; // Not an enum type
3435
+ const enumDef = this.registry.enums.get(typeId);
3436
+ if (!enumDef)
3437
+ return true; // Enum not registered, skip validation
3438
+ if (typeof value !== "number")
3439
+ return false; // Enum values must be numbers
3440
+ return enumDef.valueToLabel.has(value);
3441
+ };
3165
3442
  const pushIssue = (level, code, message, data) => {
3166
3443
  issues.push({ level, code, message, data });
3167
3444
  };
@@ -3185,6 +3462,51 @@ class GraphBuilder {
3185
3462
  if (!this.registry.categories.has(nodeType.categoryId)) {
3186
3463
  pushIssue("error", "CATEGORY_MISSING", `Unknown category ${nodeType.categoryId} for node type ${n.typeId}`);
3187
3464
  }
3465
+ // Validate enum values in node params
3466
+ if (n.params) {
3467
+ const effectiveHandles = getEffectiveHandles(n);
3468
+ for (const [paramKey, paramValue] of Object.entries(n.params)) {
3469
+ // Skip policy and other non-input params
3470
+ if (paramKey === "policy")
3471
+ continue;
3472
+ // Check if this param corresponds to an input handle
3473
+ const inputTypeId = getInputTypeId(effectiveHandles.inputs, paramKey);
3474
+ if (inputTypeId && inputTypeId.startsWith("enum:")) {
3475
+ if (!validateEnumValue(inputTypeId, paramValue, n.nodeId)) {
3476
+ const enumDef = this.registry.enums.get(inputTypeId);
3477
+ const validValues = enumDef
3478
+ ? Array.from(enumDef.valueToLabel.keys()).join(", ")
3479
+ : "unknown";
3480
+ pushIssue("error", "ENUM_VALUE_INVALID", `Node ${n.nodeId} param ${paramKey} has invalid enum value ${paramValue}. Valid values: ${validValues}`, {
3481
+ nodeId: n.nodeId,
3482
+ input: paramKey,
3483
+ typeId: inputTypeId,
3484
+ });
3485
+ }
3486
+ }
3487
+ }
3488
+ }
3489
+ // Validate enum values in input defaults
3490
+ const resolved = n.resolvedHandles;
3491
+ if (resolved?.inputDefaults) {
3492
+ const effectiveHandles = getEffectiveHandles(n);
3493
+ for (const [handle, defaultValue] of Object.entries(resolved.inputDefaults)) {
3494
+ const inputTypeId = getInputTypeId(effectiveHandles.inputs, handle);
3495
+ if (inputTypeId && inputTypeId.startsWith("enum:")) {
3496
+ if (!validateEnumValue(inputTypeId, defaultValue, n.nodeId)) {
3497
+ const enumDef = this.registry.enums.get(inputTypeId);
3498
+ const validValues = enumDef
3499
+ ? Array.from(enumDef.valueToLabel.keys()).join(", ")
3500
+ : "unknown";
3501
+ pushIssue("warning", "ENUM_DEFAULT_INVALID", `Node ${n.nodeId} input default ${handle} has invalid enum value ${defaultValue}. Valid values: ${validValues}`, {
3502
+ nodeId: n.nodeId,
3503
+ input: handle,
3504
+ typeId: inputTypeId,
3505
+ });
3506
+ }
3507
+ }
3508
+ }
3509
+ }
3188
3510
  }
3189
3511
  // edges validation: nodes exist, handles exist, type exists
3190
3512
  const inboundCounts = new Map();
@@ -5574,6 +5896,7 @@ exports.CompositeCategory = CompositeCategory;
5574
5896
  exports.ComputeCategory = ComputeCategory;
5575
5897
  exports.GraphBuilder = GraphBuilder;
5576
5898
  exports.GraphRuntime = GraphRuntime;
5899
+ exports.LevelLogger = LevelLogger;
5577
5900
  exports.LocalEngine = LocalEngine;
5578
5901
  exports.Registry = Registry;
5579
5902
  exports.buildValueConverter = buildValueConverter;