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