@bian-womp/spark-graph 0.3.36 → 0.3.37

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
@@ -1148,6 +1148,7 @@ class RunContextManager {
1148
1148
  pendingNodes: 0,
1149
1149
  pendingEdges: 0,
1150
1150
  pendingResolvers: 0,
1151
+ pendingQueued: 0,
1151
1152
  skipPropagateValues: options?.skipPropagateValues ?? false,
1152
1153
  propagate: options?.propagate ?? true,
1153
1154
  resolve,
@@ -1179,6 +1180,47 @@ class RunContextManager {
1179
1180
  hasActiveRunContexts() {
1180
1181
  return this.runContexts.size > 0;
1181
1182
  }
1183
+ /**
1184
+ * Increment queued work count for a run-context.
1185
+ * Used by asyncConcurrency: "queue" to keep contexts alive while work is queued.
1186
+ */
1187
+ incrementQueued(id, nodeId) {
1188
+ const ctx = this.runContexts.get(id);
1189
+ if (!ctx) {
1190
+ this.logger.debug("increment-queued-context-not-found", {
1191
+ runContextId: id,
1192
+ nodeId,
1193
+ });
1194
+ return;
1195
+ }
1196
+ ctx.pendingQueued++;
1197
+ this.logger.debug("increment-queued", {
1198
+ runContextId: id,
1199
+ nodeId,
1200
+ pendingQueued: ctx.pendingQueued,
1201
+ });
1202
+ }
1203
+ /**
1204
+ * Decrement queued work count for a run-context.
1205
+ * Called when queued work is either started or dropped.
1206
+ */
1207
+ decrementQueued(id, nodeId) {
1208
+ const ctx = this.runContexts.get(id);
1209
+ if (!ctx) {
1210
+ this.logger.debug("decrement-queued-context-not-found", {
1211
+ runContextId: id,
1212
+ nodeId,
1213
+ });
1214
+ return;
1215
+ }
1216
+ ctx.pendingQueued--;
1217
+ this.logger.debug("decrement-queued", {
1218
+ runContextId: id,
1219
+ nodeId,
1220
+ pendingQueued: ctx.pendingQueued,
1221
+ });
1222
+ this.finishRunContextIfPossible(id);
1223
+ }
1182
1224
  startNodeRun(id, nodeId) {
1183
1225
  const ctx = this.runContexts.get(id);
1184
1226
  if (!ctx) {
@@ -1291,7 +1333,8 @@ class RunContextManager {
1291
1333
  }
1292
1334
  if (ctx.pendingNodes > 0 ||
1293
1335
  ctx.pendingEdges > 0 ||
1294
- ctx.pendingResolvers > 0) {
1336
+ ctx.pendingResolvers > 0 ||
1337
+ ctx.pendingQueued > 0) {
1295
1338
  return; // Still has pending work
1296
1339
  }
1297
1340
  this.logger.info("finish-run-context", {
@@ -2459,8 +2502,8 @@ class NodeExecutor {
2459
2502
  }
2460
2503
  // Handle debouncing
2461
2504
  const now = Date.now();
2462
- if (this.shouldDebounce(nodeId, node, now)) {
2463
- this.handleDebouncedSchedule(node, nodeId, now);
2505
+ if (this.shouldDebounce(node, now)) {
2506
+ this.handleDebouncedSchedule(node, nodeId, now, runContextIds);
2464
2507
  return;
2465
2508
  }
2466
2509
  // Prepare execution plan
@@ -2471,7 +2514,7 @@ class NodeExecutor {
2471
2514
  /**
2472
2515
  * Check if execution should be debounced
2473
2516
  */
2474
- shouldDebounce(nodeId, node, now) {
2517
+ shouldDebounce(node, now) {
2475
2518
  const policy = node.policy ?? {};
2476
2519
  const lastScheduledAt = node.lastScheduledAt;
2477
2520
  return !!(policy.debounceMs &&
@@ -2481,13 +2524,23 @@ class NodeExecutor {
2481
2524
  /**
2482
2525
  * Handle debounced scheduling by replacing the latest queued item
2483
2526
  */
2484
- handleDebouncedSchedule(node, nodeId, now) {
2527
+ handleDebouncedSchedule(node, nodeId, now, runContextIds) {
2528
+ // Decrement pendingQueued for any existing queued items before replacing
2529
+ if (node.queue.length > 0) {
2530
+ this.decrementQueuedForPlans(node.queue, nodeId);
2531
+ }
2485
2532
  const effectiveInputs = this.getEffectiveInputs(nodeId);
2486
2533
  const runSeq = this.graph.incrementNodeRunSeq(nodeId);
2487
- const rid = `${nodeId}:${runSeq}:${now}`;
2488
- this.graph.replaceNodeQueue(nodeId, [
2489
- { runId: rid, inputs: effectiveInputs },
2490
- ]);
2534
+ const runId = `${nodeId}:${runSeq}:${now}`;
2535
+ const policySnapshot = node.policy ? { ...node.policy } : undefined;
2536
+ const plan = {
2537
+ runId,
2538
+ effectiveInputs,
2539
+ runContextIdsForRun: runContextIds,
2540
+ timestamp: now,
2541
+ policy: policySnapshot,
2542
+ };
2543
+ this.graph.replaceNodeQueue(nodeId, [plan]);
2491
2544
  }
2492
2545
  /**
2493
2546
  * Prepare execution plan with all necessary information
@@ -2546,12 +2599,19 @@ class NodeExecutor {
2546
2599
  const currentNode = this.graph.getNode(nodeId);
2547
2600
  if (!currentNode)
2548
2601
  return;
2549
- this.graph.addToNodeQueue(nodeId, {
2550
- runId: plan.runId,
2551
- inputs: plan.effectiveInputs,
2552
- });
2602
+ // Keep the originating run-context alive while work is queued by
2603
+ // incrementing queued counters for the plan's run-contexts.
2604
+ if (plan.runContextIdsForRun) {
2605
+ for (const rcId of plan.runContextIdsForRun) {
2606
+ this.runContextManager.incrementQueued(rcId, nodeId);
2607
+ }
2608
+ }
2609
+ this.graph.addToNodeQueue(nodeId, plan);
2553
2610
  if (currentNode.queue.length > maxQ) {
2554
- this.graph.shiftNodeQueue(nodeId);
2611
+ const dropped = this.graph.shiftNodeQueue(nodeId);
2612
+ if (dropped) {
2613
+ this.decrementQueuedForPlans(dropped, nodeId);
2614
+ }
2555
2615
  }
2556
2616
  this.processQueue(node, nodeId);
2557
2617
  }
@@ -2569,18 +2629,12 @@ class NodeExecutor {
2569
2629
  if (!next)
2570
2630
  return;
2571
2631
  this.graph.setNodeLatestRunId(nodeId, next.runId);
2572
- const policySnapshot = node.policy ? { ...node.policy } : undefined;
2573
- const activeRunContextIds = this.graph.getNodeRunContextIds(nodeId);
2574
- const plan = {
2575
- runId: next.runId,
2576
- effectiveInputs: next.inputs,
2577
- runContextIdsForRun: activeRunContextIds.size > 0 ? activeRunContextIds : undefined,
2578
- timestamp: Date.now(),
2579
- policy: policySnapshot,
2580
- };
2581
- this.startRun(node, nodeId, plan, () => {
2632
+ // Start the run first (which increments pendingNodes), then decrement
2633
+ // pendingQueued to ensure the run context stays alive.
2634
+ this.startRun(node, nodeId, next, () => {
2582
2635
  setTimeout(processNext, 0);
2583
2636
  });
2637
+ this.decrementQueuedForPlans(next, nodeId);
2584
2638
  };
2585
2639
  processNext();
2586
2640
  }
@@ -2611,6 +2665,20 @@ class NodeExecutor {
2611
2665
  }
2612
2666
  }
2613
2667
  }
2668
+ /**
2669
+ * Decrement pendingQueued counters for plans' run-contexts.
2670
+ * Used when queued work is being started, dropped, or cancelled.
2671
+ */
2672
+ decrementQueuedForPlans(plans, nodeId) {
2673
+ const plansArray = Array.isArray(plans) ? plans : [plans];
2674
+ for (const plan of plansArray) {
2675
+ if (plan.runContextIdsForRun) {
2676
+ for (const rcId of plan.runContextIdsForRun) {
2677
+ this.runContextManager.decrementQueued(rcId, nodeId);
2678
+ }
2679
+ }
2680
+ }
2681
+ }
2614
2682
  /**
2615
2683
  * Create execution controller and update node stats
2616
2684
  */
@@ -2839,6 +2907,10 @@ class NodeExecutor {
2839
2907
  }
2840
2908
  this.graph.clearNodeControllers(nodeId);
2841
2909
  this.graph.updateNodeStats(nodeId, { active: 0 });
2910
+ // Decrement pendingQueued for any queued items before clearing the queue
2911
+ if (node.queue.length > 0) {
2912
+ this.decrementQueuedForPlans(node.queue, nodeId);
2913
+ }
2842
2914
  this.graph.clearNodeQueue(nodeId);
2843
2915
  }
2844
2916
  /**