@bian-womp/spark-graph 0.3.73 → 0.3.75

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", {
@@ -2789,7 +2821,10 @@ class EdgePropagator {
2789
2821
  */
2790
2822
  shouldPropagateExecution(effectiveRunContexts) {
2791
2823
  if (!effectiveRunContexts) {
2792
- return true; // Auto mode always propagates
2824
+ // Without run-context IDs, only auto mode should schedule downstream execution.
2825
+ // In manual mode this path is used for value refresh/re-emits (e.g. handle updates),
2826
+ // and should not implicitly trigger node runs.
2827
+ return this.runtime.getRunMode() === "auto";
2793
2828
  }
2794
2829
  // Check propagate flag (only in run-context mode)
2795
2830
  for (const id of effectiveRunContexts) {
@@ -2980,13 +3015,22 @@ class NodeExecutor {
2980
3015
  execute: (opts) => {
2981
3016
  if (this.graph.allInboundHaveValue(nodeId)) {
2982
3017
  let runContextIdsToUse = this.runtime.getRunMode() === "auto" ? undefined : runContextIds;
3018
+ let runContextIdToRelease;
2983
3019
  if (this.runtime.getRunMode() === "manual" && (!runContextIds || runContextIds.size === 0)) {
2984
- runContextIdsToUse = new Set([this.runContextManager.createRunContext(nodeId, opts)]);
3020
+ runContextIdToRelease = this.runContextManager.createRunContext(nodeId, opts);
3021
+ runContextIdsToUse = new Set([runContextIdToRelease]);
3022
+ }
3023
+ try {
3024
+ this.execute(nodeId, {
3025
+ runContextIds: runContextIdsToUse,
3026
+ reason: opts?.reason ?? "executeFromContext",
3027
+ });
3028
+ }
3029
+ finally {
3030
+ if (runContextIdToRelease) {
3031
+ this.runContextManager.releaseScheduling(runContextIdToRelease);
3032
+ }
2985
3033
  }
2986
- this.execute(nodeId, {
2987
- runContextIds: runContextIdsToUse,
2988
- reason: opts?.reason ?? "executeFromContext",
2989
- });
2990
3034
  }
2991
3035
  },
2992
3036
  getInput: (handle) => inputs[handle],
@@ -3012,6 +3056,7 @@ class NodeExecutor {
3012
3056
  */
3013
3057
  execute(nodeId, opts) {
3014
3058
  let { runContextIds, canSkipHandleResolution, reason = "" } = opts ?? {};
3059
+ let autoCreatedRunContextId;
3015
3060
  const node = this.graph.getNode(nodeId);
3016
3061
  if (!node)
3017
3062
  return;
@@ -3024,81 +3069,89 @@ class NodeExecutor {
3024
3069
  if (runMode === "manual" && (!runContextIds || runContextIds.size === 0)) {
3025
3070
  // If autoRun is true, auto-generate a run context (similar to createExecutionContext pattern)
3026
3071
  if (node.policy?.autoRun === true) {
3027
- runContextIds = new Set([this.runContextManager.createRunContext(nodeId, { propagate: false })]);
3072
+ autoCreatedRunContextId = this.runContextManager.createRunContext(nodeId, { propagate: false });
3073
+ runContextIds = new Set([autoCreatedRunContextId]);
3028
3074
  }
3029
3075
  else {
3030
3076
  console.trace(`NodeExecutor.execute[${formatNodeRef(this.graph, nodeId)}:${reason}]: no runContextIds provided in manual mode, skipping execution`);
3031
3077
  return;
3032
3078
  }
3033
3079
  }
3034
- if (runMode === "auto" && runContextIds && runContextIds.size > 0) {
3035
- console.trace(`NodeExecutor.execute[${formatNodeRef(this.graph, nodeId)}:${reason}]: runContextIds provided in auto mode, ignoring`);
3036
- runContextIds = undefined;
3037
- }
3038
- // Early validation for auto-mode paused state
3039
- if (this.runtime.isPaused())
3040
- return;
3041
- // Check runtime validators (check current state, not just graph definition)
3042
- const runtimeValidationError = this.runtime.hasRuntimeValidationBlock(nodeId);
3043
- if (runtimeValidationError) {
3044
- this.eventEmitter.emit("error", {
3045
- kind: "system",
3046
- message: runtimeValidationError.message,
3047
- code: runtimeValidationError.code || "RUNTIME_VALIDATION_BLOCKED",
3048
- details: {
3049
- nodeId,
3050
- nodeTypeId: node?.typeId,
3051
- ...runtimeValidationError.details,
3052
- },
3053
- });
3054
- return;
3055
- }
3056
- // Attach run-context IDs if provided - do this BEFORE checking for pending resolution
3057
- // so that handle resolution can track these run contexts
3058
- if (runContextIds) {
3059
- this.graph.addNodeRunContextIds(nodeId, runContextIds);
3060
- }
3061
- if (!canSkipHandleResolution && !this.handleResolver.getPendingResolution(nodeId)) {
3062
- this.handleResolver.scheduleRecomputeHandles(nodeId);
3063
- }
3064
- // Check if handles are being resolved - wait for resolution before executing
3065
- // Do this AFTER setting up run contexts so handle resolution can track them
3066
- const pendingResolution = this.handleResolver.getPendingResolution(nodeId);
3067
- if (pendingResolution) {
3068
- if (runContextIds && runContextIds.size > 0) {
3069
- for (const id of runContextIds) {
3070
- this.runContextManager.startHandleResolution(id, nodeId);
3071
- }
3080
+ try {
3081
+ if (runMode === "auto" && runContextIds && runContextIds.size > 0) {
3082
+ console.trace(`NodeExecutor.execute[${formatNodeRef(this.graph, nodeId)}:${reason}]: runContextIds provided in auto mode, ignoring`);
3083
+ runContextIds = undefined;
3072
3084
  }
3073
- // Wait for resolution to complete, then re-execute
3074
- pendingResolution.then(() => {
3075
- // Re-check node still exists and conditions
3076
- const nodeAfter = this.graph.getNode(nodeId);
3077
- if (nodeAfter) {
3078
- this.execute(nodeId, {
3079
- runContextIds,
3080
- canSkipHandleResolution: true,
3081
- reason: opts?.reason,
3082
- });
3083
- }
3085
+ // Early validation for auto-mode paused state
3086
+ if (this.runtime.isPaused())
3087
+ return;
3088
+ // Check runtime validators (check current state, not just graph definition)
3089
+ const runtimeValidationError = this.runtime.hasRuntimeValidationBlock(nodeId);
3090
+ if (runtimeValidationError) {
3091
+ this.eventEmitter.emit("error", {
3092
+ kind: "system",
3093
+ message: runtimeValidationError.message,
3094
+ code: runtimeValidationError.code || "RUNTIME_VALIDATION_BLOCKED",
3095
+ details: {
3096
+ nodeId,
3097
+ nodeTypeId: node?.typeId,
3098
+ ...runtimeValidationError.details,
3099
+ },
3100
+ });
3101
+ return;
3102
+ }
3103
+ // Attach run-context IDs if provided - do this BEFORE checking for pending resolution
3104
+ // so that handle resolution can track these run contexts
3105
+ if (runContextIds) {
3106
+ this.graph.addNodeRunContextIds(nodeId, runContextIds);
3107
+ }
3108
+ if (!canSkipHandleResolution && !this.handleResolver.getPendingResolution(nodeId)) {
3109
+ this.handleResolver.scheduleRecomputeHandles(nodeId);
3110
+ }
3111
+ // Check if handles are being resolved - wait for resolution before executing
3112
+ // Do this AFTER setting up run contexts so handle resolution can track them
3113
+ const pendingResolution = this.handleResolver.getPendingResolution(nodeId);
3114
+ if (pendingResolution) {
3084
3115
  if (runContextIds && runContextIds.size > 0) {
3085
3116
  for (const id of runContextIds) {
3086
- this.runContextManager.finishHandleResolution(id, nodeId);
3117
+ this.runContextManager.startHandleResolution(id, nodeId);
3087
3118
  }
3088
3119
  }
3089
- });
3090
- return;
3120
+ // Wait for resolution to complete, then re-execute
3121
+ pendingResolution.then(() => {
3122
+ // Re-check node still exists and conditions
3123
+ const nodeAfter = this.graph.getNode(nodeId);
3124
+ if (nodeAfter) {
3125
+ this.execute(nodeId, {
3126
+ runContextIds,
3127
+ canSkipHandleResolution: true,
3128
+ reason: opts?.reason,
3129
+ });
3130
+ }
3131
+ if (runContextIds && runContextIds.size > 0) {
3132
+ for (const id of runContextIds) {
3133
+ this.runContextManager.finishHandleResolution(id, nodeId);
3134
+ }
3135
+ }
3136
+ });
3137
+ return;
3138
+ }
3139
+ // Handle debouncing
3140
+ const now = Date.now();
3141
+ if (this.shouldDebounce(nodeId, now)) {
3142
+ this.handleDebouncedSchedule(nodeId, now, runContextIds, reason);
3143
+ return;
3144
+ }
3145
+ // Prepare execution plan
3146
+ const executionPlan = this.prepareExecutionPlan(nodeId, runContextIds, now, reason);
3147
+ // Route to appropriate concurrency handler
3148
+ this.routeToConcurrencyHandler(nodeId, executionPlan);
3091
3149
  }
3092
- // Handle debouncing
3093
- const now = Date.now();
3094
- if (this.shouldDebounce(nodeId, now)) {
3095
- this.handleDebouncedSchedule(nodeId, now, runContextIds, reason);
3096
- return;
3150
+ finally {
3151
+ if (autoCreatedRunContextId) {
3152
+ this.runContextManager.releaseScheduling(autoCreatedRunContextId);
3153
+ }
3097
3154
  }
3098
- // Prepare execution plan
3099
- const executionPlan = this.prepareExecutionPlan(nodeId, runContextIds, now, reason);
3100
- // Route to appropriate concurrency handler
3101
- this.routeToConcurrencyHandler(nodeId, executionPlan);
3102
3155
  }
3103
3156
  /**
3104
3157
  * Check if execution should be debounced
@@ -3716,16 +3769,26 @@ class GraphRuntime {
3716
3769
  executeNodeAutoRun(nodeId, opts) {
3717
3770
  const node = this.graph.getNode(nodeId);
3718
3771
  const shouldAutoRun = this.runMode === "auto" || node?.policy?.autoRun === true;
3772
+ const canExecute = this.graph.allInboundHaveValue(nodeId);
3773
+ if (!shouldAutoRun || !canExecute)
3774
+ return;
3719
3775
  let runContextIdsToUse = undefined;
3776
+ let runContextIdToRelease;
3720
3777
  if (this.runMode === "manual") {
3721
- runContextIdsToUse = new Set([this.runContextManager.createRunContext(nodeId, { propagate: false })]);
3778
+ runContextIdToRelease = this.runContextManager.createRunContext(nodeId, { propagate: false });
3779
+ runContextIdsToUse = new Set([runContextIdToRelease]);
3722
3780
  }
3723
- if (shouldAutoRun && this.graph.allInboundHaveValue(nodeId)) {
3781
+ try {
3724
3782
  this.execute(nodeId, {
3725
3783
  runContextIds: runContextIdsToUse,
3726
3784
  reason: opts?.reason ?? "executeNodeAutoRun",
3727
3785
  });
3728
3786
  }
3787
+ finally {
3788
+ if (runContextIdToRelease) {
3789
+ this.runContextManager.releaseScheduling(runContextIdToRelease);
3790
+ }
3791
+ }
3729
3792
  }
3730
3793
  setInputs(nodeId, inputs) {
3731
3794
  const node = this.graph.getNode(nodeId);
@@ -3810,8 +3873,9 @@ class GraphRuntime {
3810
3873
  });
3811
3874
  if (this.runMode === "auto" && invalidate) {
3812
3875
  for (const nodeId of this.graph.getNodeIds()) {
3813
- if (this.graph.allInboundHaveValue(nodeId))
3876
+ if (this.graph.allInboundHaveValue(nodeId)) {
3814
3877
  this.execute(nodeId, { reason: "launch" });
3878
+ }
3815
3879
  }
3816
3880
  }
3817
3881
  if (startPaused) {
@@ -3958,10 +4022,15 @@ class GraphRuntime {
3958
4022
  ...opts,
3959
4023
  });
3960
4024
  this.graph.addNodeRunContextId(startNodeId, id);
3961
- this.execute(startNodeId, {
3962
- runContextIds: new Set([id]),
3963
- reason: opts?.reason ?? "runFromHereContext",
3964
- });
4025
+ try {
4026
+ this.execute(startNodeId, {
4027
+ runContextIds: new Set([id]),
4028
+ reason: opts?.reason ?? "runFromHereContext",
4029
+ });
4030
+ }
4031
+ finally {
4032
+ this.runContextManager.releaseScheduling(id);
4033
+ }
3965
4034
  });
3966
4035
  }
3967
4036
  setRunMode(runMode) {
@@ -4084,6 +4153,7 @@ class GraphRuntime {
4084
4153
  node.lifecycle?.dispose?.({
4085
4154
  state: node.state,
4086
4155
  setState: (next) => this.graph.updateNodeState(node.nodeId, next),
4156
+ disposeReason: "node-removed",
4087
4157
  });
4088
4158
  this.graph.deleteNode(nodeId);
4089
4159
  this.edgePropagator.clearArrayBuckets(nodeId);
@@ -4282,14 +4352,21 @@ class GraphRuntime {
4282
4352
  const val = this.getOutput(nodeId, handle);
4283
4353
  if (val !== undefined) {
4284
4354
  let runContextIdsToUse = undefined;
4355
+ let runContextIdToRelease;
4285
4356
  if (this.runMode === "manual") {
4286
- runContextIdsToUse = new Set([
4287
- this.runContextManager.createRunContext(nodeId, {
4288
- propagate: false,
4289
- }),
4290
- ]);
4357
+ runContextIdToRelease = this.runContextManager.createRunContext(nodeId, {
4358
+ propagate: false,
4359
+ });
4360
+ runContextIdsToUse = new Set([runContextIdToRelease]);
4361
+ }
4362
+ try {
4363
+ this.propagate(nodeId, handle, val, runContextIdsToUse);
4364
+ }
4365
+ finally {
4366
+ if (runContextIdToRelease) {
4367
+ this.runContextManager.releaseScheduling(runContextIdToRelease);
4368
+ }
4291
4369
  }
4292
- this.propagate(nodeId, handle, val, runContextIdsToUse);
4293
4370
  }
4294
4371
  }
4295
4372
  }
@@ -4297,7 +4374,7 @@ class GraphRuntime {
4297
4374
  }
4298
4375
  }
4299
4376
  }
4300
- dispose() {
4377
+ dispose(reason = "runtime-dispose") {
4301
4378
  this.runContextManager.resolveAll();
4302
4379
  this.graph.forEachNode((node) => {
4303
4380
  node.runtime.onDeactivated?.();
@@ -4305,6 +4382,7 @@ class GraphRuntime {
4305
4382
  node.lifecycle?.dispose?.({
4306
4383
  state: node.state,
4307
4384
  setState: (next) => this.graph.updateNodeState(node.nodeId, next),
4385
+ disposeReason: reason,
4308
4386
  });
4309
4387
  });
4310
4388
  this.graph.clear();