@bian-womp/spark-graph 0.3.14 → 0.3.16

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 CHANGED
@@ -1334,6 +1334,8 @@ class HandleResolver {
1334
1334
  this.registry = registry;
1335
1335
  this.recomputeTokenByNode = new Map();
1336
1336
  this.environment = {};
1337
+ this.pendingResolutions = new Map();
1338
+ this.pendingResolutionRunContexts = new Map();
1337
1339
  this.environment = environment ?? {};
1338
1340
  }
1339
1341
  setRegistry(registry) {
@@ -1342,6 +1344,33 @@ class HandleResolver {
1342
1344
  setEnvironment(environment) {
1343
1345
  this.environment = environment;
1344
1346
  }
1347
+ /**
1348
+ * Check if handle resolution is pending for a node
1349
+ */
1350
+ isResolvingHandles(nodeId) {
1351
+ return this.pendingResolutions.has(nodeId);
1352
+ }
1353
+ /**
1354
+ * Get the promise for pending handle resolution, or null if none
1355
+ */
1356
+ getPendingResolution(nodeId) {
1357
+ return this.pendingResolutions.get(nodeId) || null;
1358
+ }
1359
+ /**
1360
+ * Track additional run contexts for a pending resolution
1361
+ */
1362
+ trackRunContextsForPendingResolution(nodeId, runContextIds) {
1363
+ if (!this.pendingResolutions.has(nodeId))
1364
+ return;
1365
+ const tracked = this.pendingResolutionRunContexts.get(nodeId) ?? new Set();
1366
+ for (const runContextId of runContextIds) {
1367
+ if (!tracked.has(runContextId)) {
1368
+ this.runContextManager.startHandleResolution(runContextId, nodeId);
1369
+ tracked.add(runContextId);
1370
+ }
1371
+ }
1372
+ this.pendingResolutionRunContexts.set(nodeId, tracked);
1373
+ }
1345
1374
  /**
1346
1375
  * Schedule async recomputation of handles for a node
1347
1376
  */
@@ -1354,14 +1383,25 @@ class HandleResolver {
1354
1383
  return;
1355
1384
  // Track resolver start for all active run-contexts
1356
1385
  const activeRunContextIds = this.graph.getNodeRunContextIds(nodeId);
1386
+ const trackedRunContextIds = new Set(activeRunContextIds);
1357
1387
  if (activeRunContextIds.size > 0) {
1358
1388
  for (const runContextId of activeRunContextIds) {
1359
1389
  this.runContextManager.startHandleResolution(runContextId, nodeId);
1360
1390
  }
1361
1391
  }
1362
- setTimeout(() => {
1363
- void this.recomputeHandlesForNode(nodeId, activeRunContextIds.size > 0 ? activeRunContextIds : undefined);
1364
- }, 0);
1392
+ // Create and track the resolution promise
1393
+ const resolutionPromise = new Promise((resolve) => {
1394
+ setTimeout(async () => {
1395
+ // Get all tracked run contexts (including any added during pending state)
1396
+ const allTracked = this.pendingResolutionRunContexts.get(nodeId) ?? trackedRunContextIds;
1397
+ await this.recomputeHandlesForNode(nodeId, allTracked.size > 0 ? allTracked : undefined);
1398
+ this.pendingResolutions.delete(nodeId);
1399
+ this.pendingResolutionRunContexts.delete(nodeId);
1400
+ resolve();
1401
+ }, 0);
1402
+ });
1403
+ this.pendingResolutions.set(nodeId, resolutionPromise);
1404
+ this.pendingResolutionRunContexts.set(nodeId, trackedRunContextIds);
1365
1405
  }
1366
1406
  // Update resolved handles for a single node and refresh edge converters/types that touch it
1367
1407
  updateNodeHandles(nodeId, handles) {
@@ -1906,10 +1946,11 @@ class EdgePropagator {
1906
1946
  * NodeExecutor component - handles node execution scheduling and lifecycle
1907
1947
  */
1908
1948
  class NodeExecutor {
1909
- constructor(graph, eventEmitter, runContextManager, edgePropagator, runtime, environment) {
1949
+ constructor(graph, eventEmitter, runContextManager, handleResolver, edgePropagator, runtime, environment) {
1910
1950
  this.graph = graph;
1911
1951
  this.eventEmitter = eventEmitter;
1912
1952
  this.runContextManager = runContextManager;
1953
+ this.handleResolver = handleResolver;
1913
1954
  this.edgePropagator = edgePropagator;
1914
1955
  this.runtime = runtime;
1915
1956
  this.environment = {};
@@ -2069,10 +2110,31 @@ class NodeExecutor {
2069
2110
  // Early validation for auto-mode paused state
2070
2111
  if (this.runtime.isPaused())
2071
2112
  return;
2072
- // Attach run-context IDs if provided
2113
+ // Attach run-context IDs if provided - do this BEFORE checking for pending resolution
2114
+ // so that handle resolution can track these run contexts
2073
2115
  if (runContextIds) {
2074
2116
  this.graph.addNodeRunContextIds(nodeId, runContextIds);
2075
2117
  }
2118
+ // Check if handles are being resolved - wait for resolution before executing
2119
+ // Do this AFTER setting up run contexts so handle resolution can track them
2120
+ if (this.handleResolver && this.handleResolver.isResolvingHandles(nodeId)) {
2121
+ // Track run contexts for the pending resolution
2122
+ if (runContextIds && runContextIds.size > 0) {
2123
+ this.handleResolver.trackRunContextsForPendingResolution(nodeId, runContextIds);
2124
+ }
2125
+ const pendingResolution = this.handleResolver.getPendingResolution(nodeId);
2126
+ if (pendingResolution) {
2127
+ // Wait for resolution to complete, then re-execute
2128
+ pendingResolution.then(() => {
2129
+ // Re-check node still exists and conditions
2130
+ const nodeAfter = this.graph.getNode(nodeId);
2131
+ if (nodeAfter) {
2132
+ this.execute(nodeId, runContextIds);
2133
+ }
2134
+ });
2135
+ return;
2136
+ }
2137
+ }
2076
2138
  // Handle debouncing
2077
2139
  const now = Date.now();
2078
2140
  if (this.shouldDebounce(nodeId, node, now)) {
@@ -2515,8 +2577,8 @@ class GraphRuntime {
2515
2577
  this.runContextManager = new RunContextManager(this.graph);
2516
2578
  this.handleResolver = new HandleResolver(this.graph, this.eventEmitter, this.runContextManager, this);
2517
2579
  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);
2580
+ // Create NodeExecutor with EdgePropagator and HandleResolver
2581
+ this.nodeExecutor = new NodeExecutor(this.graph, this.eventEmitter, this.runContextManager, this.handleResolver, this, this);
2520
2582
  }
2521
2583
  static create(def, registry, opts) {
2522
2584
  const gr = new GraphRuntime();
@@ -2829,19 +2891,30 @@ class GraphRuntime {
2829
2891
  const releasePause = this.requestPause();
2830
2892
  try {
2831
2893
  const ins = payload?.inputs || {};
2894
+ const nodesWithChangedInputs = new Set();
2832
2895
  for (const [nodeId, map] of Object.entries(ins)) {
2833
2896
  if (!this.graph.hasNode(nodeId))
2834
2897
  continue;
2898
+ let nodeChanged = false;
2835
2899
  for (const [h, v] of Object.entries(map || {})) {
2900
+ const node = this.graph.getNode(nodeId);
2901
+ const prev = node?.inputs[h];
2836
2902
  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
- });
2903
+ const same = valuesEqual(prev, clonedValue);
2904
+ if (!same) {
2905
+ this.graph.updateNodeInput(nodeId, h, clonedValue);
2906
+ this.eventEmitter.emit("value", {
2907
+ nodeId,
2908
+ handle: h,
2909
+ value: clonedValue,
2910
+ io: "input",
2911
+ runtimeTypeId: getTypedOutputTypeId(clonedValue),
2912
+ });
2913
+ nodeChanged = true;
2914
+ }
2915
+ }
2916
+ if (nodeChanged) {
2917
+ nodesWithChangedInputs.add(nodeId);
2845
2918
  }
2846
2919
  }
2847
2920
  const outs = payload?.outputs || {};
@@ -2860,6 +2933,10 @@ class GraphRuntime {
2860
2933
  });
2861
2934
  }
2862
2935
  }
2936
+ // Trigger handle resolution for nodes with changed inputs
2937
+ for (const nodeId of nodesWithChangedInputs) {
2938
+ this.handleResolver.scheduleRecomputeHandles(nodeId);
2939
+ }
2863
2940
  if (opts?.invalidate) {
2864
2941
  for (const nodeId of this.graph.getNodeIds()) {
2865
2942
  this.invalidateDownstream(nodeId);
@@ -3042,6 +3119,8 @@ class GraphRuntime {
3042
3119
  }
3043
3120
  if (changed) {
3044
3121
  this.edgePropagator.clearArrayBuckets(nodeId);
3122
+ // Trigger handle resolution when inputs are removed
3123
+ this.handleResolver.scheduleRecomputeHandles(nodeId);
3045
3124
  if (this.runMode === "auto" &&
3046
3125
  this.graph.allInboundHaveValue(nodeId)) {
3047
3126
  this.execute(nodeId);
@@ -3162,6 +3241,17 @@ class GraphBuilder {
3162
3241
  const arr = Array.isArray(from) ? from : [from];
3163
3242
  return arr.every((s) => s === to || !!this.registry.canCoerce(s, to));
3164
3243
  };
3244
+ // Helper to validate enum value
3245
+ const validateEnumValue = (typeId, value, nodeId, handle) => {
3246
+ if (!typeId.startsWith("enum:"))
3247
+ return true; // Not an enum type
3248
+ const enumDef = this.registry.enums.get(typeId);
3249
+ if (!enumDef)
3250
+ return true; // Enum not registered, skip validation
3251
+ if (typeof value !== "number")
3252
+ return false; // Enum values must be numbers
3253
+ return enumDef.valueToLabel.has(value);
3254
+ };
3165
3255
  const pushIssue = (level, code, message, data) => {
3166
3256
  issues.push({ level, code, message, data });
3167
3257
  };
@@ -3185,6 +3275,51 @@ class GraphBuilder {
3185
3275
  if (!this.registry.categories.has(nodeType.categoryId)) {
3186
3276
  pushIssue("error", "CATEGORY_MISSING", `Unknown category ${nodeType.categoryId} for node type ${n.typeId}`);
3187
3277
  }
3278
+ // Validate enum values in node params
3279
+ if (n.params) {
3280
+ const effectiveHandles = getEffectiveHandles(n);
3281
+ for (const [paramKey, paramValue] of Object.entries(n.params)) {
3282
+ // Skip policy and other non-input params
3283
+ if (paramKey === "policy")
3284
+ continue;
3285
+ // Check if this param corresponds to an input handle
3286
+ const inputTypeId = getInputTypeId(effectiveHandles.inputs, paramKey);
3287
+ if (inputTypeId && inputTypeId.startsWith("enum:")) {
3288
+ if (!validateEnumValue(inputTypeId, paramValue, n.nodeId)) {
3289
+ const enumDef = this.registry.enums.get(inputTypeId);
3290
+ const validValues = enumDef
3291
+ ? Array.from(enumDef.valueToLabel.keys()).join(", ")
3292
+ : "unknown";
3293
+ pushIssue("error", "ENUM_VALUE_INVALID", `Node ${n.nodeId} param ${paramKey} has invalid enum value ${paramValue}. Valid values: ${validValues}`, {
3294
+ nodeId: n.nodeId,
3295
+ input: paramKey,
3296
+ typeId: inputTypeId,
3297
+ });
3298
+ }
3299
+ }
3300
+ }
3301
+ }
3302
+ // Validate enum values in input defaults
3303
+ const resolved = n.resolvedHandles;
3304
+ if (resolved?.inputDefaults) {
3305
+ const effectiveHandles = getEffectiveHandles(n);
3306
+ for (const [handle, defaultValue] of Object.entries(resolved.inputDefaults)) {
3307
+ const inputTypeId = getInputTypeId(effectiveHandles.inputs, handle);
3308
+ if (inputTypeId && inputTypeId.startsWith("enum:")) {
3309
+ if (!validateEnumValue(inputTypeId, defaultValue, n.nodeId)) {
3310
+ const enumDef = this.registry.enums.get(inputTypeId);
3311
+ const validValues = enumDef
3312
+ ? Array.from(enumDef.valueToLabel.keys()).join(", ")
3313
+ : "unknown";
3314
+ pushIssue("warning", "ENUM_DEFAULT_INVALID", `Node ${n.nodeId} input default ${handle} has invalid enum value ${defaultValue}. Valid values: ${validValues}`, {
3315
+ nodeId: n.nodeId,
3316
+ input: handle,
3317
+ typeId: inputTypeId,
3318
+ });
3319
+ }
3320
+ }
3321
+ }
3322
+ }
3188
3323
  }
3189
3324
  // edges validation: nodes exist, handles exist, type exists
3190
3325
  const inboundCounts = new Map();