@bian-womp/spark-graph 0.3.72 → 0.3.74

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
@@ -1684,6 +1684,7 @@ class RunContextManager {
1684
1684
  id,
1685
1685
  startNodes: new Set([startNodeId]),
1686
1686
  cancelledNodes: new Set(),
1687
+ pendingScheduling: 1,
1687
1688
  pendingNodes: 0,
1688
1689
  pendingEdges: 0,
1689
1690
  pendingResolvers: 0,
@@ -1719,6 +1720,33 @@ class RunContextManager {
1719
1720
  hasActiveRunContexts() {
1720
1721
  return this.runContexts.size > 0;
1721
1722
  }
1723
+ /**
1724
+ * Release one scheduling hold from a run-context.
1725
+ * Must be called once by the creator when scheduling decisions are complete.
1726
+ */
1727
+ releaseScheduling(id) {
1728
+ const ctx = this.runContexts.get(id);
1729
+ if (!ctx) {
1730
+ this.logger.debug("release-scheduling-context-not-found", {
1731
+ runContextId: id,
1732
+ });
1733
+ return;
1734
+ }
1735
+ if (ctx.pendingScheduling > 0) {
1736
+ ctx.pendingScheduling--;
1737
+ }
1738
+ else {
1739
+ this.logger.warn("release-scheduling-underflow", {
1740
+ runContextId: id,
1741
+ pendingScheduling: ctx.pendingScheduling,
1742
+ });
1743
+ }
1744
+ this.logger.debug("release-scheduling", {
1745
+ runContextId: id,
1746
+ pendingScheduling: ctx.pendingScheduling,
1747
+ });
1748
+ this.finishRunContextIfPossible(id);
1749
+ }
1722
1750
  /**
1723
1751
  * Increment queued work count for a run-context.
1724
1752
  * Used by asyncConcurrency: "queue" to keep contexts alive while work is queued.
@@ -1870,7 +1898,11 @@ class RunContextManager {
1870
1898
  });
1871
1899
  return;
1872
1900
  }
1873
- if (ctx.pendingNodes > 0 || ctx.pendingEdges > 0 || ctx.pendingResolvers > 0 || ctx.pendingQueued > 0) {
1901
+ if (ctx.pendingScheduling > 0 ||
1902
+ ctx.pendingNodes > 0 ||
1903
+ ctx.pendingEdges > 0 ||
1904
+ ctx.pendingResolvers > 0 ||
1905
+ ctx.pendingQueued > 0) {
1874
1906
  return; // Still has pending work
1875
1907
  }
1876
1908
  this.logger.info("finish-run-context", {
@@ -2523,11 +2555,10 @@ class HandleResolver {
2523
2555
  * EdgePropagator component - handles value propagation through edges
2524
2556
  */
2525
2557
  class EdgePropagator {
2526
- constructor(graph, eventEmitter, runContextManager, handleResolver, nodeExecutor, runtime) {
2558
+ constructor(graph, eventEmitter, runContextManager, nodeExecutor, runtime) {
2527
2559
  this.graph = graph;
2528
2560
  this.eventEmitter = eventEmitter;
2529
2561
  this.runContextManager = runContextManager;
2530
- this.handleResolver = handleResolver;
2531
2562
  this.nodeExecutor = nodeExecutor;
2532
2563
  this.runtime = runtime;
2533
2564
  this.arrayInputBuckets = new Map();
@@ -2716,7 +2747,7 @@ class EdgePropagator {
2716
2747
  // Set input value (respecting skipPropagateValues)
2717
2748
  const shouldSetValue = this.shouldSetInputValue(effectiveRunContexts);
2718
2749
  if (shouldSetValue && valueChanged) {
2719
- this.setTargetInput(edge, processedValue);
2750
+ this.runtime.setTargetInput(edge, processedValue, "applyToTarget");
2720
2751
  }
2721
2752
  else if (shouldSetValue && !valueChanged) {
2722
2753
  // Even if value didn't change, update timestamp if we're forcing execution
@@ -2772,13 +2803,6 @@ class EdgePropagator {
2772
2803
  }
2773
2804
  return true;
2774
2805
  }
2775
- /**
2776
- * Set target input value and emit event
2777
- */
2778
- setTargetInput(edge, value) {
2779
- this.graph.updateNodeInput(edge.target.nodeId, edge.target.handle, value);
2780
- this.handleResolver.scheduleRecomputeHandles(edge.target.nodeId);
2781
- }
2782
2806
  /**
2783
2807
  * Execute downstream if conditions are met
2784
2808
  */
@@ -2988,13 +3012,22 @@ class NodeExecutor {
2988
3012
  execute: (opts) => {
2989
3013
  if (this.graph.allInboundHaveValue(nodeId)) {
2990
3014
  let runContextIdsToUse = this.runtime.getRunMode() === "auto" ? undefined : runContextIds;
3015
+ let runContextIdToRelease;
2991
3016
  if (this.runtime.getRunMode() === "manual" && (!runContextIds || runContextIds.size === 0)) {
2992
- runContextIdsToUse = new Set([this.runContextManager.createRunContext(nodeId, opts)]);
3017
+ runContextIdToRelease = this.runContextManager.createRunContext(nodeId, opts);
3018
+ runContextIdsToUse = new Set([runContextIdToRelease]);
3019
+ }
3020
+ try {
3021
+ this.execute(nodeId, {
3022
+ runContextIds: runContextIdsToUse,
3023
+ reason: opts?.reason ?? "executeFromContext",
3024
+ });
3025
+ }
3026
+ finally {
3027
+ if (runContextIdToRelease) {
3028
+ this.runContextManager.releaseScheduling(runContextIdToRelease);
3029
+ }
2993
3030
  }
2994
- this.execute(nodeId, {
2995
- runContextIds: runContextIdsToUse,
2996
- reason: opts?.reason ?? "executeFromContext",
2997
- });
2998
3031
  }
2999
3032
  },
3000
3033
  getInput: (handle) => inputs[handle],
@@ -3020,6 +3053,7 @@ class NodeExecutor {
3020
3053
  */
3021
3054
  execute(nodeId, opts) {
3022
3055
  let { runContextIds, canSkipHandleResolution, reason = "" } = opts ?? {};
3056
+ let autoCreatedRunContextId;
3023
3057
  const node = this.graph.getNode(nodeId);
3024
3058
  if (!node)
3025
3059
  return;
@@ -3032,81 +3066,89 @@ class NodeExecutor {
3032
3066
  if (runMode === "manual" && (!runContextIds || runContextIds.size === 0)) {
3033
3067
  // If autoRun is true, auto-generate a run context (similar to createExecutionContext pattern)
3034
3068
  if (node.policy?.autoRun === true) {
3035
- runContextIds = new Set([this.runContextManager.createRunContext(nodeId, { propagate: false })]);
3069
+ autoCreatedRunContextId = this.runContextManager.createRunContext(nodeId, { propagate: false });
3070
+ runContextIds = new Set([autoCreatedRunContextId]);
3036
3071
  }
3037
3072
  else {
3038
3073
  console.trace(`NodeExecutor.execute[${formatNodeRef(this.graph, nodeId)}:${reason}]: no runContextIds provided in manual mode, skipping execution`);
3039
3074
  return;
3040
3075
  }
3041
3076
  }
3042
- if (runMode === "auto" && runContextIds && runContextIds.size > 0) {
3043
- console.trace(`NodeExecutor.execute[${formatNodeRef(this.graph, nodeId)}:${reason}]: runContextIds provided in auto mode, ignoring`);
3044
- runContextIds = undefined;
3045
- }
3046
- // Early validation for auto-mode paused state
3047
- if (this.runtime.isPaused())
3048
- return;
3049
- // Check runtime validators (check current state, not just graph definition)
3050
- const runtimeValidationError = this.runtime.hasRuntimeValidationBlock(nodeId);
3051
- if (runtimeValidationError) {
3052
- this.eventEmitter.emit("error", {
3053
- kind: "system",
3054
- message: runtimeValidationError.message,
3055
- code: runtimeValidationError.code || "RUNTIME_VALIDATION_BLOCKED",
3056
- details: {
3057
- nodeId,
3058
- nodeTypeId: node?.typeId,
3059
- ...runtimeValidationError.details,
3060
- },
3061
- });
3062
- return;
3063
- }
3064
- // Attach run-context IDs if provided - do this BEFORE checking for pending resolution
3065
- // so that handle resolution can track these run contexts
3066
- if (runContextIds) {
3067
- this.graph.addNodeRunContextIds(nodeId, runContextIds);
3068
- }
3069
- if (!canSkipHandleResolution && !this.handleResolver.getPendingResolution(nodeId)) {
3070
- this.handleResolver.scheduleRecomputeHandles(nodeId);
3071
- }
3072
- // Check if handles are being resolved - wait for resolution before executing
3073
- // Do this AFTER setting up run contexts so handle resolution can track them
3074
- const pendingResolution = this.handleResolver.getPendingResolution(nodeId);
3075
- if (pendingResolution) {
3076
- if (runContextIds && runContextIds.size > 0) {
3077
- for (const id of runContextIds) {
3078
- this.runContextManager.startHandleResolution(id, nodeId);
3079
- }
3077
+ try {
3078
+ if (runMode === "auto" && runContextIds && runContextIds.size > 0) {
3079
+ console.trace(`NodeExecutor.execute[${formatNodeRef(this.graph, nodeId)}:${reason}]: runContextIds provided in auto mode, ignoring`);
3080
+ runContextIds = undefined;
3080
3081
  }
3081
- // Wait for resolution to complete, then re-execute
3082
- pendingResolution.then(() => {
3083
- // Re-check node still exists and conditions
3084
- const nodeAfter = this.graph.getNode(nodeId);
3085
- if (nodeAfter) {
3086
- this.execute(nodeId, {
3087
- runContextIds,
3088
- canSkipHandleResolution: true,
3089
- reason: opts?.reason,
3090
- });
3091
- }
3082
+ // Early validation for auto-mode paused state
3083
+ if (this.runtime.isPaused())
3084
+ return;
3085
+ // Check runtime validators (check current state, not just graph definition)
3086
+ const runtimeValidationError = this.runtime.hasRuntimeValidationBlock(nodeId);
3087
+ if (runtimeValidationError) {
3088
+ this.eventEmitter.emit("error", {
3089
+ kind: "system",
3090
+ message: runtimeValidationError.message,
3091
+ code: runtimeValidationError.code || "RUNTIME_VALIDATION_BLOCKED",
3092
+ details: {
3093
+ nodeId,
3094
+ nodeTypeId: node?.typeId,
3095
+ ...runtimeValidationError.details,
3096
+ },
3097
+ });
3098
+ return;
3099
+ }
3100
+ // Attach run-context IDs if provided - do this BEFORE checking for pending resolution
3101
+ // so that handle resolution can track these run contexts
3102
+ if (runContextIds) {
3103
+ this.graph.addNodeRunContextIds(nodeId, runContextIds);
3104
+ }
3105
+ if (!canSkipHandleResolution && !this.handleResolver.getPendingResolution(nodeId)) {
3106
+ this.handleResolver.scheduleRecomputeHandles(nodeId);
3107
+ }
3108
+ // Check if handles are being resolved - wait for resolution before executing
3109
+ // Do this AFTER setting up run contexts so handle resolution can track them
3110
+ const pendingResolution = this.handleResolver.getPendingResolution(nodeId);
3111
+ if (pendingResolution) {
3092
3112
  if (runContextIds && runContextIds.size > 0) {
3093
3113
  for (const id of runContextIds) {
3094
- this.runContextManager.finishHandleResolution(id, nodeId);
3114
+ this.runContextManager.startHandleResolution(id, nodeId);
3095
3115
  }
3096
3116
  }
3097
- });
3098
- return;
3117
+ // Wait for resolution to complete, then re-execute
3118
+ pendingResolution.then(() => {
3119
+ // Re-check node still exists and conditions
3120
+ const nodeAfter = this.graph.getNode(nodeId);
3121
+ if (nodeAfter) {
3122
+ this.execute(nodeId, {
3123
+ runContextIds,
3124
+ canSkipHandleResolution: true,
3125
+ reason: opts?.reason,
3126
+ });
3127
+ }
3128
+ if (runContextIds && runContextIds.size > 0) {
3129
+ for (const id of runContextIds) {
3130
+ this.runContextManager.finishHandleResolution(id, nodeId);
3131
+ }
3132
+ }
3133
+ });
3134
+ return;
3135
+ }
3136
+ // Handle debouncing
3137
+ const now = Date.now();
3138
+ if (this.shouldDebounce(nodeId, now)) {
3139
+ this.handleDebouncedSchedule(nodeId, now, runContextIds, reason);
3140
+ return;
3141
+ }
3142
+ // Prepare execution plan
3143
+ const executionPlan = this.prepareExecutionPlan(nodeId, runContextIds, now, reason);
3144
+ // Route to appropriate concurrency handler
3145
+ this.routeToConcurrencyHandler(nodeId, executionPlan);
3099
3146
  }
3100
- // Handle debouncing
3101
- const now = Date.now();
3102
- if (this.shouldDebounce(nodeId, now)) {
3103
- this.handleDebouncedSchedule(nodeId, now, runContextIds, reason);
3104
- return;
3147
+ finally {
3148
+ if (autoCreatedRunContextId) {
3149
+ this.runContextManager.releaseScheduling(autoCreatedRunContextId);
3150
+ }
3105
3151
  }
3106
- // Prepare execution plan
3107
- const executionPlan = this.prepareExecutionPlan(nodeId, runContextIds, now, reason);
3108
- // Route to appropriate concurrency handler
3109
- this.routeToConcurrencyHandler(nodeId, executionPlan);
3110
3152
  }
3111
3153
  /**
3112
3154
  * Check if execution should be debounced
@@ -3622,7 +3664,7 @@ class GraphRuntime {
3622
3664
  this.graph = new Graph(this.eventEmitter);
3623
3665
  this.runContextManager = new RunContextManager(this.graph, "warn");
3624
3666
  this.handleResolver = new HandleResolver(this.graph, this.eventEmitter, this.runContextManager, this);
3625
- this.edgePropagator = new EdgePropagator(this.graph, this.eventEmitter, this.runContextManager, this.handleResolver, this, this);
3667
+ this.edgePropagator = new EdgePropagator(this.graph, this.eventEmitter, this.runContextManager, this, this);
3626
3668
  // Create NodeExecutor with EdgePropagator and HandleResolver
3627
3669
  this.nodeExecutor = new NodeExecutor(this.graph, this.eventEmitter, this.runContextManager, this.handleResolver, this, this);
3628
3670
  // Create RuntimeValidatorManager
@@ -3711,6 +3753,40 @@ class GraphRuntime {
3711
3753
  on(event, handler) {
3712
3754
  return this.eventEmitter.on(event, handler);
3713
3755
  }
3756
+ /**
3757
+ * Check if an event is an invalidate event that should trigger re-execution
3758
+ */
3759
+ isInvalidateEvent(event) {
3760
+ if (!event || typeof event !== "object")
3761
+ return false;
3762
+ // Check if event has action === "invalidate"
3763
+ const e = event;
3764
+ return e.action === "invalidate";
3765
+ }
3766
+ executeNodeAutoRun(nodeId, opts) {
3767
+ const node = this.graph.getNode(nodeId);
3768
+ const shouldAutoRun = this.runMode === "auto" || node?.policy?.autoRun === true;
3769
+ const canExecute = this.graph.allInboundHaveValue(nodeId);
3770
+ if (!shouldAutoRun || !canExecute)
3771
+ return;
3772
+ let runContextIdsToUse = undefined;
3773
+ let runContextIdToRelease;
3774
+ if (this.runMode === "manual") {
3775
+ runContextIdToRelease = this.runContextManager.createRunContext(nodeId, { propagate: false });
3776
+ runContextIdsToUse = new Set([runContextIdToRelease]);
3777
+ }
3778
+ try {
3779
+ this.execute(nodeId, {
3780
+ runContextIds: runContextIdsToUse,
3781
+ reason: opts?.reason ?? "executeNodeAutoRun",
3782
+ });
3783
+ }
3784
+ finally {
3785
+ if (runContextIdToRelease) {
3786
+ this.runContextManager.releaseScheduling(runContextIdToRelease);
3787
+ }
3788
+ }
3789
+ }
3714
3790
  setInputs(nodeId, inputs) {
3715
3791
  const node = this.graph.getNode(nodeId);
3716
3792
  if (!node)
@@ -3766,8 +3842,6 @@ class GraphRuntime {
3766
3842
  const same = valuesEqual(prev, value);
3767
3843
  if (!same) {
3768
3844
  this.graph.updateNodeInput(nodeId, handle, value);
3769
- // Emit value event for input updates
3770
- this.eventEmitter.emit("value", { nodeId, handle, value, io: "input" });
3771
3845
  anyChanged = true;
3772
3846
  }
3773
3847
  }
@@ -3796,8 +3870,9 @@ class GraphRuntime {
3796
3870
  });
3797
3871
  if (this.runMode === "auto" && invalidate) {
3798
3872
  for (const nodeId of this.graph.getNodeIds()) {
3799
- if (this.graph.allInboundHaveValue(nodeId))
3873
+ if (this.graph.allInboundHaveValue(nodeId)) {
3800
3874
  this.execute(nodeId, { reason: "launch" });
3875
+ }
3801
3876
  }
3802
3877
  }
3803
3878
  if (startPaused) {
@@ -3819,16 +3894,6 @@ class GraphRuntime {
3819
3894
  // Forward event to node's onExternalEvent handler for custom actions
3820
3895
  node.runtime.onExternalEvent?.(event, node.state);
3821
3896
  }
3822
- /**
3823
- * Check if an event is an invalidate event that should trigger re-execution
3824
- */
3825
- isInvalidateEvent(event) {
3826
- if (!event || typeof event !== "object")
3827
- return false;
3828
- // Check if event has action === "invalidate"
3829
- const e = event;
3830
- return e.action === "invalidate";
3831
- }
3832
3897
  cancelNodeRuns(nodeIds) {
3833
3898
  this.nodeExecutor.cancelNodeRuns(nodeIds);
3834
3899
  }
@@ -3856,6 +3921,7 @@ class GraphRuntime {
3856
3921
  this.nodeExecutor.setEnvironment(this.environment);
3857
3922
  for (const nodeId of this.graph.getNodeIds()) {
3858
3923
  this.handleResolver.scheduleRecomputeHandles(nodeId);
3924
+ this.executeNodeAutoRun(nodeId, { reason: "setEnvironment" });
3859
3925
  }
3860
3926
  }
3861
3927
  setCustomNodeData(customNodeData) {
@@ -3953,10 +4019,15 @@ class GraphRuntime {
3953
4019
  ...opts,
3954
4020
  });
3955
4021
  this.graph.addNodeRunContextId(startNodeId, id);
3956
- this.execute(startNodeId, {
3957
- runContextIds: new Set([id]),
3958
- reason: opts?.reason ?? "runFromHereContext",
3959
- });
4022
+ try {
4023
+ this.execute(startNodeId, {
4024
+ runContextIds: new Set([id]),
4025
+ reason: opts?.reason ?? "runFromHereContext",
4026
+ });
4027
+ }
4028
+ finally {
4029
+ this.runContextManager.releaseScheduling(id);
4030
+ }
3960
4031
  });
3961
4032
  }
3962
4033
  setRunMode(runMode) {
@@ -4003,19 +4074,10 @@ class GraphRuntime {
4003
4074
  }
4004
4075
  }
4005
4076
  }
4006
- executeNodeAutoRun(nodeId, opts) {
4007
- const node = this.graph.getNode(nodeId);
4008
- const shouldAutoRun = this.runMode === "auto" || node?.policy?.autoRun === true;
4009
- let runContextIdsToUse = undefined;
4010
- if (this.runMode === "manual") {
4011
- runContextIdsToUse = new Set([this.runContextManager.createRunContext(nodeId, { propagate: false })]);
4012
- }
4013
- if (shouldAutoRun && this.graph.allInboundHaveValue(nodeId)) {
4014
- this.execute(nodeId, {
4015
- runContextIds: runContextIdsToUse,
4016
- reason: opts?.reason ?? "executeNodeAutoRun",
4017
- });
4018
- }
4077
+ setTargetInput(edge, value, reason) {
4078
+ this.graph.updateNodeInput(edge.target.nodeId, edge.target.handle, value);
4079
+ this.handleResolver.scheduleRecomputeHandles(edge.target.nodeId);
4080
+ this.executeNodeAutoRun(edge.target.nodeId, { reason });
4019
4081
  }
4020
4082
  copyOutputs(fromNodeId, toNodeId, options) {
4021
4083
  const fromNode = this.getNodeData(fromNodeId);
@@ -4286,14 +4348,21 @@ class GraphRuntime {
4286
4348
  const val = this.getOutput(nodeId, handle);
4287
4349
  if (val !== undefined) {
4288
4350
  let runContextIdsToUse = undefined;
4351
+ let runContextIdToRelease;
4289
4352
  if (this.runMode === "manual") {
4290
- runContextIdsToUse = new Set([
4291
- this.runContextManager.createRunContext(nodeId, {
4292
- propagate: false,
4293
- }),
4294
- ]);
4353
+ runContextIdToRelease = this.runContextManager.createRunContext(nodeId, {
4354
+ propagate: false,
4355
+ });
4356
+ runContextIdsToUse = new Set([runContextIdToRelease]);
4357
+ }
4358
+ try {
4359
+ this.propagate(nodeId, handle, val, runContextIdsToUse);
4360
+ }
4361
+ finally {
4362
+ if (runContextIdToRelease) {
4363
+ this.runContextManager.releaseScheduling(runContextIdToRelease);
4364
+ }
4295
4365
  }
4296
- this.propagate(nodeId, handle, val, runContextIdsToUse);
4297
4366
  }
4298
4367
  }
4299
4368
  }