@bian-womp/spark-graph 0.3.6 → 0.3.8

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
@@ -1238,14 +1238,35 @@ class EdgePropagator {
1238
1238
  const processedValue = this.processArrayInput(edge, value);
1239
1239
  // Check if value changed
1240
1240
  const prev = dstNode.inputs[edge.target.handle];
1241
- if (valuesEqual(prev, processedValue)) {
1242
- return; // No change
1241
+ const valueChanged = !valuesEqual(prev, processedValue);
1242
+ // Check if we should execute even if value is same:
1243
+ // 1. If node has rerunOnSameInput policy (from node params or registry)
1244
+ // 2. If input was set after last successful run (stale input detection)
1245
+ const registry = this.graph.getRegistry();
1246
+ const desc = registry?.nodes.get(dstNode.typeId);
1247
+ const nodeRerunPolicy = dstNode.policy?.rerunOnSameInput === true;
1248
+ const descRerunPolicy = desc?.policy?.rerunOnSameInput === true;
1249
+ const shouldRerunOnSameInput = nodeRerunPolicy || descRerunPolicy;
1250
+ const inputWasSetAfterLastRun = dstNode.lastInputAt?.[edge.target.handle] &&
1251
+ dstNode.lastSuccessAt &&
1252
+ dstNode.lastInputAt[edge.target.handle] > dstNode.lastSuccessAt;
1253
+ const shouldExecute = valueChanged || shouldRerunOnSameInput || inputWasSetAfterLastRun;
1254
+ if (!shouldExecute) {
1255
+ return; // No change and no reason to rerun
1243
1256
  }
1244
1257
  // Set input value (respecting skipPropagateValues)
1245
1258
  const shouldSetValue = this.shouldSetInputValue(effectiveRunContexts);
1246
- if (shouldSetValue) {
1259
+ if (shouldSetValue && valueChanged) {
1247
1260
  this.setTargetInput(edge, dstNode, processedValue);
1248
1261
  }
1262
+ else if (shouldSetValue && !valueChanged) {
1263
+ // Even if value didn't change, update timestamp if we're forcing execution
1264
+ const now = Date.now();
1265
+ if (!dstNode.lastInputAt) {
1266
+ dstNode.lastInputAt = {};
1267
+ }
1268
+ dstNode.lastInputAt[edge.target.handle] = now;
1269
+ }
1249
1270
  // Schedule downstream execution
1250
1271
  this.executeDownstream(edge.target.nodeId, effectiveRunContexts);
1251
1272
  }
@@ -1302,7 +1323,13 @@ class EdgePropagator {
1302
1323
  * Set target input value and emit event
1303
1324
  */
1304
1325
  setTargetInput(edge, dstNode, value) {
1326
+ const now = Date.now();
1305
1327
  dstNode.inputs[edge.target.handle] = value;
1328
+ // Track when this input was set
1329
+ if (!dstNode.lastInputAt) {
1330
+ dstNode.lastInputAt = {};
1331
+ }
1332
+ dstNode.lastInputAt[edge.target.handle] = now;
1306
1333
  this.eventEmitter.emit("value", {
1307
1334
  nodeId: edge.target.nodeId,
1308
1335
  handle: edge.target.handle,
@@ -1879,11 +1906,15 @@ class NodeExecutor {
1879
1906
  : undefined;
1880
1907
  if (!hadError)
1881
1908
  node.stats.lastError = undefined;
1882
- // Only emit node-done if not cancelled (cancellation events emitted separately)
1909
+ // Track successful completion time (for detecting stale inputs)
1883
1910
  const isCancelled = controller.signal.aborted &&
1884
1911
  (controller.signal.reason === "snapshot" ||
1885
1912
  controller.signal.reason === "node-deleted" ||
1886
1913
  controller.signal.reason === "user-cancelled");
1914
+ if (!hadError && !isCancelled) {
1915
+ node.lastSuccessAt = Date.now();
1916
+ }
1917
+ // Only emit node-done if not cancelled (cancellation events emitted separately)
1887
1918
  if (!isCancelled) {
1888
1919
  this.eventEmitter.emit("stats", {
1889
1920
  kind: "node-done",
@@ -2051,6 +2082,8 @@ class GraphRuntime {
2051
2082
  progress: 0,
2052
2083
  },
2053
2084
  activeRunContextIds: new Set(),
2085
+ lastInputAt: {},
2086
+ lastSuccessAt: undefined,
2054
2087
  };
2055
2088
  gr.graph.setNode(n.nodeId, rn);
2056
2089
  }
@@ -2157,8 +2190,27 @@ class GraphRuntime {
2157
2190
  const node = this.graph.getNode(nodeId);
2158
2191
  if (!node)
2159
2192
  return;
2193
+ // If event is an invalidate event, re-run the node with same inputs
2194
+ if (this.isInvalidateEvent(event)) {
2195
+ // Check if node has all inbound inputs (required for execution)
2196
+ if (this.graph.allInboundHaveValue(nodeId)) {
2197
+ this.execute(nodeId);
2198
+ }
2199
+ return;
2200
+ }
2201
+ // Forward event to node's onExternalEvent handler for custom actions
2160
2202
  node.runtime.onExternalEvent?.(event, node.state);
2161
2203
  }
2204
+ /**
2205
+ * Check if an event is an invalidate event that should trigger re-execution
2206
+ */
2207
+ isInvalidateEvent(event) {
2208
+ if (!event || typeof event !== "object")
2209
+ return false;
2210
+ // Check if event has action === "invalidate"
2211
+ const e = event;
2212
+ return e.action === "invalidate";
2213
+ }
2162
2214
  cancelNodeRuns(nodeIds) {
2163
2215
  this.nodeExecutor.cancelNodeRuns(nodeIds);
2164
2216
  }
@@ -2385,6 +2437,8 @@ class GraphRuntime {
2385
2437
  progress: 0,
2386
2438
  },
2387
2439
  activeRunContextIds: new Set(),
2440
+ lastInputAt: {},
2441
+ lastSuccessAt: undefined,
2388
2442
  };
2389
2443
  this.graph.setNode(n.nodeId, newNode);
2390
2444
  const effectiveInputs = this.nodeExecutor.getEffectiveInputs(newNode.nodeId);
@@ -2531,8 +2585,17 @@ class GraphRuntime {
2531
2585
  const afterTargetSet = afterMap.get(handle) ?? new Set();
2532
2586
  if (!setsEqual(beforeTargetSet, afterTargetSet)) {
2533
2587
  const val = this.getOutput(nodeId, handle);
2534
- if (val !== undefined)
2535
- this.propagate(nodeId, handle, val);
2588
+ if (val !== undefined) {
2589
+ let runContextIdsToUse = undefined;
2590
+ if (this.runMode === "manual") {
2591
+ runContextIdsToUse = new Set([
2592
+ this.runContextManager.createRunContext(nodeId, undefined, {
2593
+ propagate: false,
2594
+ }),
2595
+ ]);
2596
+ }
2597
+ this.propagate(nodeId, handle, val, runContextIdsToUse);
2598
+ }
2536
2599
  }
2537
2600
  }
2538
2601
  }