@bian-womp/spark-graph 0.3.1 → 0.3.3

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.
Files changed (79) hide show
  1. package/lib/cjs/index.cjs +1417 -1220
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/core/types.d.ts +1 -1
  4. package/lib/cjs/src/core/types.d.ts.map +1 -1
  5. package/lib/cjs/src/index.d.ts +3 -3
  6. package/lib/cjs/src/index.d.ts.map +1 -1
  7. package/lib/cjs/src/misc/utils/json.d.ts +9 -0
  8. package/lib/cjs/src/misc/utils/json.d.ts.map +1 -1
  9. package/lib/cjs/src/runtime/GraphLifecycleApi.d.ts +4 -1
  10. package/lib/cjs/src/runtime/GraphLifecycleApi.d.ts.map +1 -1
  11. package/lib/cjs/src/runtime/GraphRuntime.d.ts +18 -27
  12. package/lib/cjs/src/runtime/GraphRuntime.d.ts.map +1 -1
  13. package/lib/cjs/src/runtime/{UnifiedEngine.d.ts → LocalEngine.d.ts} +20 -7
  14. package/lib/cjs/src/runtime/LocalEngine.d.ts.map +1 -0
  15. package/lib/cjs/src/runtime/components/EdgePropagator.d.ts +101 -0
  16. package/lib/cjs/src/runtime/components/EdgePropagator.d.ts.map +1 -0
  17. package/lib/cjs/src/runtime/components/Graph.d.ts +31 -0
  18. package/lib/cjs/src/runtime/components/Graph.d.ts.map +1 -0
  19. package/lib/cjs/src/runtime/components/HandleResolver.d.ts +11 -8
  20. package/lib/cjs/src/runtime/components/HandleResolver.d.ts.map +1 -1
  21. package/lib/cjs/src/runtime/components/NodeExecutor.d.ts +108 -0
  22. package/lib/cjs/src/runtime/components/NodeExecutor.d.ts.map +1 -0
  23. package/lib/cjs/src/runtime/components/RunContextManager.d.ts +26 -13
  24. package/lib/cjs/src/runtime/components/RunContextManager.d.ts.map +1 -1
  25. package/lib/cjs/src/runtime/components/graph-utils.d.ts +22 -0
  26. package/lib/cjs/src/runtime/components/graph-utils.d.ts.map +1 -0
  27. package/lib/cjs/src/runtime/components/interfaces.d.ts +9 -13
  28. package/lib/cjs/src/runtime/components/interfaces.d.ts.map +1 -1
  29. package/lib/cjs/src/runtime/components/types.d.ts +1 -10
  30. package/lib/cjs/src/runtime/components/types.d.ts.map +1 -1
  31. package/lib/esm/index.js +1416 -1220
  32. package/lib/esm/index.js.map +1 -1
  33. package/lib/esm/src/core/types.d.ts +1 -1
  34. package/lib/esm/src/core/types.d.ts.map +1 -1
  35. package/lib/esm/src/index.d.ts +3 -3
  36. package/lib/esm/src/index.d.ts.map +1 -1
  37. package/lib/esm/src/misc/utils/json.d.ts +9 -0
  38. package/lib/esm/src/misc/utils/json.d.ts.map +1 -1
  39. package/lib/esm/src/runtime/GraphLifecycleApi.d.ts +4 -1
  40. package/lib/esm/src/runtime/GraphLifecycleApi.d.ts.map +1 -1
  41. package/lib/esm/src/runtime/GraphRuntime.d.ts +18 -27
  42. package/lib/esm/src/runtime/GraphRuntime.d.ts.map +1 -1
  43. package/lib/esm/src/runtime/{UnifiedEngine.d.ts → LocalEngine.d.ts} +20 -7
  44. package/lib/esm/src/runtime/LocalEngine.d.ts.map +1 -0
  45. package/lib/esm/src/runtime/components/EdgePropagator.d.ts +101 -0
  46. package/lib/esm/src/runtime/components/EdgePropagator.d.ts.map +1 -0
  47. package/lib/esm/src/runtime/components/Graph.d.ts +31 -0
  48. package/lib/esm/src/runtime/components/Graph.d.ts.map +1 -0
  49. package/lib/esm/src/runtime/components/HandleResolver.d.ts +11 -8
  50. package/lib/esm/src/runtime/components/HandleResolver.d.ts.map +1 -1
  51. package/lib/esm/src/runtime/components/NodeExecutor.d.ts +108 -0
  52. package/lib/esm/src/runtime/components/NodeExecutor.d.ts.map +1 -0
  53. package/lib/esm/src/runtime/components/RunContextManager.d.ts +26 -13
  54. package/lib/esm/src/runtime/components/RunContextManager.d.ts.map +1 -1
  55. package/lib/esm/src/runtime/components/graph-utils.d.ts +22 -0
  56. package/lib/esm/src/runtime/components/graph-utils.d.ts.map +1 -0
  57. package/lib/esm/src/runtime/components/interfaces.d.ts +9 -13
  58. package/lib/esm/src/runtime/components/interfaces.d.ts.map +1 -1
  59. package/lib/esm/src/runtime/components/types.d.ts +1 -10
  60. package/lib/esm/src/runtime/components/types.d.ts.map +1 -1
  61. package/package.json +2 -2
  62. package/lib/cjs/src/runtime/AbstractEngine.d.ts +0 -28
  63. package/lib/cjs/src/runtime/AbstractEngine.d.ts.map +0 -1
  64. package/lib/cjs/src/runtime/UnifiedEngine.d.ts.map +0 -1
  65. package/lib/cjs/src/runtime/components/ExecutionScheduler.d.ts +0 -56
  66. package/lib/cjs/src/runtime/components/ExecutionScheduler.d.ts.map +0 -1
  67. package/lib/cjs/src/runtime/components/GraphStructure.d.ts +0 -36
  68. package/lib/cjs/src/runtime/components/GraphStructure.d.ts.map +0 -1
  69. package/lib/cjs/src/runtime/components/ValuePropagator.d.ts +0 -46
  70. package/lib/cjs/src/runtime/components/ValuePropagator.d.ts.map +0 -1
  71. package/lib/esm/src/runtime/AbstractEngine.d.ts +0 -28
  72. package/lib/esm/src/runtime/AbstractEngine.d.ts.map +0 -1
  73. package/lib/esm/src/runtime/UnifiedEngine.d.ts.map +0 -1
  74. package/lib/esm/src/runtime/components/ExecutionScheduler.d.ts +0 -56
  75. package/lib/esm/src/runtime/components/ExecutionScheduler.d.ts.map +0 -1
  76. package/lib/esm/src/runtime/components/GraphStructure.d.ts +0 -36
  77. package/lib/esm/src/runtime/components/GraphStructure.d.ts.map +0 -1
  78. package/lib/esm/src/runtime/components/ValuePropagator.d.ts +0 -46
  79. package/lib/esm/src/runtime/components/ValuePropagator.d.ts.map +0 -1
package/lib/esm/index.js CHANGED
@@ -403,48 +403,14 @@ class Registry {
403
403
  Registry.idCounter = 0;
404
404
 
405
405
  /**
406
- * Shared utility functions for runtime components
407
- */
408
- /**
409
- * Type guard to check if a value is a Promise
410
- */
411
- function isPromise(value) {
412
- return !!value && typeof value.then === "function";
413
- }
414
- /**
415
- * Unwrap a value that might be a Promise
416
- */
417
- async function unwrapMaybePromise(value) {
418
- return isPromise(value) ? await value : value;
419
- }
420
- /**
421
- * Shallow/deep-ish equality check to avoid unnecessary runs on identical values
422
- */
423
- function valuesEqual(a, b) {
424
- if (a === b)
425
- return true;
426
- if (typeof a !== typeof b)
427
- return false;
428
- if (a && b && typeof a === "object") {
429
- try {
430
- return JSON.stringify(a) === JSON.stringify(b);
431
- }
432
- catch {
433
- return false;
434
- }
435
- }
436
- return false;
437
- }
438
-
439
- /**
440
- * GraphStructure component - manages nodes, edges, and handle resolution
406
+ * Graph component - manages nodes, edges, and handle resolution
441
407
  */
442
- class GraphStructure {
408
+ class Graph {
443
409
  constructor(registry) {
410
+ this.registry = registry;
444
411
  this.nodes = new Map();
445
412
  this.edges = [];
446
413
  this.resolvedByNode = new Map();
447
- this.registry = registry;
448
414
  }
449
415
  // Node accessors
450
416
  getNode(nodeId) {
@@ -486,99 +452,21 @@ class GraphStructure {
486
452
  getResolvedHandlesMap() {
487
453
  return this.resolvedByNode;
488
454
  }
489
- // Static factory methods
490
- static computeResolvedHandleMap(def, registry, environment) {
491
- const out = new Map();
492
- const pending = new Set();
493
- for (const n of def.nodes) {
494
- const desc = registry.nodes.get(n.typeId);
495
- if (!desc)
496
- continue;
497
- const overrideInputs = n.resolvedHandles?.inputs;
498
- const overrideOutputs = n.resolvedHandles?.outputs;
499
- const overrideDefaults = n.resolvedHandles?.inputDefaults;
500
- // Resolve dynamic handles if available (initial pass: inputs may be undefined)
501
- let dyn = {};
502
- try {
503
- if (typeof desc.resolveHandles === "function") {
504
- const maybe = desc.resolveHandles({
505
- nodeId: n.nodeId,
506
- environment: environment || {},
507
- params: n.params,
508
- inputs: undefined,
509
- });
510
- // Only use sync results here; async results are applied via recompute later
511
- if (isPromise(maybe)) {
512
- // mark node as pending async recompute
513
- pending.add(n.nodeId);
514
- }
515
- else {
516
- dyn = maybe || {};
517
- }
518
- }
519
- }
520
- catch {
521
- // ignore dynamic resolution errors at this stage
522
- }
523
- // Merge base with dynamic and overrides (allow partial resolvedHandles)
524
- const inputs = {
525
- ...desc.inputs,
526
- ...dyn.inputs,
527
- ...overrideInputs,
528
- };
529
- const outputs = {
530
- ...desc.outputs,
531
- ...dyn.outputs,
532
- ...overrideOutputs,
533
- };
534
- const inputDefaults = {
535
- ...desc.inputDefaults,
536
- ...dyn.inputDefaults,
537
- ...overrideDefaults,
538
- };
539
- out.set(n.nodeId, { inputs, outputs, inputDefaults });
455
+ /**
456
+ * Check if all inbound edges for a node have values
457
+ */
458
+ allInboundHaveValue(nodeId) {
459
+ const node = this.nodes.get(nodeId);
460
+ if (!node)
461
+ return false;
462
+ const inbound = this.edges.filter((e) => e.target.nodeId === nodeId);
463
+ if (inbound.length === 0)
464
+ return true;
465
+ for (const e of inbound) {
466
+ if (!(e.target.handle in node.inputs))
467
+ return false;
540
468
  }
541
- return { map: out, pending };
542
- }
543
- static buildEdges(def, registry, resolvedByNode) {
544
- return def.edges.map((e) => {
545
- const srcNode = def.nodes.find((n) => n.nodeId === e.source.nodeId);
546
- const dstNode = def.nodes.find((n) => n.nodeId === e.target.nodeId);
547
- let effectiveTypeId = e.typeId; // Start with original
548
- let srcDeclared;
549
- let dstDeclared;
550
- if (srcNode) {
551
- const resolved = resolvedByNode.get(srcNode.nodeId);
552
- if (resolved)
553
- srcDeclared = resolved.outputs[e.source.handle];
554
- }
555
- if (!effectiveTypeId) {
556
- // Infer if not explicitly set
557
- effectiveTypeId = Array.isArray(srcDeclared)
558
- ? srcDeclared[0]
559
- : srcDeclared;
560
- }
561
- if (dstNode) {
562
- const resolved = resolvedByNode.get(dstNode.nodeId);
563
- if (resolved)
564
- dstDeclared = getInputTypeId(resolved.inputs, e.target.handle);
565
- }
566
- const { convert, convertAsync } = GraphStructure.buildEdgeConverters(srcDeclared, dstDeclared, registry, `buildEdges: ${srcNode?.typeId || ""}.${e.source.nodeId}.${e.source.handle} -> ${dstNode?.typeId || ""}.${e.target.nodeId}.${e.target.handle}`);
567
- return {
568
- id: e.id,
569
- source: { ...e.source },
570
- target: { ...e.target },
571
- typeId: e.typeId, // Preserve original (may be undefined)
572
- effectiveTypeId: effectiveTypeId ?? "untyped", // Always present
573
- convert,
574
- convertAsync,
575
- srcUnionTypes: Array.isArray(srcDeclared)
576
- ? [...srcDeclared]
577
- : undefined,
578
- dstDeclared,
579
- stats: { runs: 0, inFlight: false, progress: 0 },
580
- };
581
- });
469
+ return true;
582
470
  }
583
471
  // Clear all data
584
472
  clear() {
@@ -586,70 +474,6 @@ class GraphStructure {
586
474
  this.edges = [];
587
475
  this.resolvedByNode.clear();
588
476
  }
589
- // Static helper: build edge converters for type coercion
590
- static buildEdgeConverters(srcDeclared, dstDeclared, registry, edgeLabel) {
591
- if (!dstDeclared || !srcDeclared) {
592
- return {};
593
- }
594
- const isUnion = Array.isArray(srcDeclared);
595
- const srcTypes = isUnion ? srcDeclared : [srcDeclared];
596
- // Helper to get the coercion for a specific type
597
- const getCoercion = (typeId) => {
598
- return registry.resolveCoercion(typeId, dstDeclared);
599
- };
600
- // Resolve coercions for all source types
601
- const coercions = srcTypes.map(getCoercion);
602
- const hasAsync = coercions.some((r) => r?.kind === "async");
603
- // Helper to extract and validate typed output for unions
604
- const extractPayload = (v) => {
605
- const typeId = getTypedOutputTypeId(v);
606
- const payload = getTypedOutputValue(v);
607
- if (isUnion) {
608
- if (!typeId) {
609
- throw new Error(`Typed output required for union source (${edgeLabel}); allowed: ${srcTypes.join("|")}`);
610
- }
611
- if (!srcTypes.includes(typeId)) {
612
- throw new Error(`Invalid typed output ${typeId} (${edgeLabel}); allowed: ${srcTypes.join("|")}`);
613
- }
614
- }
615
- else if (typeId) {
616
- // Warn if typed output is used for non-union source
617
- console.warn(`Typed output ${typeId} is fed even though source is not union (${edgeLabel}): ${srcDeclared} -> ${dstDeclared}`);
618
- }
619
- return { typeId: typeId || srcTypes[0], payload };
620
- };
621
- if (hasAsync) {
622
- return {
623
- convertAsync: async (v, signal) => {
624
- const { typeId, payload } = extractPayload(v);
625
- const res = getCoercion(typeId);
626
- if (!res)
627
- return payload;
628
- if (res.kind === "async") {
629
- return await res.convertAsync(payload, signal);
630
- }
631
- return res.convert(payload);
632
- },
633
- };
634
- }
635
- // Sync path
636
- const firstCoercion = coercions.find((r) => r?.kind === "sync");
637
- if (!firstCoercion) {
638
- return {};
639
- }
640
- return {
641
- convert: (v) => {
642
- const { typeId, payload } = extractPayload(v);
643
- const res = getCoercion(typeId);
644
- if (!res)
645
- return payload;
646
- if (res.kind === "async") {
647
- throw new Error(`Async coercion required but convert used (${edgeLabel})`);
648
- }
649
- return res.convert(payload);
650
- },
651
- };
652
- }
653
477
  }
654
478
 
655
479
  /**
@@ -689,25 +513,30 @@ class EventEmitter {
689
513
  * RunContextManager component - manages run-context lifecycle
690
514
  */
691
515
  class RunContextManager {
692
- constructor() {
516
+ constructor(graph) {
517
+ this.graph = graph;
693
518
  this.runContexts = new Map();
694
519
  this.runContextCounter = 0;
520
+ this.graph = graph;
695
521
  }
696
522
  /**
697
523
  * Create a new run-context for runFromHere
698
524
  */
699
- createRunContext(startNodeId, options) {
525
+ createRunContext(startNodeId, resolve, options) {
700
526
  const id = `rc-${++this.runContextCounter}`;
701
527
  const ctx = {
702
528
  id,
703
529
  startNodes: new Set([startNodeId]),
704
530
  cancelledNodes: new Set(),
705
- pending: 0,
531
+ pendingNodes: 0,
532
+ pendingEdges: 0,
533
+ pendingResolvers: 0,
706
534
  skipPropagateValues: options?.skipPropagateValues ?? false,
707
535
  propagate: options?.propagate ?? true,
536
+ resolve,
708
537
  };
709
538
  this.runContexts.set(id, ctx);
710
- return ctx;
539
+ return id;
711
540
  }
712
541
  /**
713
542
  * Get a run-context by ID
@@ -727,18 +556,60 @@ class RunContextManager {
727
556
  hasActiveRunContexts() {
728
557
  return this.runContexts.size > 0;
729
558
  }
559
+ startNodeRun(id, nodeId) {
560
+ const ctx = this.runContexts.get(id);
561
+ if (!ctx)
562
+ return;
563
+ ctx.pendingNodes++;
564
+ }
565
+ finishNodeRun(id, nodeId) {
566
+ const ctx = this.runContexts.get(id);
567
+ if (!ctx)
568
+ return;
569
+ ctx.pendingNodes--;
570
+ this.finishRunContextIfPossible(id);
571
+ }
572
+ startEdgeConversion(id, edgeId) {
573
+ const ctx = this.runContexts.get(id);
574
+ if (!ctx)
575
+ return;
576
+ ctx.pendingEdges++;
577
+ }
578
+ finishEdgeConversion(id, edgeId) {
579
+ const ctx = this.runContexts.get(id);
580
+ if (!ctx)
581
+ return;
582
+ ctx.pendingEdges--;
583
+ this.finishRunContextIfPossible(id);
584
+ }
585
+ startHandleResolution(id, nodeId) {
586
+ const ctx = this.runContexts.get(id);
587
+ if (!ctx)
588
+ return;
589
+ ctx.pendingResolvers++;
590
+ }
591
+ finishHandleResolution(id, nodeId) {
592
+ const ctx = this.runContexts.get(id);
593
+ if (!ctx)
594
+ return;
595
+ ctx.pendingResolvers--;
596
+ this.finishRunContextIfPossible(id);
597
+ }
730
598
  /**
731
- * Finish and remove a run-context when its pending count reaches zero
599
+ * Finish and remove a run-context when all pending operations reach zero
732
600
  */
733
- finishRunContext(id, nodes) {
601
+ finishRunContextIfPossible(id) {
734
602
  const ctx = this.runContexts.get(id);
735
603
  if (!ctx)
736
604
  return;
737
- if (ctx.pending > 0)
605
+ if (ctx.pendingNodes > 0 ||
606
+ ctx.pendingEdges > 0 ||
607
+ ctx.pendingResolvers > 0) {
738
608
  return; // Still has pending work
609
+ }
739
610
  // Clean up activeRunContexts from all nodes
740
- for (const node of nodes.values()) {
741
- node.activeRunContexts.delete(id);
611
+ for (const node of this.graph.getNodes().values()) {
612
+ node.activeRunContextIds.delete(id);
742
613
  }
743
614
  this.runContexts.delete(id);
744
615
  if (ctx.resolve)
@@ -751,7 +622,7 @@ class RunContextManager {
751
622
  * @param edges - All edges in the graph (for downstream traversal)
752
623
  * @param nodes - All nodes in the graph (for clearing activeRunContexts)
753
624
  */
754
- cancelNodeInRunContexts(nodeId, includeDownstream, edges, nodes) {
625
+ cancelNodeInRunContexts(nodeId, includeDownstream) {
755
626
  const toCancel = new Set([nodeId]);
756
627
  if (includeDownstream) {
757
628
  // Collect all downstream nodes
@@ -762,7 +633,7 @@ class RunContextManager {
762
633
  if (visited.has(cur))
763
634
  continue;
764
635
  visited.add(cur);
765
- for (const e of edges) {
636
+ for (const e of this.graph.getEdges()) {
766
637
  if (e.source.nodeId === cur) {
767
638
  const targetId = e.target.nodeId;
768
639
  if (!visited.has(targetId)) {
@@ -781,9 +652,9 @@ class RunContextManager {
781
652
  }
782
653
  // Clear activeRunContexts for cancelled nodes
783
654
  for (const id of toCancel) {
784
- const node = nodes.get(id);
655
+ const node = this.graph.getNode(id);
785
656
  if (node)
786
- node.activeRunContexts.clear();
657
+ node.activeRunContextIds.clear();
787
658
  }
788
659
  }
789
660
  /**
@@ -811,17 +682,214 @@ const LOG_LEVEL_VALUES = {
811
682
  silent: 4,
812
683
  };
813
684
 
685
+ /**
686
+ * Shared utility functions for runtime components
687
+ */
688
+ /**
689
+ * Type guard to check if a value is a Promise
690
+ */
691
+ function isPromise(value) {
692
+ return !!value && typeof value.then === "function";
693
+ }
694
+ /**
695
+ * Unwrap a value that might be a Promise
696
+ */
697
+ async function unwrapMaybePromise(value) {
698
+ return isPromise(value) ? await value : value;
699
+ }
700
+ /**
701
+ * Shallow/deep-ish equality check to avoid unnecessary runs on identical values
702
+ */
703
+ function valuesEqual(a, b) {
704
+ if (a === b)
705
+ return true;
706
+ if (typeof a !== typeof b)
707
+ return false;
708
+ if (a && b && typeof a === "object") {
709
+ try {
710
+ return JSON.stringify(a) === JSON.stringify(b);
711
+ }
712
+ catch {
713
+ return false;
714
+ }
715
+ }
716
+ return false;
717
+ }
718
+
719
+ function tryHandleResolving(def, registry, environment) {
720
+ const out = new Map();
721
+ const pending = new Set();
722
+ for (const n of def.nodes) {
723
+ const desc = registry.nodes.get(n.typeId);
724
+ if (!desc)
725
+ continue;
726
+ const overrideInputs = n.resolvedHandles?.inputs;
727
+ const overrideOutputs = n.resolvedHandles?.outputs;
728
+ const overrideDefaults = n.resolvedHandles?.inputDefaults;
729
+ // Resolve dynamic handles if available (initial pass: inputs may be undefined)
730
+ let dyn = {};
731
+ try {
732
+ if (typeof desc.resolveHandles === "function") {
733
+ const maybe = desc.resolveHandles({
734
+ nodeId: n.nodeId,
735
+ environment: environment || {},
736
+ params: n.params,
737
+ inputs: undefined,
738
+ });
739
+ // Only use sync results here; async results are applied via recompute later
740
+ if (isPromise(maybe)) {
741
+ // mark node as pending async recompute
742
+ pending.add(n.nodeId);
743
+ }
744
+ else {
745
+ dyn = maybe || {};
746
+ }
747
+ }
748
+ }
749
+ catch {
750
+ // ignore dynamic resolution errors at this stage
751
+ }
752
+ // Merge base with dynamic and overrides (allow partial resolvedHandles)
753
+ const inputs = {
754
+ ...desc.inputs,
755
+ ...dyn.inputs,
756
+ ...overrideInputs,
757
+ };
758
+ const outputs = {
759
+ ...desc.outputs,
760
+ ...dyn.outputs,
761
+ ...overrideOutputs,
762
+ };
763
+ const inputDefaults = {
764
+ ...desc.inputDefaults,
765
+ ...dyn.inputDefaults,
766
+ ...overrideDefaults,
767
+ };
768
+ out.set(n.nodeId, { inputs, outputs, inputDefaults });
769
+ }
770
+ return { resolved: out, pending };
771
+ }
772
+ function buildEdges(def, registry, resolvedByNode) {
773
+ return def.edges.map((e) => {
774
+ const srcNode = def.nodes.find((n) => n.nodeId === e.source.nodeId);
775
+ const dstNode = def.nodes.find((n) => n.nodeId === e.target.nodeId);
776
+ const { srcDeclared, dstDeclared, effectiveTypeId } = extractEdgeTypes(e.source.nodeId, e.source.handle, e.target.nodeId, e.target.handle, resolvedByNode, e.typeId);
777
+ const { convert, convertAsync } = buildEdgeConverters(srcDeclared, dstDeclared, registry, `buildEdges: ${srcNode?.typeId || ""}.${e.source.nodeId}.${e.source.handle} -> ${dstNode?.typeId || ""}.${e.target.nodeId}.${e.target.handle}`);
778
+ return {
779
+ id: e.id,
780
+ source: { ...e.source },
781
+ target: { ...e.target },
782
+ typeId: e.typeId, // Preserve original (may be undefined)
783
+ effectiveTypeId, // Always present
784
+ convert,
785
+ convertAsync,
786
+ srcUnionTypes: Array.isArray(srcDeclared) ? [...srcDeclared] : undefined,
787
+ dstDeclared,
788
+ stats: { runs: 0, inFlight: false, progress: 0 },
789
+ };
790
+ });
791
+ }
792
+ /**
793
+ * Extract edge type information from resolved handles
794
+ * Used by both buildEdges and updateNodeHandles to avoid duplication
795
+ */
796
+ function extractEdgeTypes(sourceNodeId, sourceHandle, targetNodeId, targetHandle, resolvedByNode, explicitTypeId) {
797
+ const srcResolved = resolvedByNode.get(sourceNodeId);
798
+ const dstResolved = resolvedByNode.get(targetNodeId);
799
+ const srcDeclared = srcResolved
800
+ ? srcResolved.outputs[sourceHandle]
801
+ : undefined;
802
+ const dstDeclared = dstResolved
803
+ ? getInputTypeId(dstResolved.inputs, targetHandle)
804
+ : undefined;
805
+ let effectiveTypeId = explicitTypeId;
806
+ if (!effectiveTypeId) {
807
+ // Infer if not explicitly set
808
+ effectiveTypeId = Array.isArray(srcDeclared) ? srcDeclared[0] : srcDeclared;
809
+ }
810
+ return {
811
+ srcDeclared,
812
+ dstDeclared,
813
+ effectiveTypeId: effectiveTypeId ?? "untyped",
814
+ };
815
+ }
816
+ // Static helper: build edge converters for type coercion
817
+ function buildEdgeConverters(srcDeclared, dstDeclared, registry, edgeLabel) {
818
+ if (!dstDeclared || !srcDeclared) {
819
+ return {};
820
+ }
821
+ const isUnion = Array.isArray(srcDeclared);
822
+ const srcTypes = isUnion ? srcDeclared : [srcDeclared];
823
+ // Helper to get the coercion for a specific type
824
+ const getCoercion = (typeId) => {
825
+ return registry.resolveCoercion(typeId, dstDeclared);
826
+ };
827
+ // Resolve coercions for all source types
828
+ const coercions = srcTypes.map(getCoercion);
829
+ const hasAsync = coercions.some((r) => r?.kind === "async");
830
+ // Helper to extract and validate typed output for unions
831
+ const extractPayload = (v) => {
832
+ const typeId = getTypedOutputTypeId(v);
833
+ const payload = getTypedOutputValue(v);
834
+ if (isUnion) {
835
+ if (!typeId) {
836
+ throw new Error(`Typed output required for union source (${edgeLabel}); allowed: ${srcTypes.join("|")}`);
837
+ }
838
+ if (!srcTypes.includes(typeId)) {
839
+ throw new Error(`Invalid typed output ${typeId} (${edgeLabel}); allowed: ${srcTypes.join("|")}`);
840
+ }
841
+ }
842
+ else if (typeId) {
843
+ // Warn if typed output is used for non-union source
844
+ console.warn(`Typed output ${typeId} is fed even though source is not union (${edgeLabel}): ${srcDeclared} -> ${dstDeclared}`);
845
+ }
846
+ return { typeId: typeId || srcTypes[0], payload };
847
+ };
848
+ if (hasAsync) {
849
+ return {
850
+ convertAsync: async (v, signal) => {
851
+ const { typeId, payload } = extractPayload(v);
852
+ const res = getCoercion(typeId);
853
+ if (!res)
854
+ return payload;
855
+ if (res.kind === "async") {
856
+ return await res.convertAsync(payload, signal);
857
+ }
858
+ return res.convert(payload);
859
+ },
860
+ };
861
+ }
862
+ // Sync path
863
+ const firstCoercion = coercions.find((r) => r?.kind === "sync");
864
+ if (!firstCoercion) {
865
+ return {};
866
+ }
867
+ return {
868
+ convert: (v) => {
869
+ const { typeId, payload } = extractPayload(v);
870
+ const res = getCoercion(typeId);
871
+ if (!res)
872
+ return payload;
873
+ if (res.kind === "async") {
874
+ throw new Error(`Async coercion required but convert used (${edgeLabel})`);
875
+ }
876
+ return res.convert(payload);
877
+ },
878
+ };
879
+ }
880
+
814
881
  /**
815
882
  * HandleResolver component - manages dynamic handle resolution
816
883
  */
817
884
  class HandleResolver {
818
- constructor(graphStructure, eventEmitter, runtimeCoordinator, registry, environment) {
819
- this.recomputeTokenByNode = new Map();
820
- this.environment = {};
821
- this.graphStructure = graphStructure;
885
+ constructor(graph, eventEmitter, runContextManager, edgePropagator, registry, environment) {
886
+ this.graph = graph;
822
887
  this.eventEmitter = eventEmitter;
823
- this.runtimeCoordinator = runtimeCoordinator;
888
+ this.runContextManager = runContextManager;
889
+ this.edgePropagator = edgePropagator;
824
890
  this.registry = registry;
891
+ this.recomputeTokenByNode = new Map();
892
+ this.environment = {};
825
893
  this.environment = environment ?? {};
826
894
  }
827
895
  setRegistry(registry) {
@@ -837,106 +905,162 @@ class HandleResolver {
837
905
  // If no registry or node not found, skip
838
906
  if (!this.registry)
839
907
  return;
840
- const node = this.graphStructure.getNode(nodeId);
908
+ const node = this.graph.getNode(nodeId);
841
909
  if (!node)
842
910
  return;
911
+ // Track resolver start for all active run-contexts
912
+ if (node.activeRunContextIds && node.activeRunContextIds.size > 0) {
913
+ for (const runContextId of node.activeRunContextIds) {
914
+ this.runContextManager.startHandleResolution(runContextId, nodeId);
915
+ }
916
+ }
843
917
  setTimeout(() => {
844
- void this.recomputeHandlesForNode(nodeId);
918
+ void this.recomputeHandlesForNode(nodeId, node.activeRunContextIds);
845
919
  }, 0);
846
920
  }
847
- /**
848
- * Recompute dynamic handles for a single node using current inputs/environment
849
- */
850
- async recomputeHandlesForNode(nodeId) {
851
- const registry = this.registry;
852
- const node = this.graphStructure.getNode(nodeId);
853
- if (!node)
854
- return;
855
- const desc = registry.nodes.get(node.typeId);
856
- if (!desc)
921
+ // Update resolved handles for a single node and refresh edge converters/types that touch it
922
+ updateNodeHandles(nodeId, handles) {
923
+ if (!this.registry)
857
924
  return;
858
- const resolveHandles = desc.resolveHandles;
859
- if (typeof resolveHandles !== "function")
925
+ const node = this.graph.getNode(nodeId);
926
+ if (!node)
860
927
  return;
861
- const token = (this.recomputeTokenByNode.get(nodeId) ?? 0) + 1;
862
- this.recomputeTokenByNode.set(nodeId, token);
863
- // Log resolveHandles-start
864
- const nodeLogLevel = node.logLevel ?? "info";
865
- const nodeLogValue = LOG_LEVEL_VALUES[nodeLogLevel] ?? 1;
866
- const shouldLog = nodeLogValue <= LOG_LEVEL_VALUES.debug && nodeLogLevel !== "silent";
867
- if (shouldLog) {
868
- console.info(`[node:${nodeId}:${node.typeId}] resolveHandles-start`);
869
- }
870
- let r;
871
- try {
872
- const res = resolveHandles({
873
- nodeId,
874
- environment: this.environment || {},
875
- params: node.params,
876
- inputs: node.inputs || {},
877
- });
878
- r = await unwrapMaybePromise(res);
879
- }
880
- catch {
881
- // Log resolveHandles-done even on error
882
- if (shouldLog) {
883
- console.info(`[node:${nodeId}:${node.typeId}] resolveHandles-done (error)`);
928
+ this.graph.setResolvedHandles(nodeId, handles);
929
+ const edges = this.graph.getEdges();
930
+ const resolvedByNode = this.graph.getResolvedHandlesMap();
931
+ for (const e of edges) {
932
+ // Only update edges that touch the changed node
933
+ const touchesChangedNode = e.source.nodeId === nodeId || e.target.nodeId === nodeId;
934
+ if (!touchesChangedNode)
935
+ continue;
936
+ const srcNode = this.graph.getNode(e.source.nodeId);
937
+ const dstNode = this.graph.getNode(e.target.nodeId);
938
+ const oldDstDeclared = e.dstDeclared;
939
+ // Extract edge types using shared helper (handles both source and target updates)
940
+ const { srcDeclared, dstDeclared, effectiveTypeId } = extractEdgeTypes(e.source.nodeId, e.source.handle, e.target.nodeId, e.target.handle, resolvedByNode, e.typeId);
941
+ // Update edge properties
942
+ if (!e.typeId) {
943
+ e.effectiveTypeId = effectiveTypeId;
944
+ }
945
+ e.dstDeclared = dstDeclared;
946
+ e.srcUnionTypes = Array.isArray(srcDeclared)
947
+ ? [...srcDeclared]
948
+ : undefined;
949
+ // Update converters
950
+ const conv = buildEdgeConverters(srcDeclared, dstDeclared, this.registry, `updateNodeHandles: ${srcNode?.typeId || ""}.${e.source.nodeId}.${e.source.handle} -> ${dstNode?.typeId || ""}.${e.target.nodeId}.${e.target.handle}`);
951
+ e.convert = conv.convert;
952
+ e.convertAsync = conv.convertAsync;
953
+ if (e.target.nodeId === nodeId &&
954
+ oldDstDeclared === undefined &&
955
+ dstDeclared !== undefined) {
956
+ const srcNode = this.graph.getNode(e.source.nodeId);
957
+ if (srcNode) {
958
+ const srcValue = srcNode.outputs[e.source.handle];
959
+ if (srcValue !== undefined) {
960
+ this.edgePropagator.propagate(e.source.nodeId, e.source.handle, srcValue, srcNode.activeRunContextIds);
961
+ }
962
+ }
963
+ }
964
+ }
965
+ this.edgePropagator.invalidateDownstream(nodeId);
966
+ }
967
+ /**
968
+ * Recompute dynamic handles for a single node using current inputs/environment
969
+ */
970
+ async recomputeHandlesForNode(nodeId, activeRunContextIds) {
971
+ try {
972
+ if (!this.registry)
973
+ return;
974
+ const node = this.graph.getNode(nodeId);
975
+ if (!node)
976
+ return;
977
+ const desc = this.registry.nodes.get(node.typeId);
978
+ if (!desc)
979
+ return;
980
+ const resolveHandles = desc.resolveHandles;
981
+ if (typeof resolveHandles !== "function")
982
+ return;
983
+ const token = (this.recomputeTokenByNode.get(nodeId) ?? 0) + 1;
984
+ this.recomputeTokenByNode.set(nodeId, token);
985
+ // Log resolveHandles-start
986
+ const nodeLogLevel = node.logLevel ?? "info";
987
+ const nodeLogValue = LOG_LEVEL_VALUES[nodeLogLevel] ?? 1;
988
+ const shouldLog = nodeLogValue <= LOG_LEVEL_VALUES.debug && nodeLogLevel !== "silent";
989
+ if (shouldLog) {
990
+ console.info(`[node:${nodeId}:${node.typeId}] resolveHandles-start`);
991
+ }
992
+ let resolved;
993
+ try {
994
+ const res = resolveHandles({
995
+ nodeId,
996
+ environment: this.environment || {},
997
+ params: node.params,
998
+ inputs: node.inputs || {},
999
+ });
1000
+ resolved = await unwrapMaybePromise(res);
1001
+ }
1002
+ catch {
1003
+ // Log resolveHandles-done even on error
1004
+ if (shouldLog) {
1005
+ console.info(`[node:${nodeId}:${node.typeId}] resolveHandles-done (error)`);
1006
+ }
1007
+ return;
1008
+ }
1009
+ // Log resolveHandles-done
1010
+ if (shouldLog) {
1011
+ console.info(`[node:${nodeId}:${node.typeId}] resolveHandles-done`);
1012
+ }
1013
+ // If a newer recompute was scheduled, drop this result
1014
+ if ((this.recomputeTokenByNode.get(nodeId) ?? 0) !== token)
1015
+ return;
1016
+ const before = this.graph.getResolvedHandles(nodeId);
1017
+ if (!before)
1018
+ return;
1019
+ // Re-fetch desc to ensure we have the latest (node might have been updated)
1020
+ const nodeDesc = this.registry.nodes.get(node.typeId);
1021
+ if (!nodeDesc)
1022
+ return;
1023
+ const inputs = { ...nodeDesc.inputs, ...resolved?.inputs };
1024
+ const outputs = { ...nodeDesc.outputs, ...resolved?.outputs };
1025
+ const inputDefaults = {
1026
+ ...nodeDesc.inputDefaults,
1027
+ ...resolved?.inputDefaults,
1028
+ };
1029
+ const after = { inputs, outputs, inputDefaults };
1030
+ // Compare shallow-structurally via JSON
1031
+ if (JSON.stringify(before) === JSON.stringify(after))
1032
+ return;
1033
+ // Call GraphRuntime's updateNodeHandles to update edges and re-propagate values
1034
+ // Note: updateNodeHandles will set the resolved handles internally
1035
+ this.updateNodeHandles(nodeId, after);
1036
+ // Notify graph updated with the changed handles
1037
+ this.eventEmitter.emit("invalidate", {
1038
+ reason: "graph-updated",
1039
+ resolvedHandles: { [nodeId]: after },
1040
+ });
1041
+ }
1042
+ finally {
1043
+ // Track resolver finish after successful completion
1044
+ if (activeRunContextIds && activeRunContextIds.size > 0) {
1045
+ for (const runContextId of activeRunContextIds) {
1046
+ this.runContextManager.finishHandleResolution(runContextId, nodeId);
1047
+ }
884
1048
  }
885
- return;
886
- }
887
- // Log resolveHandles-done
888
- if (shouldLog) {
889
- console.info(`[node:${nodeId}:${node.typeId}] resolveHandles-done`);
890
1049
  }
891
- // If a newer recompute was scheduled, drop this result
892
- if ((this.recomputeTokenByNode.get(nodeId) ?? 0) !== token)
893
- return;
894
- const resolved = this.graphStructure.getResolvedHandles(nodeId);
895
- if (!resolved)
896
- return; // Node was deleted
897
- // Re-fetch desc to ensure we have the latest (node might have been updated)
898
- const nodeDesc = registry.nodes.get(node.typeId);
899
- if (!nodeDesc)
900
- return;
901
- const inputs = { ...nodeDesc.inputs, ...r?.inputs };
902
- const outputs = { ...nodeDesc.outputs, ...r?.outputs };
903
- const inputDefaults = {
904
- ...nodeDesc.inputDefaults,
905
- ...r?.inputDefaults,
906
- };
907
- const next = { inputs, outputs, inputDefaults };
908
- const before = resolved;
909
- // Compare shallow-structurally via JSON
910
- if (JSON.stringify(before) === JSON.stringify(next))
911
- return;
912
- this.graphStructure.setResolvedHandles(nodeId, next);
913
- // Call GraphRuntime's updateNodeHandles to update edges and re-propagate values
914
- this.runtimeCoordinator.updateNodeHandles(nodeId, next, registry);
915
- // Notify graph updated with the changed handles
916
- this.eventEmitter.emit("invalidate", {
917
- reason: "graph-updated",
918
- resolvedHandles: { [nodeId]: next },
919
- });
920
1050
  }
921
1051
  }
922
1052
 
923
1053
  /**
924
- * ValuePropagator component - handles value propagation through edges
1054
+ * EdgePropagator component - handles value propagation through edges
925
1055
  */
926
- class ValuePropagator {
927
- constructor(graphStructure, eventEmitter, runContextManager, handleResolver, runtimeCoordinator) {
928
- this.arrayInputBuckets = new Map();
929
- this.graphStructure = graphStructure;
1056
+ class EdgePropagator {
1057
+ constructor(graph, eventEmitter, runContextManager, handleResolver, nodeExecutor) {
1058
+ this.graph = graph;
930
1059
  this.eventEmitter = eventEmitter;
931
1060
  this.runContextManager = runContextManager;
932
1061
  this.handleResolver = handleResolver;
933
- this.runtimeCoordinator = runtimeCoordinator;
934
- }
935
- /**
936
- * Set the execution scheduler (called after construction to resolve circular dependency)
937
- */
938
- setExecutionScheduler(executionScheduler) {
939
- this.executionScheduler = executionScheduler;
1062
+ this.nodeExecutor = nodeExecutor;
1063
+ this.arrayInputBuckets = new Map();
940
1064
  }
941
1065
  /**
942
1066
  * Propagate value through edges
@@ -945,10 +1069,24 @@ class ValuePropagator {
945
1069
  */
946
1070
  propagate(srcNodeId, srcHandle, value, runContextIds) {
947
1071
  // Set source output
948
- const srcNode = this.graphStructure.getNode(srcNodeId);
1072
+ if (!this.setSourceOutput(srcNodeId, srcHandle, value)) {
1073
+ return; // Node was removed
1074
+ }
1075
+ // Find outgoing edges
1076
+ const outEdges = this.findOutgoingEdges(srcNodeId, srcHandle);
1077
+ // Process each edge
1078
+ for (const edge of outEdges) {
1079
+ this.propagateToEdge(edge, value, srcNodeId, runContextIds);
1080
+ }
1081
+ }
1082
+ /**
1083
+ * Set source output value and emit event
1084
+ */
1085
+ setSourceOutput(srcNodeId, srcHandle, value) {
1086
+ const srcNode = this.graph.getNode(srcNodeId);
949
1087
  if (!srcNode) {
950
1088
  // Node was removed (e.g., graph updated) but an async emit arrived late; ignore
951
- return;
1089
+ return false;
952
1090
  }
953
1091
  srcNode.outputs[srcHandle] = value;
954
1092
  this.eventEmitter.emit("value", {
@@ -958,221 +1096,306 @@ class ValuePropagator {
958
1096
  io: "output",
959
1097
  runtimeTypeId: getTypedOutputTypeId(value),
960
1098
  });
961
- // Fan-out along all edges from this output
962
- const edges = this.graphStructure.getEdges();
963
- const outEdges = edges.filter((e) => e.source.nodeId === srcNodeId && e.source.handle === srcHandle);
964
- const isRunContextAware = runContextIds !== undefined && runContextIds.size > 0;
965
- for (const e of outEdges) {
966
- // Filter run-contexts: skip any where src or dst is cancelled (only in run-context mode)
967
- let effectiveRunContexts;
968
- if (isRunContextAware) {
969
- effectiveRunContexts = new Set();
970
- for (const id of runContextIds) {
971
- const ctx = this.runContextManager.getRunContext(id);
972
- if (!ctx)
973
- continue;
974
- if (ctx.cancelledNodes.has(srcNodeId))
975
- continue;
976
- if (ctx.cancelledNodes.has(e.target.nodeId))
977
- continue;
978
- effectiveRunContexts.add(id);
979
- }
980
- if (effectiveRunContexts.size === 0)
981
- continue; // No valid run-contexts for this edge
982
- }
983
- // If source declares a union for this handle, require typed output
984
- const isUnion = Array.isArray(e.srcUnionTypes);
985
- const isTyped = isTypedOutput(value);
986
- if (isUnion && !isTyped) {
987
- const err = new Error(`Output ${srcNodeId}.${srcHandle} requires typed value for union output (allowed: ${e.srcUnionTypes.join("|")})`);
988
- this.eventEmitter.emit("error", {
989
- kind: "edge-convert",
990
- edgeId: e.id,
991
- source: { nodeId: e.source.nodeId, handle: e.source.handle },
992
- target: { nodeId: e.target.nodeId, handle: e.target.handle },
993
- err,
994
- });
1099
+ return true;
1100
+ }
1101
+ /**
1102
+ * Find all outgoing edges from a source node handle
1103
+ */
1104
+ findOutgoingEdges(srcNodeId, srcHandle) {
1105
+ const edges = this.graph.getEdges();
1106
+ return edges.filter((e) => e.source.nodeId === srcNodeId && e.source.handle === srcHandle);
1107
+ }
1108
+ /**
1109
+ * Propagate value to a single edge
1110
+ */
1111
+ propagateToEdge(edge, value, srcNodeId, runContextIds) {
1112
+ // Filter run-contexts
1113
+ const effectiveRunContexts = runContextIds && runContextIds.size > 0
1114
+ ? this.filterEffectiveRunContexts(edge, srcNodeId, runContextIds)
1115
+ : undefined;
1116
+ if (runContextIds &&
1117
+ runContextIds.size > 0 &&
1118
+ !(effectiveRunContexts && effectiveRunContexts.size > 0)) {
1119
+ return; // No valid run-contexts for this edge
1120
+ }
1121
+ // Validate union types
1122
+ if (!this.validateUnionType(edge, value, srcNodeId)) {
1123
+ return;
1124
+ }
1125
+ // Clone value per edge to isolate conversions
1126
+ let nextVal = structuredClone(value);
1127
+ // Apply conversion and propagate
1128
+ if (edge.convertAsync) {
1129
+ this.handleAsyncConversion(edge, nextVal, effectiveRunContexts);
1130
+ }
1131
+ else {
1132
+ this.handleSyncConversion(edge, nextVal, effectiveRunContexts);
1133
+ }
1134
+ }
1135
+ /**
1136
+ * Filter run-contexts to exclude cancelled nodes
1137
+ */
1138
+ filterEffectiveRunContexts(edge, srcNodeId, runContextIds) {
1139
+ const effectiveRunContexts = new Set();
1140
+ for (const id of runContextIds) {
1141
+ const ctx = this.runContextManager.getRunContext(id);
1142
+ if (!ctx)
995
1143
  continue;
1144
+ if (ctx.cancelledNodes.has(srcNodeId))
1145
+ continue;
1146
+ if (ctx.cancelledNodes.has(edge.target.nodeId))
1147
+ continue;
1148
+ effectiveRunContexts.add(id);
1149
+ }
1150
+ return effectiveRunContexts.size > 0 ? effectiveRunContexts : undefined;
1151
+ }
1152
+ /**
1153
+ * Validate union type requirements
1154
+ */
1155
+ validateUnionType(edge, value, srcNodeId) {
1156
+ const isUnion = Array.isArray(edge.srcUnionTypes);
1157
+ const isTyped = isTypedOutput(value);
1158
+ if (isUnion && !isTyped) {
1159
+ const err = new Error(`Output ${srcNodeId}.${edge.source.handle} requires typed value for union output (allowed: ${edge.srcUnionTypes.join("|")})`);
1160
+ this.eventEmitter.emit("error", {
1161
+ kind: "edge-convert",
1162
+ edgeId: edge.id,
1163
+ source: { nodeId: edge.source.nodeId, handle: edge.source.handle },
1164
+ target: { nodeId: edge.target.nodeId, handle: edge.target.handle },
1165
+ err,
1166
+ });
1167
+ return false;
1168
+ }
1169
+ return true;
1170
+ }
1171
+ /**
1172
+ * Handle synchronous conversion
1173
+ */
1174
+ handleSyncConversion(edge, value, effectiveRunContexts) {
1175
+ let convertedValue = value;
1176
+ if (edge.convert) {
1177
+ convertedValue = edge.convert(value);
1178
+ }
1179
+ this.applyToTarget(edge, convertedValue, effectiveRunContexts);
1180
+ }
1181
+ /**
1182
+ * Handle asynchronous conversion
1183
+ */
1184
+ handleAsyncConversion(edge, value, effectiveRunContexts) {
1185
+ if (!edge.convertAsync)
1186
+ return;
1187
+ // Track edge run-context IDs for pendingEdges tracking
1188
+ const edgeRunContextIds = effectiveRunContexts
1189
+ ? Array.from(effectiveRunContexts)
1190
+ : undefined;
1191
+ if (edgeRunContextIds) {
1192
+ for (const id of edgeRunContextIds) {
1193
+ this.runContextManager.startEdgeConversion(id, edge.id);
996
1194
  }
997
- // Clone per edge to isolate conversions
998
- let nextVal = structuredClone(value);
999
- const applyToTarget = (v) => {
1000
- const dstNode = this.graphStructure.getNode(e.target.nodeId);
1001
- if (!dstNode)
1002
- return;
1003
- // Skip writing to unresolved handles
1004
- if (e.dstDeclared === undefined)
1005
- return;
1006
- const dstIsArray = typeof e.dstDeclared === "string" && e.dstDeclared.endsWith("[]");
1007
- let next = v;
1008
- // Handle array types
1009
- if (dstIsArray) {
1010
- const toArray = (x) => Array.isArray(x) ? x : x === undefined ? [] : [x];
1011
- let forNode = this.arrayInputBuckets.get(e.target.nodeId);
1012
- if (!forNode) {
1013
- forNode = new Map();
1014
- this.arrayInputBuckets.set(e.target.nodeId, forNode);
1015
- }
1016
- let forHandle = forNode.get(e.target.handle);
1017
- if (!forHandle) {
1018
- forHandle = new Map();
1019
- forNode.set(e.target.handle, forHandle);
1020
- }
1021
- forHandle.set(e.id, toArray(v));
1022
- const merged = [];
1023
- for (const ed of edges) {
1024
- if (ed.target.nodeId === e.target.nodeId &&
1025
- ed.target.handle === e.target.handle) {
1026
- const part = forHandle.get(ed.id);
1027
- if (part && part.length)
1028
- merged.push(...part);
1029
- }
1030
- }
1031
- next = merged;
1032
- }
1033
- const prev = dstNode.inputs[e.target.handle];
1034
- const same = valuesEqual(prev, next);
1035
- if (!same) {
1036
- // Check skipPropagateValues (only in run-context mode)
1037
- let shouldSkipPropagateValues = false;
1038
- if (isRunContextAware && effectiveRunContexts) {
1039
- for (const id of effectiveRunContexts) {
1040
- const ctx = this.runContextManager.getRunContext(id);
1041
- if (ctx && ctx.skipPropagateValues) {
1042
- shouldSkipPropagateValues = true;
1043
- break;
1044
- }
1045
- }
1046
- }
1047
- // Set input values unless skipPropagateValues is enabled
1048
- if (!shouldSkipPropagateValues) {
1049
- dstNode.inputs[e.target.handle] = next;
1050
- this.eventEmitter.emit("value", {
1051
- nodeId: e.target.nodeId,
1052
- handle: e.target.handle,
1053
- value: next,
1054
- io: "input",
1055
- runtimeTypeId: getTypedOutputTypeId(next),
1056
- });
1057
- this.handleResolver.scheduleRecomputeHandles(e.target.nodeId);
1058
- }
1059
- // Check propagate flag (only in run-context mode; auto mode always propagates)
1060
- let shouldPropagate = true; // Default: always propagate in auto mode
1061
- if (isRunContextAware && effectiveRunContexts) {
1062
- shouldPropagate = false;
1063
- for (const id of effectiveRunContexts) {
1064
- const ctx = this.runContextManager.getRunContext(id);
1065
- if (ctx && ctx.propagate) {
1066
- shouldPropagate = true;
1067
- break;
1068
- }
1069
- }
1070
- }
1071
- // Schedule downstream execution if propagation is enabled
1072
- if (!this.runtimeCoordinator.isPaused() &&
1073
- shouldPropagate &&
1074
- this.executionScheduler.allInboundHaveValue(e.target.nodeId)) {
1075
- if (isRunContextAware && effectiveRunContexts) {
1076
- this.executionScheduler.scheduleInputsChangedWithRunContexts(e.target.nodeId, effectiveRunContexts);
1077
- }
1078
- else {
1079
- this.executionScheduler.scheduleInputsChanged(e.target.nodeId);
1080
- }
1081
- }
1082
- }
1083
- };
1084
- // Handle async conversion
1085
- if (e.convertAsync) {
1086
- this.eventEmitter.emit("stats", {
1087
- kind: "edge-start",
1088
- edgeId: e.id,
1089
- typeId: e.typeId,
1090
- source: { nodeId: e.source.nodeId, handle: e.source.handle },
1091
- target: { nodeId: e.target.nodeId, handle: e.target.handle },
1092
- });
1093
- const controller = new AbortController();
1094
- const startAt = Date.now();
1095
- e.stats.runs += 1;
1096
- e.stats.inFlight = true;
1097
- if (!isRunContextAware) {
1098
- // Only track progress in auto mode
1099
- e.stats.progress = 0;
1100
- }
1101
- const sig = controller.signal;
1102
- e.convertAsync(nextVal, sig)
1103
- .then((converted) => {
1104
- if (!sig.aborted) {
1105
- applyToTarget(converted);
1106
- e.stats.inFlight = false;
1107
- const duration = Date.now() - startAt;
1108
- e.stats.lastDurationMs = duration;
1109
- if (!isRunContextAware) {
1110
- // More detailed stats in auto mode
1111
- e.stats.lastEndAt = Date.now();
1112
- e.stats.lastError = undefined;
1113
- this.eventEmitter.emit("stats", {
1114
- kind: "edge-done",
1115
- edgeId: e.id,
1116
- typeId: e.typeId,
1117
- source: { nodeId: e.source.nodeId, handle: e.source.handle },
1118
- target: { nodeId: e.target.nodeId, handle: e.target.handle },
1119
- durationMs: duration,
1120
- });
1121
- }
1122
- }
1123
- })
1124
- .catch((err) => {
1125
- if (sig.aborted)
1126
- return;
1127
- e.stats.inFlight = false;
1128
- if (!isRunContextAware) {
1129
- e.stats.lastError = err;
1130
- }
1131
- this.eventEmitter.emit("error", {
1132
- kind: "edge-convert",
1133
- edgeId: e.id,
1134
- source: { nodeId: e.source.nodeId, handle: e.source.handle },
1135
- target: { nodeId: e.target.nodeId, handle: e.target.handle },
1136
- err,
1137
- });
1138
- });
1195
+ }
1196
+ this.eventEmitter.emit("stats", {
1197
+ kind: "edge-start",
1198
+ edgeId: edge.id,
1199
+ typeId: edge.typeId,
1200
+ source: { nodeId: edge.source.nodeId, handle: edge.source.handle },
1201
+ target: { nodeId: edge.target.nodeId, handle: edge.target.handle },
1202
+ });
1203
+ const controller = new AbortController();
1204
+ const startAt = Date.now();
1205
+ edge.stats.runs += 1;
1206
+ edge.stats.inFlight = true;
1207
+ edge.stats.progress = 0;
1208
+ edge
1209
+ .convertAsync(value, controller.signal)
1210
+ .then((converted) => {
1211
+ if (!controller.signal.aborted) {
1212
+ this.applyToTarget(edge, converted, effectiveRunContexts);
1213
+ this.updateEdgeStatsOnSuccess(edge, startAt);
1139
1214
  }
1140
- else {
1141
- // Synchronous conversion
1142
- if (e.convert) {
1143
- nextVal = e.convert(nextVal);
1144
- }
1145
- applyToTarget(nextVal);
1215
+ })
1216
+ .catch((err) => {
1217
+ if (controller.signal.aborted)
1218
+ return;
1219
+ this.handleEdgeConversionError(edge, err);
1220
+ })
1221
+ .finally(() => {
1222
+ this.finishEdgeConversion(edgeRunContextIds, edge.id);
1223
+ });
1224
+ }
1225
+ /**
1226
+ * Apply value to target node input
1227
+ */
1228
+ applyToTarget(edge, value, effectiveRunContexts) {
1229
+ const dstNode = this.graph.getNode(edge.target.nodeId);
1230
+ if (!dstNode)
1231
+ return;
1232
+ // Skip writing to unresolved handles
1233
+ if (edge.dstDeclared === undefined)
1234
+ return;
1235
+ // Handle array types
1236
+ const processedValue = this.processArrayInput(edge, value);
1237
+ // Check if value changed
1238
+ const prev = dstNode.inputs[edge.target.handle];
1239
+ if (valuesEqual(prev, processedValue)) {
1240
+ return; // No change
1241
+ }
1242
+ // Set input value (respecting skipPropagateValues)
1243
+ const shouldSetValue = this.shouldSetInputValue(effectiveRunContexts);
1244
+ if (shouldSetValue) {
1245
+ this.setTargetInput(edge, dstNode, processedValue);
1246
+ }
1247
+ // Schedule downstream execution
1248
+ this.executeDownstream(edge.target.nodeId, effectiveRunContexts);
1249
+ }
1250
+ /**
1251
+ * Process array input by merging values from all edges
1252
+ */
1253
+ processArrayInput(edge, value) {
1254
+ const dstIsArray = typeof edge.dstDeclared === "string" && edge.dstDeclared.endsWith("[]");
1255
+ if (!dstIsArray) {
1256
+ return value;
1257
+ }
1258
+ const toArray = (x) => Array.isArray(x) ? x : x === undefined ? [] : [x];
1259
+ let forNode = this.arrayInputBuckets.get(edge.target.nodeId);
1260
+ if (!forNode) {
1261
+ forNode = new Map();
1262
+ this.arrayInputBuckets.set(edge.target.nodeId, forNode);
1263
+ }
1264
+ let forHandle = forNode.get(edge.target.handle);
1265
+ if (!forHandle) {
1266
+ forHandle = new Map();
1267
+ forNode.set(edge.target.handle, forHandle);
1268
+ }
1269
+ forHandle.set(edge.id, toArray(value));
1270
+ // Merge all parts for this handle
1271
+ const edges = this.graph.getEdges();
1272
+ const merged = [];
1273
+ for (const ed of edges) {
1274
+ if (ed.target.nodeId === edge.target.nodeId &&
1275
+ ed.target.handle === edge.target.handle) {
1276
+ const part = forHandle.get(ed.id);
1277
+ if (part && part.length)
1278
+ merged.push(...part);
1146
1279
  }
1147
1280
  }
1281
+ return merged;
1148
1282
  }
1149
1283
  /**
1150
- * Propagate value in run-context aware mode (convenience method)
1284
+ * Check if input value should be set (respecting skipPropagateValues)
1151
1285
  */
1152
- propagateInRunContexts(srcNodeId, srcHandle, value, runContextIds) {
1153
- this.propagate(srcNodeId, srcHandle, value, runContextIds);
1286
+ shouldSetInputValue(effectiveRunContexts) {
1287
+ if (!effectiveRunContexts) {
1288
+ return true; // Auto mode always sets values
1289
+ }
1290
+ // Check skipPropagateValues (only in run-context mode)
1291
+ for (const id of effectiveRunContexts) {
1292
+ const ctx = this.runContextManager.getRunContext(id);
1293
+ if (ctx && ctx.skipPropagateValues) {
1294
+ return false;
1295
+ }
1296
+ }
1297
+ return true;
1298
+ }
1299
+ /**
1300
+ * Set target input value and emit event
1301
+ */
1302
+ setTargetInput(edge, dstNode, value) {
1303
+ dstNode.inputs[edge.target.handle] = value;
1304
+ this.eventEmitter.emit("value", {
1305
+ nodeId: edge.target.nodeId,
1306
+ handle: edge.target.handle,
1307
+ value,
1308
+ io: "input",
1309
+ runtimeTypeId: getTypedOutputTypeId(value),
1310
+ });
1311
+ this.handleResolver.scheduleRecomputeHandles(edge.target.nodeId);
1312
+ }
1313
+ /**
1314
+ * Execute downstream if conditions are met
1315
+ */
1316
+ executeDownstream(targetNodeId, effectiveRunContexts) {
1317
+ // Determine if we should propagate
1318
+ const shouldPropagate = this.shouldPropagateExecution(effectiveRunContexts);
1319
+ if (shouldPropagate && this.graph.allInboundHaveValue(targetNodeId)) {
1320
+ this.nodeExecutor.execute(targetNodeId, effectiveRunContexts);
1321
+ }
1322
+ }
1323
+ /**
1324
+ * Check if execution should propagate
1325
+ */
1326
+ shouldPropagateExecution(effectiveRunContexts) {
1327
+ if (!effectiveRunContexts) {
1328
+ return true; // Auto mode always propagates
1329
+ }
1330
+ // Check propagate flag (only in run-context mode)
1331
+ for (const id of effectiveRunContexts) {
1332
+ const ctx = this.runContextManager.getRunContext(id);
1333
+ if (ctx && ctx.propagate) {
1334
+ return true;
1335
+ }
1336
+ }
1337
+ return false;
1338
+ }
1339
+ /**
1340
+ * Update edge stats on successful conversion
1341
+ */
1342
+ updateEdgeStatsOnSuccess(edge, startAt) {
1343
+ edge.stats.inFlight = false;
1344
+ const duration = Date.now() - startAt;
1345
+ edge.stats.lastDurationMs = duration;
1346
+ this.eventEmitter.emit("stats", {
1347
+ kind: "edge-done",
1348
+ edgeId: edge.id,
1349
+ typeId: edge.typeId,
1350
+ source: { nodeId: edge.source.nodeId, handle: edge.source.handle },
1351
+ target: { nodeId: edge.target.nodeId, handle: edge.target.handle },
1352
+ durationMs: duration,
1353
+ });
1354
+ edge.stats.lastEndAt = Date.now();
1355
+ edge.stats.lastError = undefined;
1356
+ }
1357
+ /**
1358
+ * Handle edge conversion error
1359
+ */
1360
+ handleEdgeConversionError(edge, err) {
1361
+ edge.stats.inFlight = false;
1362
+ edge.stats.lastError = err;
1363
+ this.eventEmitter.emit("error", {
1364
+ kind: "edge-convert",
1365
+ edgeId: edge.id,
1366
+ source: { nodeId: edge.source.nodeId, handle: edge.source.handle },
1367
+ target: { nodeId: edge.target.nodeId, handle: edge.target.handle },
1368
+ err,
1369
+ });
1370
+ }
1371
+ /**
1372
+ * Finish edge conversion and decrement pending edges
1373
+ */
1374
+ finishEdgeConversion(edgeRunContextIds, edgeId) {
1375
+ if (!edgeRunContextIds)
1376
+ return;
1377
+ for (const id of edgeRunContextIds) {
1378
+ this.runContextManager.finishEdgeConversion(id, edgeId);
1379
+ }
1154
1380
  }
1155
1381
  /**
1156
1382
  * Re-emit all outputs from a node (used when graph updates)
1157
1383
  * Only re-emits outputs that are valid according to resolved handles
1158
1384
  */
1159
- reemitNodeOutputs(nodeId) {
1160
- const node = this.graphStructure.getNode(nodeId);
1385
+ invalidateDownstream(nodeId) {
1386
+ const node = this.graph.getNode(nodeId);
1161
1387
  if (!node)
1162
1388
  return;
1163
1389
  // Get resolved handles to filter out invalid outputs
1164
- const resolved = this.graphStructure.getResolvedHandles(nodeId);
1390
+ const resolved = this.graph.getResolvedHandles(nodeId);
1165
1391
  const validOutputHandles = resolved?.outputs
1166
1392
  ? new Set(Object.keys(resolved.outputs))
1167
1393
  : new Set();
1168
1394
  // Use node's activeRunContexts to propagate to new nodes that were added
1169
- const runContextIds = node.activeRunContexts.size > 0
1170
- ? new Set(node.activeRunContexts)
1171
- : undefined;
1172
1395
  for (const [handle, value] of Object.entries(node.outputs)) {
1173
1396
  // Only re-emit if this handle is still valid
1174
1397
  if (validOutputHandles.has(handle)) {
1175
- this.propagate(nodeId, handle, value, runContextIds);
1398
+ this.propagate(nodeId, handle, value, node.activeRunContextIds);
1176
1399
  }
1177
1400
  }
1178
1401
  }
@@ -1191,52 +1414,35 @@ class ValuePropagator {
1191
1414
  }
1192
1415
 
1193
1416
  /**
1194
- * ExecutionScheduler component - handles node execution scheduling and lifecycle
1417
+ * NodeExecutor component - handles node execution scheduling and lifecycle
1195
1418
  */
1196
- class ExecutionScheduler {
1197
- constructor(graphStructure, eventEmitter, runContextManager, valuePropagator, runtimeCoordinator, environment) {
1198
- this.environment = {};
1199
- this.graphStructure = graphStructure;
1419
+ class NodeExecutor {
1420
+ constructor(graph, eventEmitter, runContextManager, edgePropagator, runtime, environment) {
1421
+ this.graph = graph;
1200
1422
  this.eventEmitter = eventEmitter;
1201
1423
  this.runContextManager = runContextManager;
1202
- this.valuePropagator = valuePropagator;
1203
- this.runtimeCoordinator = runtimeCoordinator;
1424
+ this.edgePropagator = edgePropagator;
1425
+ this.runtime = runtime;
1426
+ this.environment = {};
1204
1427
  this.environment = environment ?? {};
1205
1428
  }
1206
1429
  setEnvironment(environment) {
1207
1430
  this.environment = environment;
1208
1431
  }
1209
- /**
1210
- * Check if all inbound edges for a node have values
1211
- */
1212
- allInboundHaveValue(nodeId) {
1213
- const node = this.graphStructure.getNode(nodeId);
1214
- if (!node)
1215
- return false;
1216
- const edges = this.graphStructure.getEdges();
1217
- const inbound = edges.filter((e) => e.target.nodeId === nodeId);
1218
- if (inbound.length === 0)
1219
- return true;
1220
- for (const e of inbound) {
1221
- if (!(e.target.handle in node.inputs))
1222
- return false;
1223
- }
1224
- return true;
1225
- }
1226
1432
  /**
1227
1433
  * Compute effective inputs for a node by merging real inputs with defaults
1228
1434
  */
1229
1435
  getEffectiveInputs(nodeId) {
1230
- const node = this.graphStructure.getNode(nodeId);
1436
+ const node = this.graph.getNode(nodeId);
1231
1437
  if (!node)
1232
1438
  return {};
1233
- const registry = this.graphStructure.getRegistry();
1439
+ const registry = this.graph.getRegistry();
1234
1440
  if (!registry)
1235
1441
  return {};
1236
1442
  const desc = registry.nodes.get(node.typeId);
1237
1443
  if (!desc)
1238
1444
  return {};
1239
- const resolved = this.graphStructure.getResolvedHandles(nodeId);
1445
+ const resolved = this.graph.getResolvedHandles(nodeId);
1240
1446
  const regDefaults = desc.inputDefaults ?? {};
1241
1447
  const dynDefaults = resolved?.inputDefaults ?? {};
1242
1448
  // Identify which handles are dynamically resolved (not in registry statics)
@@ -1250,7 +1456,7 @@ class ExecutionScheduler {
1250
1456
  // Start with real inputs only (no defaults)
1251
1457
  const effective = { ...node.inputs };
1252
1458
  // Build set of inbound handles (wired inputs)
1253
- const edges = this.graphStructure.getEdges();
1459
+ const edges = this.graph.getEdges();
1254
1460
  const inbound = new Set(edges
1255
1461
  .filter((e) => e.target.nodeId === nodeId)
1256
1462
  .map((e) => e.target.handle));
@@ -1277,7 +1483,7 @@ class ExecutionScheduler {
1277
1483
  createExecutionContext(nodeId, node, inputs, runId, abortSignal, runContextIds, options) {
1278
1484
  const emitHandler = options?.emitHandler ??
1279
1485
  ((handle, value) => {
1280
- this.valuePropagator.propagate(nodeId, handle, value, runContextIds);
1486
+ this.edgePropagator.propagate(nodeId, handle, value, runContextIds);
1281
1487
  });
1282
1488
  const reportProgress = options?.reportProgress ??
1283
1489
  ((p) => {
@@ -1312,20 +1518,18 @@ class ExecutionScheduler {
1312
1518
  }
1313
1519
  }
1314
1520
  };
1315
- // Store run-context IDs for use in scheduleInputsChanged
1316
- const storedRunContextIds = runContextIds;
1317
1521
  return {
1318
1522
  nodeId,
1319
1523
  state: node.state,
1320
1524
  setState: (next) => Object.assign(node.state, next),
1321
1525
  emit: emitHandler,
1322
1526
  invalidateDownstream: () => {
1323
- this.runtimeCoordinator.invalidateDownstream(nodeId);
1527
+ this.edgePropagator.invalidateDownstream(nodeId);
1324
1528
  },
1325
- scheduleInputsChanged: () => {
1326
- if (this.allInboundHaveValue(nodeId)) {
1529
+ execute: () => {
1530
+ if (this.graph.allInboundHaveValue(nodeId)) {
1327
1531
  // Preserve run-context IDs when scheduling from execution context
1328
- this.scheduleInputsChangedInternal(nodeId, storedRunContextIds);
1532
+ this.execute(nodeId, runContextIds);
1329
1533
  }
1330
1534
  },
1331
1535
  getInput: (handle) => inputs[handle],
@@ -1337,232 +1541,345 @@ class ExecutionScheduler {
1337
1541
  };
1338
1542
  }
1339
1543
  /**
1340
- * Schedule a node for execution when its inputs change
1544
+ * Internal method for executing inputs changed (also used by GraphRuntime)
1545
+ */
1546
+ execute(nodeId, runContextIds) {
1547
+ const node = this.graph.getNode(nodeId);
1548
+ if (!node)
1549
+ return;
1550
+ const runMode = this.runtime.getRunMode();
1551
+ if (!runMode) {
1552
+ console.warn("NodeExecutor.execute: no runMode, skipping execution");
1553
+ return;
1554
+ }
1555
+ if (runMode === "manual" && (!runContextIds || runContextIds.size === 0)) {
1556
+ console.warn("NodeExecutor.execute: no runContextIds provided, skipping execution");
1557
+ return;
1558
+ }
1559
+ if (runMode === "auto" && runContextIds && runContextIds.size > 0) {
1560
+ console.warn("NodeExecutor.execute: runContextIds provided in auto-mode, ignoring");
1561
+ runContextIds = undefined;
1562
+ }
1563
+ // Early validation for auto-mode paused state
1564
+ if (this.runtime.isPaused())
1565
+ return;
1566
+ // Attach run-context IDs if provided
1567
+ this.attachRunContexts(node, runContextIds);
1568
+ // Handle debouncing
1569
+ const now = Date.now();
1570
+ if (this.shouldDebounce(node, now)) {
1571
+ this.handleDebouncedSchedule(node, nodeId, now);
1572
+ return;
1573
+ }
1574
+ // Prepare execution plan
1575
+ const executionPlan = this.prepareExecutionPlan(node, nodeId, runContextIds, now);
1576
+ // Route to appropriate concurrency handler
1577
+ this.routeToConcurrencyHandler(node, nodeId, executionPlan);
1578
+ }
1579
+ /**
1580
+ * Attach run-context IDs to the node
1581
+ */
1582
+ attachRunContexts(node, runContextIds) {
1583
+ if (!runContextIds)
1584
+ return;
1585
+ node.activeRunContextIds = new Set([
1586
+ ...node.activeRunContextIds,
1587
+ ...runContextIds,
1588
+ ]);
1589
+ }
1590
+ /**
1591
+ * Check if execution should be debounced
1592
+ */
1593
+ shouldDebounce(node, now) {
1594
+ const policy = node.policy ?? {};
1595
+ return !!(policy.debounceMs &&
1596
+ node.lastScheduledAt &&
1597
+ now - node.lastScheduledAt < policy.debounceMs);
1598
+ }
1599
+ /**
1600
+ * Handle debounced scheduling by replacing the latest queued item
1601
+ */
1602
+ handleDebouncedSchedule(node, nodeId, now) {
1603
+ const effectiveInputs = this.getEffectiveInputs(nodeId);
1604
+ node.queue.splice(0, node.queue.length);
1605
+ node.runSeq += 1;
1606
+ const rid = `${nodeId}:${node.runSeq}:${now}`;
1607
+ node.queue.push({ runId: rid, inputs: effectiveInputs });
1608
+ }
1609
+ /**
1610
+ * Prepare execution plan with all necessary information
1611
+ */
1612
+ prepareExecutionPlan(node, nodeId, runContextIds, now) {
1613
+ node.lastScheduledAt = now;
1614
+ node.runSeq += 1;
1615
+ const runId = `${nodeId}:${node.runSeq}:${now}`;
1616
+ node.latestRunId = runId;
1617
+ const effectiveInputs = this.getEffectiveInputs(nodeId);
1618
+ // Take a shallow snapshot of the current policy for this run
1619
+ const policySnapshot = node.policy ? { ...node.policy } : undefined;
1620
+ return {
1621
+ runId,
1622
+ effectiveInputs,
1623
+ runContextIdsForRun: runContextIds,
1624
+ timestamp: now,
1625
+ policy: policySnapshot,
1626
+ };
1627
+ }
1628
+ /**
1629
+ * Route execution to appropriate concurrency handler
1630
+ */
1631
+ routeToConcurrencyHandler(node, nodeId, plan) {
1632
+ const mode = plan.policy?.asyncConcurrency ?? "switch";
1633
+ switch (mode) {
1634
+ case "drop":
1635
+ this.handleDropMode(node, nodeId, plan);
1636
+ break;
1637
+ case "queue":
1638
+ this.handleQueueMode(node, nodeId, plan);
1639
+ break;
1640
+ case "switch":
1641
+ case "merge":
1642
+ default:
1643
+ this.startRun(node, nodeId, plan);
1644
+ break;
1645
+ }
1646
+ }
1647
+ /**
1648
+ * Handle drop mode - drop execution if node is already running, otherwise start run
1649
+ */
1650
+ handleDropMode(node, nodeId, plan) {
1651
+ // Drop if node is already running
1652
+ if (node.activeControllers.size > 0) {
1653
+ return; // Don't increment pendingCount if we're dropping this run
1654
+ }
1655
+ // Start run if node is not running
1656
+ this.startRun(node, nodeId, plan);
1657
+ }
1658
+ /**
1659
+ * Handle queue mode - add to queue and process sequentially
1660
+ */
1661
+ handleQueueMode(node, nodeId, plan) {
1662
+ const maxQ = plan.policy?.maxQueue ?? 8;
1663
+ node.queue.push({ runId: plan.runId, inputs: plan.effectiveInputs });
1664
+ if (node.queue.length > maxQ)
1665
+ node.queue.shift();
1666
+ this.processQueue(node, nodeId);
1667
+ }
1668
+ /**
1669
+ * Process queued executions sequentially
1670
+ */
1671
+ processQueue(node, nodeId) {
1672
+ const processNext = () => {
1673
+ if (node.activeControllers.size > 0)
1674
+ return;
1675
+ const next = node.queue.shift();
1676
+ if (!next)
1677
+ return;
1678
+ node.latestRunId = next.runId;
1679
+ const policySnapshot = node.policy ? { ...node.policy } : undefined;
1680
+ const plan = {
1681
+ runId: next.runId,
1682
+ effectiveInputs: next.inputs,
1683
+ runContextIdsForRun: node.activeRunContextIds,
1684
+ timestamp: Date.now(),
1685
+ policy: policySnapshot,
1686
+ };
1687
+ this.startRun(node, nodeId, plan, () => {
1688
+ setTimeout(processNext, 0);
1689
+ });
1690
+ };
1691
+ processNext();
1692
+ }
1693
+ /**
1694
+ * Start a node execution run
1695
+ */
1696
+ startRun(node, nodeId, plan, onDone) {
1697
+ // Track run-contexts
1698
+ this.trackRunContextStart(nodeId, plan.runContextIdsForRun);
1699
+ // Setup execution controller
1700
+ const controller = this.createExecutionController(node, plan.runId);
1701
+ // Handle concurrency mode
1702
+ this.applyConcurrencyMode(node, controller, plan);
1703
+ // Setup timeout if needed
1704
+ const timeoutId = this.setupTimeout(node, controller, plan);
1705
+ // Create execution context
1706
+ const ctx = this.createExecutionContext(nodeId, node, plan.effectiveInputs, plan.runId, controller.signal, plan.runContextIdsForRun, this.createEmitAndProgressHandlers(node, nodeId, plan));
1707
+ // Execute
1708
+ this.executeNode(node, nodeId, ctx, plan, controller, timeoutId, onDone);
1709
+ }
1710
+ /**
1711
+ * Track run-context start for pending nodes
1712
+ */
1713
+ trackRunContextStart(nodeId, runContextIdsForRun) {
1714
+ if (runContextIdsForRun && runContextIdsForRun.size > 0) {
1715
+ for (const id of runContextIdsForRun) {
1716
+ this.runContextManager.startNodeRun(id, nodeId);
1717
+ }
1718
+ }
1719
+ }
1720
+ /**
1721
+ * Create execution controller and update node stats
1722
+ */
1723
+ createExecutionController(node, runId) {
1724
+ const controller = new AbortController();
1725
+ node.stats.runs += 1;
1726
+ node.stats.active += 1;
1727
+ node.stats.lastStartAt = Date.now();
1728
+ node.stats.progress = 0;
1729
+ node.activeControllers.add(controller);
1730
+ node.controllerRunIds.set(controller, runId);
1731
+ return controller;
1732
+ }
1733
+ /**
1734
+ * Apply concurrency mode (switch mode aborts other controllers)
1735
+ */
1736
+ applyConcurrencyMode(node, controller, plan) {
1737
+ const mode = plan.policy?.asyncConcurrency ?? "switch";
1738
+ if (mode === "switch") {
1739
+ for (const c of Array.from(node.activeControllers)) {
1740
+ if (c !== controller)
1741
+ c.abort("switch");
1742
+ }
1743
+ }
1744
+ }
1745
+ /**
1746
+ * Setup timeout for execution if configured
1747
+ */
1748
+ setupTimeout(node, controller, plan) {
1749
+ const policy = plan.policy ?? {};
1750
+ if (policy.timeoutMs && policy.timeoutMs > 0) {
1751
+ return setTimeout(() => controller.abort("timeout"), policy.timeoutMs);
1752
+ }
1753
+ return undefined;
1754
+ }
1755
+ /**
1756
+ * Create emit and progress handlers for execution context
1757
+ */
1758
+ createEmitAndProgressHandlers(node, nodeId, plan) {
1759
+ const policy = plan.policy ?? {};
1760
+ return {
1761
+ emitHandler: (handle, value) => {
1762
+ const m = policy.asyncConcurrency ?? "switch";
1763
+ // Drop emits from runs that were explicitly cancelled due to a
1764
+ // snapshot/undo/redo operation, regardless of asyncConcurrency.
1765
+ if (node.snapshotCancelledRunIds?.has(plan.runId))
1766
+ return;
1767
+ if (m !== "merge" && plan.runId !== node.latestRunId)
1768
+ return;
1769
+ this.edgePropagator.propagate(nodeId, handle, value, plan.runContextIdsForRun);
1770
+ },
1771
+ reportProgress: (p) => {
1772
+ node.stats.progress = Math.max(0, Math.min(1, Number(p) || 0));
1773
+ this.eventEmitter.emit("stats", {
1774
+ kind: "node-progress",
1775
+ nodeId,
1776
+ typeId: node.typeId,
1777
+ runId: plan.runId,
1778
+ progress: node.stats.progress,
1779
+ });
1780
+ },
1781
+ };
1782
+ }
1783
+ /**
1784
+ * Execute the node with retry logic and cleanup
1785
+ */
1786
+ executeNode(node, nodeId, ctx, plan, controller, timeoutId, onDone) {
1787
+ // Fire node-start event
1788
+ this.eventEmitter.emit("stats", {
1789
+ kind: "node-start",
1790
+ nodeId,
1791
+ typeId: node.typeId,
1792
+ runId: plan.runId,
1793
+ });
1794
+ ctx.log("debug", "node-start");
1795
+ const exec = async (attempt) => {
1796
+ let hadError = false;
1797
+ try {
1798
+ if (node.lifecycle?.prepare) {
1799
+ ctx.log("debug", "prepare-start");
1800
+ node.lifecycle.prepare(node.params ?? {}, ctx);
1801
+ ctx.log("debug", "prepare-done");
1802
+ }
1803
+ await node.runtime.onInputsChanged?.(plan.effectiveInputs, ctx);
1804
+ }
1805
+ catch (err) {
1806
+ // Suppress errors caused by expected cancellations
1807
+ if (controller.signal.aborted) {
1808
+ const reason = controller.signal.reason;
1809
+ if (reason === "switch" ||
1810
+ reason === "snapshot" ||
1811
+ reason === "node-deleted" ||
1812
+ reason === "user-cancelled") {
1813
+ return; // Cancellation events are emitted separately, skip error handling
1814
+ }
1815
+ }
1816
+ hadError = true;
1817
+ node.stats.lastError = err;
1818
+ const retry = plan.policy?.retry;
1819
+ if (retry && attempt < (retry.attempts ?? 0)) {
1820
+ const delay = retry.backoffMs ? retry.backoffMs(attempt) : 0;
1821
+ await new Promise((r) => setTimeout(r, delay));
1822
+ return exec(attempt + 1);
1823
+ }
1824
+ this.eventEmitter.emit("error", {
1825
+ kind: "node-run",
1826
+ nodeId,
1827
+ runId: plan.runId,
1828
+ err,
1829
+ });
1830
+ }
1831
+ finally {
1832
+ this.cleanupExecution(node, nodeId, ctx, plan, controller, timeoutId, hadError, onDone);
1833
+ }
1834
+ };
1835
+ exec(0);
1836
+ }
1837
+ /**
1838
+ * Cleanup after execution completes
1341
1839
  */
1342
- scheduleInputsChanged(nodeId) {
1343
- this.scheduleInputsChangedInternal(nodeId);
1344
- }
1345
- /**
1346
- * Schedule a node for execution with run-context IDs
1347
- */
1348
- scheduleInputsChangedWithRunContexts(nodeId, runContextIds) {
1349
- this.scheduleInputsChangedInternal(nodeId, runContextIds);
1350
- }
1351
- /**
1352
- * Internal method for scheduling (also used by GraphRuntime)
1353
- */
1354
- scheduleInputsChangedInternal(nodeId, runContextIds) {
1355
- const node = this.graphStructure.getNode(nodeId);
1356
- if (!node)
1357
- return;
1358
- if (this.runtimeCoordinator.isPaused())
1359
- return;
1360
- // If run-context IDs are provided, attach them to the node
1361
- if (runContextIds) {
1362
- for (const id of runContextIds) {
1363
- node.activeRunContexts.add(id);
1840
+ cleanupExecution(node, nodeId, ctx, plan, controller, timeoutId, hadError, onDone) {
1841
+ // Decrement pendingNodes count for all relevant run-contexts
1842
+ if (plan.runContextIdsForRun && plan.runContextIdsForRun.size > 0) {
1843
+ for (const id of plan.runContextIdsForRun) {
1844
+ this.runContextManager.finishNodeRun(id, nodeId);
1364
1845
  }
1365
1846
  }
1366
- const now = Date.now();
1367
- const policy = node.policy ?? {};
1368
- // Compute effective inputs (real inputs + defaults) for this execution
1369
- const effectiveInputs = this.getEffectiveInputs(nodeId);
1370
- if (policy.debounceMs &&
1371
- node.lastScheduledAt &&
1372
- now - node.lastScheduledAt < policy.debounceMs) {
1373
- // debounce: replace latest queued
1374
- node.queue.splice(0, node.queue.length);
1375
- node.runSeq += 1;
1376
- const rid = `${nodeId}:${node.runSeq}:${now}`;
1377
- node.queue.push({ runId: rid, inputs: effectiveInputs });
1847
+ // Skip cleanup if node was deleted (cleanup already handled)
1848
+ if (!this.graph.hasNode(nodeId)) {
1378
1849
  return;
1379
1850
  }
1380
- node.lastScheduledAt = now;
1381
- node.runSeq += 1;
1382
- const rid = `${nodeId}:${node.runSeq}:${now}`;
1383
- node.latestRunId = rid;
1384
- // Get run-context IDs for this node (use provided or from node's activeRunContexts)
1385
- const runContextIdsForRun = runContextIds ||
1386
- (node.activeRunContexts.size > 0
1387
- ? new Set(node.activeRunContexts)
1388
- : undefined);
1389
- const startRun = (runId, capturedInputs, runContextIdsForRun, onDone) => {
1390
- // Increment pending count for all relevant run-contexts (node will run)
1391
- if (runContextIdsForRun && runContextIdsForRun.size > 0) {
1392
- for (const id of runContextIdsForRun) {
1393
- const ctx = this.runContextManager.getRunContext(id);
1394
- if (ctx)
1395
- ctx.pending++;
1396
- }
1397
- }
1398
- const controller = new AbortController();
1399
- node.stats.runs += 1;
1400
- node.stats.active += 1;
1401
- node.stats.lastStartAt = now;
1402
- node.stats.progress = 0;
1403
- node.activeControllers.add(controller);
1404
- node.controllerRunIds.set(controller, runId);
1405
- const mode = policy.asyncConcurrency ?? "switch";
1406
- if (mode === "switch") {
1407
- for (const c of Array.from(node.activeControllers)) {
1408
- if (c !== controller)
1409
- c.abort("switch");
1410
- }
1411
- }
1412
- let timeoutId;
1413
- if (policy.timeoutMs && policy.timeoutMs > 0) {
1414
- timeoutId = setTimeout(() => controller.abort("timeout"), policy.timeoutMs);
1415
- }
1416
- const ctx = this.createExecutionContext(nodeId, node, capturedInputs, runId, controller.signal, runContextIdsForRun, {
1417
- emitHandler: (handle, value) => {
1418
- const m = policy.asyncConcurrency ?? "switch";
1419
- // Drop emits from runs that were explicitly cancelled due to a
1420
- // snapshot/undo/redo operation, regardless of asyncConcurrency.
1421
- if (node.snapshotCancelledRunIds?.has(runId))
1422
- return;
1423
- if (m !== "merge" && runId !== node.latestRunId)
1424
- return;
1425
- this.valuePropagator.propagate(nodeId, handle, value, runContextIdsForRun);
1426
- },
1427
- reportProgress: (p) => {
1428
- node.stats.progress = Math.max(0, Math.min(1, Number(p) || 0));
1429
- this.eventEmitter.emit("stats", {
1430
- kind: "node-progress",
1431
- nodeId,
1432
- typeId: node.typeId,
1433
- runId,
1434
- progress: node.stats.progress,
1435
- });
1436
- },
1437
- });
1438
- const exec = async (attempt) => {
1439
- let hadError = false;
1440
- try {
1441
- if (node.lifecycle?.prepare) {
1442
- ctx.log("debug", "prepare-start");
1443
- node.lifecycle.prepare(node.params ?? {}, ctx);
1444
- ctx.log("debug", "prepare-done");
1445
- }
1446
- await node.runtime.onInputsChanged?.(capturedInputs, ctx);
1447
- }
1448
- catch (err) {
1449
- // Suppress errors caused by expected cancellations
1450
- if (controller.signal.aborted) {
1451
- const reason = controller.signal.reason;
1452
- if (reason === "switch" ||
1453
- reason === "snapshot" ||
1454
- reason === "node-deleted" ||
1455
- reason === "user-cancelled") {
1456
- return; // Cancellation events are emitted separately, skip error handling
1457
- }
1458
- }
1459
- hadError = true;
1460
- node.stats.lastError = err;
1461
- const retry = policy.retry;
1462
- if (retry && attempt < (retry.attempts ?? 0)) {
1463
- const delay = retry.backoffMs ? retry.backoffMs(attempt) : 0;
1464
- await new Promise((r) => setTimeout(r, delay));
1465
- return exec(attempt + 1);
1466
- }
1467
- this.eventEmitter.emit("error", {
1468
- kind: "node-run",
1469
- nodeId,
1470
- runId,
1471
- err,
1472
- });
1473
- }
1474
- finally {
1475
- // Skip cleanup if node was deleted (cleanup already handled)
1476
- if (!this.graphStructure.hasNode(nodeId)) {
1477
- return;
1478
- }
1479
- if (timeoutId)
1480
- clearTimeout(timeoutId);
1481
- node.activeControllers.delete(controller);
1482
- node.controllerRunIds.delete(controller);
1483
- node.stats.active = Math.max(0, node.activeControllers.size);
1484
- node.stats.lastEndAt = Date.now();
1485
- node.stats.lastDurationMs =
1486
- node.stats.lastStartAt && node.stats.lastEndAt
1487
- ? node.stats.lastEndAt - node.stats.lastStartAt
1488
- : undefined;
1489
- if (!hadError)
1490
- node.stats.lastError = undefined;
1491
- // Only emit node-done if not cancelled (cancellation events emitted separately)
1492
- const isCancelled = controller.signal.aborted &&
1493
- (controller.signal.reason === "snapshot" ||
1494
- controller.signal.reason === "node-deleted" ||
1495
- controller.signal.reason === "user-cancelled");
1496
- if (!isCancelled) {
1497
- this.eventEmitter.emit("stats", {
1498
- kind: "node-done",
1499
- nodeId,
1500
- typeId: node.typeId,
1501
- runId,
1502
- durationMs: node.stats.lastDurationMs,
1503
- });
1504
- }
1505
- ctx.log("debug", "node-done", {
1506
- durationMs: node.stats.lastDurationMs,
1507
- hadError,
1508
- });
1509
- // Decrement pending count for all relevant run-contexts
1510
- if (runContextIdsForRun && runContextIdsForRun.size > 0) {
1511
- for (const id of runContextIdsForRun) {
1512
- const ctx = this.runContextManager.getRunContext(id);
1513
- if (ctx) {
1514
- ctx.pending--;
1515
- if (ctx.pending === 0) {
1516
- this.runContextManager.finishRunContext(id, this.graphStructure.getNodes());
1517
- }
1518
- }
1519
- }
1520
- }
1521
- if (onDone)
1522
- onDone();
1523
- }
1524
- };
1525
- // Fire node-start event
1851
+ if (timeoutId)
1852
+ clearTimeout(timeoutId);
1853
+ node.activeControllers.delete(controller);
1854
+ node.controllerRunIds.delete(controller);
1855
+ node.stats.active = Math.max(0, node.activeControllers.size);
1856
+ node.stats.lastEndAt = Date.now();
1857
+ node.stats.lastDurationMs =
1858
+ node.stats.lastStartAt && node.stats.lastEndAt
1859
+ ? node.stats.lastEndAt - node.stats.lastStartAt
1860
+ : undefined;
1861
+ if (!hadError)
1862
+ node.stats.lastError = undefined;
1863
+ // Only emit node-done if not cancelled (cancellation events emitted separately)
1864
+ const isCancelled = controller.signal.aborted &&
1865
+ (controller.signal.reason === "snapshot" ||
1866
+ controller.signal.reason === "node-deleted" ||
1867
+ controller.signal.reason === "user-cancelled");
1868
+ if (!isCancelled) {
1526
1869
  this.eventEmitter.emit("stats", {
1527
- kind: "node-start",
1870
+ kind: "node-done",
1528
1871
  nodeId,
1529
1872
  typeId: node.typeId,
1530
- runId,
1873
+ runId: plan.runId,
1874
+ durationMs: node.stats.lastDurationMs,
1531
1875
  });
1532
- ctx.log("debug", "node-start");
1533
- exec(0);
1534
- };
1535
- const mode = policy.asyncConcurrency ?? "switch";
1536
- if (mode === "drop" && node.activeControllers.size > 0) {
1537
- // Don't increment pendingCount if we're dropping this run
1538
- return;
1539
- }
1540
- if (mode === "queue") {
1541
- const maxQ = policy.maxQueue ?? 8;
1542
- node.queue.push({ runId: rid, inputs: effectiveInputs });
1543
- if (node.queue.length > maxQ)
1544
- node.queue.shift();
1545
- const processNext = () => {
1546
- if (node.activeControllers.size > 0)
1547
- return;
1548
- const next = node.queue.shift();
1549
- if (!next)
1550
- return;
1551
- node.latestRunId = next.runId;
1552
- // Use node's activeRunContexts for queued items
1553
- const queuedRunContextIds = node.activeRunContexts.size > 0
1554
- ? new Set(node.activeRunContexts)
1555
- : undefined;
1556
- startRun(next.runId, next.inputs, queuedRunContextIds, () => {
1557
- // After finishing, schedule next
1558
- setTimeout(processNext, 0);
1559
- });
1560
- };
1561
- processNext();
1562
- return;
1563
1876
  }
1564
- // switch or merge
1565
- startRun(rid, effectiveInputs, runContextIdsForRun);
1877
+ ctx.log("debug", "node-done", {
1878
+ durationMs: node.stats.lastDurationMs,
1879
+ hadError,
1880
+ });
1881
+ if (onDone)
1882
+ onDone();
1566
1883
  }
1567
1884
  /**
1568
1885
  * Cancel all active runs for a node
@@ -1610,7 +1927,7 @@ class ExecutionScheduler {
1610
1927
  const toCancel = new Set(nodeIds);
1611
1928
  const visited = new Set();
1612
1929
  const queue = [...nodeIds];
1613
- const edges = this.graphStructure.getEdges();
1930
+ const edges = this.graph.getEdges();
1614
1931
  // Collect all downstream nodes to cancel
1615
1932
  for (let i = 0; i < queue.length; i++) {
1616
1933
  const nodeId = queue[i];
@@ -1629,7 +1946,7 @@ class ExecutionScheduler {
1629
1946
  }
1630
1947
  // Cancel runs for all affected nodes
1631
1948
  for (const nodeId of toCancel) {
1632
- const node = this.graphStructure.getNode(nodeId);
1949
+ const node = this.graph.getNode(nodeId);
1633
1950
  if (!node)
1634
1951
  continue;
1635
1952
  this.cancelNodeActiveRuns(node, reason);
@@ -1640,8 +1957,8 @@ class ExecutionScheduler {
1640
1957
  }
1641
1958
  // Cancel nodes in run-contexts (exclude them from active run-contexts)
1642
1959
  for (const nodeId of toCancel) {
1643
- this.runContextManager.cancelNodeInRunContexts(nodeId, false, // includeDownstream = false (already collected above)
1644
- edges, this.graphStructure.getNodes());
1960
+ // includeDownstream = false (already collected above)
1961
+ this.runContextManager.cancelNodeInRunContexts(nodeId, false);
1645
1962
  }
1646
1963
  }
1647
1964
  }
@@ -1650,45 +1967,30 @@ class ExecutionScheduler {
1650
1967
  class GraphRuntime {
1651
1968
  constructor() {
1652
1969
  // State
1653
- this.paused = false;
1654
1970
  this.environment = {};
1971
+ this.runMode = null;
1972
+ this.pauseRefCount = 0;
1655
1973
  // Initialize components
1656
- this.graphStructure = new GraphStructure();
1974
+ this.graph = new Graph();
1657
1975
  this.eventEmitter = new EventEmitter();
1658
- this.runContextManager = new RunContextManager();
1659
- // Initialize components with interface-based dependencies
1660
- // HandleResolver only needs IRuntimeCoordinator (this)
1661
- this.handleResolver = new HandleResolver(this.graphStructure, this.eventEmitter, this, // IRuntimeCoordinator
1662
- undefined, // registry set later
1663
- undefined // environment set later
1664
- );
1665
- // ValuePropagator needs IHandleResolver and IRuntimeCoordinator
1666
- // ExecutionScheduler needs IValuePropagator - circular dependency!
1667
- // Solution: Create ValuePropagator first (without executionScheduler),
1668
- // then create ExecutionScheduler with ValuePropagator,
1669
- // then wire ValuePropagator with ExecutionScheduler via setExecutionScheduler
1670
- this.valuePropagator = new ValuePropagator(this.graphStructure, this.eventEmitter, this.runContextManager, this.handleResolver, // IHandleResolver
1671
- this // IRuntimeCoordinator
1672
- );
1673
- // Create ExecutionScheduler with ValuePropagator
1674
- this.executionScheduler = new ExecutionScheduler(this.graphStructure, this.eventEmitter, this.runContextManager, this.valuePropagator, // IValuePropagator
1675
- this, // IRuntimeCoordinator
1676
- this.environment);
1677
- // Wire ValuePropagator with ExecutionScheduler to resolve circular dependency
1678
- this.valuePropagator.setExecutionScheduler(this.executionScheduler);
1976
+ this.runContextManager = new RunContextManager(this.graph);
1977
+ this.handleResolver = new HandleResolver(this.graph, this.eventEmitter, this.runContextManager, this);
1978
+ this.edgePropagator = new EdgePropagator(this.graph, this.eventEmitter, this.runContextManager, this.handleResolver, this);
1979
+ // Create NodeExecutor with EdgePropagator
1980
+ this.nodeExecutor = new NodeExecutor(this.graph, this.eventEmitter, this.runContextManager, this, this);
1679
1981
  }
1680
1982
  static create(def, registry, opts) {
1681
1983
  const gr = new GraphRuntime();
1682
1984
  gr.environment = opts?.environment ?? {};
1683
1985
  // Set registry and environment on components
1684
- gr.graphStructure.setRegistry(registry);
1986
+ gr.graph.setRegistry(registry);
1685
1987
  gr.handleResolver.setRegistry(registry);
1686
1988
  gr.handleResolver.setEnvironment(gr.environment);
1687
- gr.executionScheduler.setEnvironment(gr.environment);
1989
+ gr.nodeExecutor.setEnvironment(gr.environment);
1688
1990
  // Precompute per-node resolved handles (use def-provided overrides; do not compute dynamically here)
1689
- const initial = GraphStructure.computeResolvedHandleMap(def, registry, gr.environment);
1690
- for (const [nodeId, handles] of initial.map) {
1691
- gr.graphStructure.setResolvedHandles(nodeId, handles);
1991
+ const initial = tryHandleResolving(def, registry, gr.environment);
1992
+ for (const [nodeId, handles] of initial.resolved) {
1993
+ gr.graph.setResolvedHandles(nodeId, handles);
1692
1994
  }
1693
1995
  // Instantiate nodes
1694
1996
  for (const n of def.nodes) {
@@ -1729,13 +2031,13 @@ class GraphRuntime {
1729
2031
  queued: 0,
1730
2032
  progress: 0,
1731
2033
  },
1732
- activeRunContexts: new Set(),
2034
+ activeRunContextIds: new Set(),
1733
2035
  };
1734
- gr.graphStructure.setNode(n.nodeId, rn);
2036
+ gr.graph.setNode(n.nodeId, rn);
1735
2037
  }
1736
2038
  // Instantiate edges
1737
- const edges = GraphStructure.buildEdges(def, registry, gr.graphStructure.getResolvedHandlesMap());
1738
- gr.graphStructure.setEdges(edges);
2039
+ const edges = buildEdges(def, registry, gr.graph.getResolvedHandlesMap());
2040
+ gr.graph.setEdges(edges);
1739
2041
  // Schedule async recompute only for nodes that indicated Promise-based resolveHandles
1740
2042
  for (const nodeId of initial.pending) {
1741
2043
  gr.handleResolver.scheduleRecomputeHandles(nodeId);
@@ -1746,12 +2048,12 @@ class GraphRuntime {
1746
2048
  return this.eventEmitter.on(event, handler);
1747
2049
  }
1748
2050
  setInputs(nodeId, inputs) {
1749
- const node = this.graphStructure.getNode(nodeId);
2051
+ const node = this.graph.getNode(nodeId);
1750
2052
  if (!node)
1751
2053
  throw new Error(`Node not found: ${nodeId}`);
1752
2054
  let anyChanged = false;
1753
- const edges = this.graphStructure.getEdges();
1754
- const registry = this.graphStructure.getRegistry();
2055
+ const edges = this.graph.getEdges();
2056
+ const registry = this.graph.getRegistry();
1755
2057
  for (const [handle, value] of Object.entries(inputs)) {
1756
2058
  const hasInbound = edges.some((e) => e.target.nodeId === nodeId && e.target.handle === handle);
1757
2059
  if (hasInbound)
@@ -1759,7 +2061,7 @@ class GraphRuntime {
1759
2061
  // Validate input value against declared type
1760
2062
  if (value !== undefined && registry) {
1761
2063
  const desc = registry.nodes.get(node.typeId);
1762
- const resolved = this.graphStructure.getResolvedHandles(nodeId);
2064
+ const resolved = this.graph.getResolvedHandles(nodeId);
1763
2065
  // Get typeId from resolved handles first, then registry statics
1764
2066
  const typeId = resolved
1765
2067
  ? getInputTypeId(resolved.inputs, handle)
@@ -1798,100 +2100,23 @@ class GraphRuntime {
1798
2100
  anyChanged = true;
1799
2101
  }
1800
2102
  }
1801
- if (!this.paused) {
1802
- // Only schedule if all inbound inputs are present (or there are none)
1803
- if (anyChanged && this.executionScheduler.allInboundHaveValue(nodeId))
1804
- this.executionScheduler.scheduleInputsChangedInternal(nodeId);
1805
- // Recompute dynamic handles for this node when its direct inputs change
1806
- if (anyChanged)
1807
- this.handleResolver.scheduleRecomputeHandles(nodeId);
2103
+ // In auto mode, input updates can trigger execution; in manual mode they never should.
2104
+ if (anyChanged) {
2105
+ this.handleResolver.scheduleRecomputeHandles(nodeId);
2106
+ if (this.runMode === "auto" && this.graph.allInboundHaveValue(nodeId)) {
2107
+ this.execute(nodeId);
2108
+ }
1808
2109
  }
1809
2110
  }
1810
2111
  getOutput(nodeId, output) {
1811
- const node = this.graphStructure.getNode(nodeId);
2112
+ const node = this.graph.getNode(nodeId);
1812
2113
  return node?.outputs[output];
1813
2114
  }
1814
- // Update resolved handles for a single node and refresh edge converters/types that touch it
1815
- updateNodeHandles(nodeId, handles, registry) {
1816
- const node = this.graphStructure.getNode(nodeId);
1817
- if (!node)
1818
- return;
1819
- const oldResolved = this.graphStructure.getResolvedHandles(nodeId);
1820
- this.graphStructure.setResolvedHandles(nodeId, handles);
1821
- // Clear outputs that are no longer valid handles
1822
- const oldOutputs = oldResolved?.outputs ?? {};
1823
- const newOutputs = handles.outputs ?? {};
1824
- const oldOutputHandles = new Set(Object.keys(oldOutputs));
1825
- const newOutputHandles = new Set(Object.keys(newOutputs));
1826
- for (const handle of oldOutputHandles) {
1827
- if (!newOutputHandles.has(handle)) {
1828
- // Output handle was removed - clear it and emit undefined to invalidate downstream
1829
- delete node.outputs[handle];
1830
- this.eventEmitter.emit("value", {
1831
- nodeId,
1832
- handle,
1833
- value: undefined,
1834
- io: "output",
1835
- });
1836
- }
1837
- }
1838
- const edges = this.graphStructure.getEdges();
1839
- // Recompute edge converter/type for edges where this node is source or target
1840
- for (const e of edges) {
1841
- const srcNode = this.graphStructure.getNode(e.source.nodeId);
1842
- const dstNode = this.graphStructure.getNode(e.target.nodeId);
1843
- let srcDeclared = e.effectiveTypeId; // Use effectiveTypeId as fallback
1844
- let dstDeclared = e.dstDeclared;
1845
- const oldDstDeclared = dstDeclared; // Track old value to detect resolution
1846
- if (e.source.nodeId === nodeId) {
1847
- const resolved = this.graphStructure.getResolvedHandles(nodeId);
1848
- srcDeclared = resolved
1849
- ? resolved.outputs[e.source.handle]
1850
- : srcDeclared;
1851
- // Update effectiveTypeId if original wasn't explicit
1852
- if (!e.typeId) {
1853
- e.effectiveTypeId = Array.isArray(srcDeclared)
1854
- ? srcDeclared?.[0] ?? "untyped"
1855
- : srcDeclared ?? "untyped";
1856
- }
1857
- }
1858
- if (e.target.nodeId === nodeId) {
1859
- const resolved = this.graphStructure.getResolvedHandles(nodeId);
1860
- if (resolved) {
1861
- dstDeclared = getInputTypeId(resolved.inputs, e.target.handle);
1862
- e.dstDeclared = dstDeclared;
1863
- }
1864
- }
1865
- const conv = GraphStructure.buildEdgeConverters(srcDeclared, dstDeclared, registry, `updateNodeHandles: ${srcNode?.typeId || ""}.${e.source.nodeId}.${e.source.handle} -> ${dstNode?.typeId || ""}.${e.target.nodeId}.${e.target.handle}`);
1866
- e.convert = conv.convert;
1867
- e.convertAsync = conv.convertAsync;
1868
- // If target handle was just resolved (was undefined, now has a type), re-propagate values
1869
- if (e.target.nodeId === nodeId &&
1870
- oldDstDeclared === undefined &&
1871
- dstDeclared !== undefined) {
1872
- const srcNode = this.graphStructure.getNode(e.source.nodeId);
1873
- if (srcNode) {
1874
- const srcValue = srcNode.outputs[e.source.handle];
1875
- if (srcValue !== undefined) {
1876
- // Re-propagate through the now-resolved edge converter
1877
- // Preserve run-contexts if source node has them
1878
- const runContextIds = srcNode.activeRunContexts.size > 0
1879
- ? new Set(srcNode.activeRunContexts)
1880
- : undefined;
1881
- this.valuePropagator.propagate(e.source.nodeId, e.source.handle, srcValue, runContextIds);
1882
- }
1883
- }
1884
- }
1885
- }
1886
- // Re-emit only valid outputs (after clearing removed ones)
1887
- this.valuePropagator.reemitNodeOutputs(nodeId);
1888
- }
1889
2115
  launch(invalidate = false) {
1890
- // call onActivated for nodes that implement it
1891
- for (const node of this.graphStructure.getNodes().values()) {
1892
- const effectiveInputs = this.executionScheduler.getEffectiveInputs(node.nodeId);
2116
+ for (const node of this.graph.getNodes().values()) {
2117
+ const effectiveInputs = this.nodeExecutor.getEffectiveInputs(node.nodeId);
1893
2118
  const ctrl = new AbortController();
1894
- const ctx = this.executionScheduler.createExecutionContext(node.nodeId, node, effectiveInputs, `${node.nodeId}:init`, ctrl.signal);
2119
+ const ctx = this.nodeExecutor.createExecutionContext(node.nodeId, node, effectiveInputs, `${node.nodeId}:init`, ctrl.signal);
1895
2120
  if (node.lifecycle?.prepare) {
1896
2121
  ctx.log("debug", "prepare-start");
1897
2122
  node.lifecycle.prepare(node.params ?? {}, ctx);
@@ -1899,40 +2124,27 @@ class GraphRuntime {
1899
2124
  }
1900
2125
  node.runtime.onActivated?.();
1901
2126
  }
1902
- if (invalidate) {
1903
- // After activation, schedule nodes that have all inbound inputs present
1904
- for (const nodeId of this.graphStructure.getNodes().keys()) {
1905
- if (this.executionScheduler.allInboundHaveValue(nodeId))
1906
- this.executionScheduler.scheduleInputsChangedInternal(nodeId);
2127
+ if (this.runMode === "auto" && invalidate) {
2128
+ for (const nodeId of this.graph.getNodes().keys()) {
2129
+ if (this.graph.allInboundHaveValue(nodeId))
2130
+ this.execute(nodeId);
1907
2131
  }
1908
2132
  }
1909
2133
  }
1910
2134
  triggerExternal(nodeId, event) {
1911
- const node = this.graphStructure.getNode(nodeId);
2135
+ const node = this.graph.getNode(nodeId);
1912
2136
  if (!node)
1913
2137
  return;
1914
- // Forward event to node's onExternalEvent handler for custom actions
1915
2138
  node.runtime.onExternalEvent?.(event, node.state);
1916
2139
  }
1917
- dispose() {
1918
- // Resolve all pending run-context promises before cleanup
1919
- this.runContextManager.resolveAll();
1920
- for (const node of this.graphStructure.getNodes().values()) {
1921
- node.runtime.onDeactivated?.();
1922
- node.runtime.dispose?.();
1923
- node.lifecycle?.dispose?.({
1924
- state: node.state,
1925
- setState: (next) => Object.assign(node.state, next),
1926
- });
1927
- }
1928
- this.graphStructure.clear();
2140
+ cancelNodeRuns(nodeIds) {
2141
+ this.nodeExecutor.cancelNodeRuns(nodeIds);
1929
2142
  }
1930
2143
  getNodeIds() {
1931
- return Array.from(this.graphStructure.getNodes().keys());
2144
+ return Array.from(this.graph.getNodes().keys());
1932
2145
  }
1933
- // Unsafe helpers for serializer: read-only accessors and hydration
1934
2146
  getNodeData(nodeId) {
1935
- const node = this.graphStructure.getNode(nodeId);
2147
+ const node = this.graph.getNode(nodeId);
1936
2148
  if (!node)
1937
2149
  return undefined;
1938
2150
  return {
@@ -1949,16 +2161,14 @@ class GraphRuntime {
1949
2161
  setEnvironment(env) {
1950
2162
  this.environment = { ...env };
1951
2163
  this.handleResolver.setEnvironment(this.environment);
1952
- this.executionScheduler.setEnvironment(this.environment);
1953
- // Recompute dynamic handles for all nodes when environment changes
1954
- for (const nodeId of this.graphStructure.getNodes().keys()) {
2164
+ this.nodeExecutor.setEnvironment(this.environment);
2165
+ for (const nodeId of this.graph.getNodes().keys()) {
1955
2166
  this.handleResolver.scheduleRecomputeHandles(nodeId);
1956
2167
  }
1957
2168
  }
1958
- // Export a GraphDefinition reflecting the current runtime view
1959
2169
  getGraphDef() {
1960
- const nodes = Array.from(this.graphStructure.getNodes().values()).map((n) => {
1961
- const resolved = this.graphStructure.getResolvedHandles(n.nodeId);
2170
+ const nodes = Array.from(this.graph.getNodes().values()).map((n) => {
2171
+ const resolved = this.graph.getResolvedHandles(n.nodeId);
1962
2172
  return {
1963
2173
  nodeId: n.nodeId,
1964
2174
  typeId: n.typeId,
@@ -1966,18 +2176,15 @@ class GraphRuntime {
1966
2176
  resolvedHandles: resolved ? { ...resolved } : undefined,
1967
2177
  };
1968
2178
  });
1969
- const edges = this.graphStructure
1970
- .getEdges()
1971
- .map((e) => ({
2179
+ const edges = this.graph.getEdges().map((e) => ({
1972
2180
  id: e.id,
1973
2181
  source: { nodeId: e.source.nodeId, handle: e.source.handle },
1974
2182
  target: { nodeId: e.target.nodeId, handle: e.target.handle },
1975
- typeId: e.typeId, // Only export original typeId (may be undefined)
2183
+ typeId: e.typeId,
1976
2184
  }));
1977
2185
  return { nodes, edges };
1978
2186
  }
1979
2187
  async whenIdle() {
1980
- // If we have active run-contexts, wait for all of them to complete
1981
2188
  const allRunContexts = this.runContextManager.getAllRunContexts();
1982
2189
  if (allRunContexts.size > 0) {
1983
2190
  await new Promise((resolve) => {
@@ -1993,7 +2200,7 @@ class GraphRuntime {
1993
2200
  });
1994
2201
  }
1995
2202
  const isIdle = () => {
1996
- for (const n of this.graphStructure.getNodes().values()) {
2203
+ for (const n of this.graph.getNodes().values()) {
1997
2204
  if (n.activeControllers.size > 0)
1998
2205
  return false;
1999
2206
  if (n.queue.length > 0)
@@ -2013,97 +2220,51 @@ class GraphRuntime {
2013
2220
  setTimeout(check, 10);
2014
2221
  });
2015
2222
  }
2016
- /**
2017
- * Run this node and optionally all dynamically reachable downstream nodes as a run-context.
2018
- * Includes nodes added later behind the same path (via re-emits).
2019
- * @param startNodeId - The node to start execution from
2020
- * @param options - Execution options
2021
- * @param options.skipPropagateValues - If true, don't set inputs of linked nodes (default: false)
2022
- * @param options.propagate - If false, don't schedule downstream nodes (default: true)
2023
- */
2024
2223
  async runFromHereContext(startNodeId, options) {
2025
- const node = this.graphStructure.getNode(startNodeId);
2026
- if (!node) {
2027
- // Node doesn't exist - resolve immediately
2028
- return;
2029
- }
2030
- const ctx = this.runContextManager.createRunContext(startNodeId, options);
2031
- // Create promise that resolves when context finishes
2032
- const promise = new Promise((resolve) => {
2033
- ctx.resolve = resolve;
2034
- });
2035
- // Temporarily unpause if needed
2036
- const wasPaused = this.paused;
2037
- if (wasPaused) {
2038
- this.paused = false;
2039
- }
2040
- try {
2041
- // Seed the start node with this run-context
2042
- node.activeRunContexts.add(ctx.id);
2043
- this.scheduleInputsChangedWithRunContexts(startNodeId, new Set([ctx.id]));
2044
- await promise;
2045
- }
2046
- finally {
2047
- // Restore pause state if it was paused and no other run-contexts are active
2048
- if (wasPaused && this.runContextManager.getAllRunContexts().size === 0) {
2049
- this.paused = true;
2050
- }
2051
- }
2052
- }
2053
- /**
2054
- * Schedule a node with run-context IDs attached
2055
- */
2056
- scheduleInputsChangedWithRunContexts(nodeId, runContextIds) {
2057
- const node = this.graphStructure.getNode(nodeId);
2224
+ const node = this.graph.getNode(startNodeId);
2058
2225
  if (!node)
2059
2226
  return;
2060
- // Attach run-contexts to the node
2061
- for (const id of runContextIds) {
2062
- node.activeRunContexts.add(id);
2063
- }
2064
- this.executionScheduler.scheduleInputsChangedInternal(nodeId, runContextIds);
2065
- }
2066
- pause() {
2067
- this.paused = true;
2068
- }
2069
- isPaused() {
2070
- return this.paused;
2227
+ return new Promise((resolve) => {
2228
+ const id = this.runContextManager.createRunContext(startNodeId, resolve, options);
2229
+ node.activeRunContextIds.add(id);
2230
+ this.execute(startNodeId, new Set([id]));
2231
+ });
2071
2232
  }
2072
- resume() {
2073
- this.paused = false;
2233
+ setRunMode(runMode) {
2234
+ this.runMode = runMode;
2074
2235
  }
2075
- invalidateDownstream(nodeId) {
2076
- this.valuePropagator.reemitNodeOutputs(nodeId);
2236
+ getRunMode() {
2237
+ return this.runMode;
2077
2238
  }
2078
- scheduleInputsChanged(nodeId) {
2079
- this.executionScheduler.scheduleInputsChangedInternal(nodeId);
2239
+ requestPause() {
2240
+ this.pauseRefCount++;
2241
+ let released = false;
2242
+ return () => {
2243
+ if (released)
2244
+ return;
2245
+ released = true;
2246
+ this.pauseRefCount--;
2247
+ };
2080
2248
  }
2081
- cancelNodeRuns(nodeIds) {
2082
- this.executionScheduler.cancelNodeRuns(nodeIds);
2249
+ isPaused() {
2250
+ return this.pauseRefCount > 0;
2083
2251
  }
2084
2252
  copyOutputs(fromNodeId, toNodeId, options) {
2085
- // Get outputs from source node
2086
2253
  const fromNode = this.getNodeData(fromNodeId);
2087
2254
  if (!fromNode?.outputs)
2088
2255
  return;
2089
- // Copy outputs to target node using hydrate
2090
- // hydrate already pauses internally, so we don't need to handle dry option here
2091
- // reemit: !options?.dry means don't propagate downstream if dry mode
2092
- this.hydrate({ outputs: { [toNodeId]: { ...fromNode.outputs } } }, { reemit: !options?.dry });
2256
+ this.hydrate({ outputs: { [toNodeId]: { ...fromNode.outputs } } }, { invalidate: !options?.dry });
2093
2257
  }
2094
- // Hydrate inputs/outputs without triggering computation; optionally re-emit outputs downstream
2095
2258
  hydrate(payload, opts) {
2096
- const prevPaused = this.paused;
2097
- this.paused = true;
2259
+ const releasePause = this.requestPause();
2098
2260
  try {
2099
2261
  const ins = payload?.inputs || {};
2100
2262
  for (const [nodeId, map] of Object.entries(ins)) {
2101
- const node = this.graphStructure.getNode(nodeId);
2263
+ const node = this.graph.getNode(nodeId);
2102
2264
  if (!node)
2103
2265
  continue;
2104
2266
  for (const [h, v] of Object.entries(map || {})) {
2105
2267
  node.inputs[h] = structuredClone(v);
2106
- // emit input value event
2107
2268
  this.eventEmitter.emit("value", {
2108
2269
  nodeId,
2109
2270
  handle: h,
@@ -2115,12 +2276,11 @@ class GraphRuntime {
2115
2276
  }
2116
2277
  const outs = payload?.outputs || {};
2117
2278
  for (const [nodeId, map] of Object.entries(outs)) {
2118
- const node = this.graphStructure.getNode(nodeId);
2279
+ const node = this.graph.getNode(nodeId);
2119
2280
  if (!node)
2120
2281
  continue;
2121
2282
  for (const [h, v] of Object.entries(map || {})) {
2122
2283
  node.outputs[h] = structuredClone(v);
2123
- // emit output value event
2124
2284
  this.eventEmitter.emit("value", {
2125
2285
  nodeId,
2126
2286
  handle: h,
@@ -2130,238 +2290,245 @@ class GraphRuntime {
2130
2290
  });
2131
2291
  }
2132
2292
  }
2133
- if (opts?.reemit) {
2134
- for (const nodeId of this.graphStructure.getNodes().keys()) {
2135
- this.valuePropagator.reemitNodeOutputs(nodeId);
2293
+ if (opts?.invalidate) {
2294
+ for (const nodeId of this.graph.getNodes().keys()) {
2295
+ this.invalidateDownstream(nodeId);
2136
2296
  }
2137
2297
  }
2138
2298
  }
2139
2299
  finally {
2140
- this.paused = prevPaused;
2300
+ releasePause();
2141
2301
  }
2142
2302
  }
2143
- // Incrementally update nodes/edges to match new definition without full rebuild
2144
2303
  update(def, registry) {
2145
- // Handle node additions and removals
2146
- const desiredIds = new Set(def.nodes.map((n) => n.nodeId));
2147
- const currentIds = new Set(this.graphStructure.getNodes().keys());
2148
- // Remove nodes not present
2149
- for (const nodeId of Array.from(currentIds)) {
2150
- if (!desiredIds.has(nodeId)) {
2151
- const node = this.graphStructure.getNode(nodeId);
2152
- // Cancel all active runs and emit cancellation events
2153
- this.executionScheduler.cancelNodeActiveRuns(node, "node-deleted");
2154
- // Cancel node in all run-contexts (marks it as cancelled and clears activeRunContexts)
2155
- this.runContextManager.cancelNodeInRunContexts(nodeId,
2156
- /* includeDownstream */ true, this.graphStructure.getEdges(), this.graphStructure.getNodes());
2157
- // Check for run-context completion (they may finish if pending reaches 0)
2158
- const allRunContexts = this.runContextManager.getAllRunContexts();
2159
- for (const ctx of Array.from(allRunContexts.values())) {
2160
- if (ctx.pending === 0) {
2161
- this.runContextManager.finishRunContext(ctx.id, this.graphStructure.getNodes());
2162
- }
2304
+ {
2305
+ // Delete nodes that are no longer in the definition
2306
+ const afterIds = new Set(def.nodes.map((n) => n.nodeId));
2307
+ const beforeIds = new Set(this.graph.getNodes().keys());
2308
+ for (const nodeId of Array.from(beforeIds)) {
2309
+ if (!afterIds.has(nodeId)) {
2310
+ const node = this.graph.getNode(nodeId);
2311
+ this.nodeExecutor.cancelNodeActiveRuns(node, "node-deleted");
2312
+ this.runContextManager.cancelNodeInRunContexts(nodeId, true);
2313
+ node.runtime.onDeactivated?.();
2314
+ node.runtime.dispose?.();
2315
+ node.lifecycle?.dispose?.({
2316
+ state: node.state,
2317
+ setState: (next) => Object.assign(node.state, next),
2318
+ });
2319
+ this.graph.deleteNode(nodeId);
2320
+ this.edgePropagator.clearArrayBuckets(nodeId);
2163
2321
  }
2164
- // Cleanup node resources
2165
- node.runtime.onDeactivated?.();
2166
- node.runtime.dispose?.();
2167
- node.lifecycle?.dispose?.({
2168
- state: node.state,
2169
- setState: (next) => Object.assign(node.state, next),
2170
- });
2171
- this.graphStructure.deleteNode(nodeId);
2172
- this.valuePropagator.clearArrayBuckets(nodeId);
2173
2322
  }
2174
2323
  }
2175
- // Add or update existing nodes
2176
- for (const n of def.nodes) {
2177
- const existing = this.graphStructure.getNode(n.nodeId);
2178
- if (!existing) {
2179
- // create new runtime node
2180
- const desc = registry.nodes.get(n.typeId);
2181
- if (!desc)
2182
- throw new Error(`Unknown node type: ${n.typeId}`);
2183
- const cat = registry.categories.get(desc.categoryId);
2184
- if (!cat)
2185
- throw new Error(`Unknown category: ${desc.categoryId}`);
2186
- if (cat.validateImpl)
2187
- cat.validateImpl(desc.impl);
2188
- const runtime = cat.createRuntime({
2189
- nodeId: n.nodeId,
2190
- impl: desc.impl,
2191
- });
2192
- const rn = {
2193
- typeId: n.typeId,
2194
- nodeId: n.nodeId,
2195
- lifecycle: desc.lifecycle,
2196
- inputs: {},
2197
- outputs: {},
2198
- state: {},
2199
- runtime,
2200
- params: n.params,
2201
- policy: {
2202
- ...cat.policy,
2203
- ...desc.policy,
2204
- ...n.params?.policy,
2205
- },
2206
- runSeq: 0,
2207
- activeControllers: new Set(),
2208
- controllerRunIds: new Map(),
2209
- queue: [],
2210
- stats: {
2211
- runs: 0,
2212
- active: 0,
2213
- queued: 0,
2214
- progress: 0,
2215
- },
2216
- activeRunContexts: new Set(),
2217
- };
2218
- this.graphStructure.setNode(n.nodeId, rn);
2219
- // Activate new node
2220
- const effectiveInputs = this.executionScheduler.getEffectiveInputs(rn.nodeId);
2221
- const ctrl = new AbortController();
2222
- const ctx = this.executionScheduler.createExecutionContext(rn.nodeId, rn, effectiveInputs, `${rn.nodeId}:init`, ctrl.signal);
2223
- if (rn.lifecycle?.prepare) {
2224
- ctx.log("debug", "prepare-start");
2225
- rn.lifecycle.prepare(rn.params ?? {}, ctx);
2226
- ctx.log("debug", "prepare-done");
2227
- }
2228
- rn.runtime.onActivated?.();
2229
- }
2230
- else {
2231
- // update params/policy
2232
- existing.params = n.params;
2233
- if (!existing.stats) {
2234
- existing.stats = {
2235
- runs: 0,
2236
- active: 0,
2237
- queued: 0,
2238
- progress: 0,
2324
+ {
2325
+ // Add or update nodes that are in the definition
2326
+ for (const n of def.nodes) {
2327
+ const existing = this.graph.getNode(n.nodeId);
2328
+ if (!existing) {
2329
+ const desc = registry.nodes.get(n.typeId);
2330
+ if (!desc)
2331
+ throw new Error(`Unknown node type: ${n.typeId}`);
2332
+ const cat = registry.categories.get(desc.categoryId);
2333
+ if (!cat)
2334
+ throw new Error(`Unknown category: ${desc.categoryId}`);
2335
+ if (cat.validateImpl)
2336
+ cat.validateImpl(desc.impl);
2337
+ const runtime = cat.createRuntime({
2338
+ nodeId: n.nodeId,
2339
+ impl: desc.impl,
2340
+ });
2341
+ const newNode = {
2342
+ typeId: n.typeId,
2343
+ nodeId: n.nodeId,
2344
+ lifecycle: desc.lifecycle,
2345
+ inputs: {},
2346
+ outputs: {},
2347
+ state: {},
2348
+ runtime,
2349
+ params: n.params,
2350
+ policy: {
2351
+ ...cat.policy,
2352
+ ...desc.policy,
2353
+ ...n.params?.policy,
2354
+ },
2355
+ runSeq: 0,
2356
+ activeControllers: new Set(),
2357
+ controllerRunIds: new Map(),
2358
+ queue: [],
2359
+ stats: {
2360
+ runs: 0,
2361
+ active: 0,
2362
+ queued: 0,
2363
+ progress: 0,
2364
+ },
2365
+ activeRunContextIds: new Set(),
2239
2366
  };
2367
+ this.graph.setNode(n.nodeId, newNode);
2368
+ const effectiveInputs = this.nodeExecutor.getEffectiveInputs(newNode.nodeId);
2369
+ const ctrl = new AbortController();
2370
+ const ctx = this.nodeExecutor.createExecutionContext(newNode.nodeId, newNode, effectiveInputs, `${newNode.nodeId}:init`, ctrl.signal);
2371
+ if (newNode.lifecycle?.prepare) {
2372
+ ctx.log("debug", "prepare-start");
2373
+ newNode.lifecycle.prepare(newNode.params ?? {}, ctx);
2374
+ ctx.log("debug", "prepare-done");
2375
+ }
2376
+ newNode.runtime.onActivated?.();
2377
+ }
2378
+ else {
2379
+ existing.params = n.params;
2380
+ if (!existing.stats) {
2381
+ existing.stats = {
2382
+ runs: 0,
2383
+ active: 0,
2384
+ queued: 0,
2385
+ progress: 0,
2386
+ };
2387
+ }
2240
2388
  }
2241
2389
  }
2242
2390
  }
2243
- // Capture previous inbound map before rebuilding edges
2244
- const edges = this.graphStructure.getEdges();
2245
- const prevInbound = new Map();
2246
- for (const e of edges) {
2247
- const set = prevInbound.get(e.target.nodeId) ?? new Set();
2248
- set.add(e.target.handle);
2249
- prevInbound.set(e.target.nodeId, set);
2250
- }
2251
- // Capture previous per-handle target sets before rebuilding edges
2252
- const prevOutTargets = new Map();
2253
- for (const e of edges) {
2254
- const tmap = prevOutTargets.get(e.source.nodeId) ?? new Map();
2255
- const tset = tmap.get(e.source.handle) ?? new Set();
2256
- tset.add(`${e.target.nodeId}.${e.target.handle}`);
2257
- tmap.set(e.source.handle, tset);
2258
- prevOutTargets.set(e.source.nodeId, tmap);
2259
- }
2260
- // Precompute per-node resolved handles for updated graph (include dynamic)
2261
- const resolved = GraphStructure.computeResolvedHandleMap(def, registry, this.environment);
2262
- // Check which handles changed and emit events for those
2263
- const changedHandles = {};
2264
- for (const [nodeId, newHandles] of resolved.map) {
2265
- const oldHandles = this.graphStructure.getResolvedHandles(nodeId);
2266
- if (!oldHandles ||
2267
- JSON.stringify(oldHandles) !== JSON.stringify(newHandles)) {
2268
- changedHandles[nodeId] = newHandles;
2391
+ {
2392
+ const beforeEdges = this.graph.getEdges();
2393
+ const beforeInbound = new Map();
2394
+ for (const e of beforeEdges) {
2395
+ const set = beforeInbound.get(e.target.nodeId) ?? new Set();
2396
+ set.add(e.target.handle);
2397
+ beforeInbound.set(e.target.nodeId, set);
2269
2398
  }
2270
- }
2271
- // Update resolved handles
2272
- for (const [nodeId, handles] of resolved.map) {
2273
- this.graphStructure.setResolvedHandles(nodeId, handles);
2274
- }
2275
- // Rebuild edges mapping with coercions
2276
- const newEdges = GraphStructure.buildEdges(def, registry, this.graphStructure.getResolvedHandlesMap());
2277
- this.graphStructure.setEdges(newEdges);
2278
- // Build new inbound map
2279
- const nextInbound = new Map();
2280
- const updatedEdges = this.graphStructure.getEdges();
2281
- for (const e of updatedEdges) {
2282
- const set = nextInbound.get(e.target.nodeId) ?? new Set();
2283
- set.add(e.target.handle);
2284
- nextInbound.set(e.target.nodeId, set);
2285
- }
2286
- // For inputs that lost inbound connections, clear and schedule recompute
2287
- for (const [nodeId, prevSet] of prevInbound) {
2288
- const currSet = nextInbound.get(nodeId) ?? new Set();
2289
- const node = this.graphStructure.getNode(nodeId);
2290
- if (!node)
2291
- continue;
2292
- let changed = false;
2293
- for (const handle of Array.from(prevSet)) {
2294
- if (!currSet.has(handle)) {
2295
- if (handle in node.inputs) {
2296
- delete node.inputs[handle];
2297
- changed = true;
2399
+ const beforeOutTargets = new Map();
2400
+ for (const e of beforeEdges) {
2401
+ const tmap = beforeOutTargets.get(e.source.nodeId) ??
2402
+ new Map();
2403
+ const tset = tmap.get(e.source.handle) ?? new Set();
2404
+ tset.add(`${e.target.nodeId}.${e.target.handle}`);
2405
+ tmap.set(e.source.handle, tset);
2406
+ beforeOutTargets.set(e.source.nodeId, tmap);
2407
+ }
2408
+ {
2409
+ // Update handles and edges
2410
+ const result = tryHandleResolving(def, registry, this.environment);
2411
+ const changedHandles = {};
2412
+ for (const [nodeId, newHandles] of result.resolved) {
2413
+ const oldHandles = this.graph.getResolvedHandles(nodeId);
2414
+ if (!oldHandles ||
2415
+ JSON.stringify(oldHandles) !== JSON.stringify(newHandles)) {
2416
+ changedHandles[nodeId] = newHandles;
2298
2417
  }
2299
2418
  }
2419
+ for (const [nodeId, handles] of result.resolved) {
2420
+ this.graph.setResolvedHandles(nodeId, handles);
2421
+ }
2422
+ const afterEdges = buildEdges(def, registry, this.graph.getResolvedHandlesMap());
2423
+ this.graph.setEdges(afterEdges);
2424
+ for (const nodeId of result.pending) {
2425
+ this.handleResolver.scheduleRecomputeHandles(nodeId);
2426
+ }
2427
+ if (Object.keys(changedHandles).length > 0) {
2428
+ this.eventEmitter.emit("invalidate", {
2429
+ reason: "graph-updated",
2430
+ resolvedHandles: changedHandles,
2431
+ });
2432
+ }
2300
2433
  }
2301
- if (changed) {
2302
- // Clear buckets for handles that lost inbound (handled by ValuePropagator)
2303
- this.valuePropagator.clearArrayBuckets(nodeId);
2304
- this.executionScheduler.scheduleInputsChangedInternal(nodeId);
2305
- }
2306
- }
2307
- // Re-emit outputs when per-handle target sets change (precise and simple)
2308
- const nextOutTargets = new Map();
2309
- for (const e of updatedEdges) {
2310
- const tmap = nextOutTargets.get(e.source.nodeId) ?? new Map();
2311
- const tset = tmap.get(e.source.handle) ?? new Set();
2312
- tset.add(`${e.target.nodeId}.${e.target.handle}`);
2313
- tmap.set(e.source.handle, tset);
2314
- nextOutTargets.set(e.source.nodeId, tmap);
2315
- }
2316
- const setsEqualStr = (a, b) => {
2317
- if (!a && !b)
2318
- return true;
2319
- if (!a || !b)
2320
- return false;
2321
- if (a.size !== b.size)
2322
- return false;
2323
- for (const v of a)
2324
- if (!b.has(v))
2325
- return false;
2326
- return true;
2327
- };
2328
- const nodesToCheck = new Set([
2329
- ...Array.from(prevOutTargets.keys()),
2330
- ...Array.from(nextOutTargets.keys()),
2331
- ]);
2332
- for (const nodeId of nodesToCheck) {
2333
- const pmap = prevOutTargets.get(nodeId) ?? new Map();
2334
- const nmap = nextOutTargets.get(nodeId) ?? new Map();
2335
- const handles = new Set([
2336
- ...Array.from(pmap.keys()),
2337
- ...Array.from(nmap.keys()),
2338
- ]);
2339
- for (const handle of handles) {
2340
- const pset = pmap.get(handle) ?? new Set();
2341
- const nset = nmap.get(handle) ?? new Set();
2342
- if (!setsEqualStr(pset, nset)) {
2343
- const val = this.getOutput(nodeId, handle);
2344
- if (val !== undefined)
2345
- this.valuePropagator.propagate(nodeId, handle, val);
2346
- else if (this.executionScheduler.allInboundHaveValue(nodeId))
2347
- this.executionScheduler.scheduleInputsChangedInternal(nodeId);
2434
+ {
2435
+ // Update inputs and propagate changes
2436
+ const afterInbound = new Map();
2437
+ const afterEdges = this.graph.getEdges();
2438
+ for (const e of afterEdges) {
2439
+ const set = afterInbound.get(e.target.nodeId) ?? new Set();
2440
+ set.add(e.target.handle);
2441
+ afterInbound.set(e.target.nodeId, set);
2442
+ }
2443
+ // Propagate changes on edges removed
2444
+ for (const [nodeId, beforeSet] of beforeInbound) {
2445
+ const currSet = afterInbound.get(nodeId) ?? new Set();
2446
+ const node = this.graph.getNode(nodeId);
2447
+ if (!node)
2448
+ continue;
2449
+ let changed = false;
2450
+ for (const handle of Array.from(beforeSet)) {
2451
+ if (!currSet.has(handle)) {
2452
+ if (handle in node.inputs) {
2453
+ delete node.inputs[handle];
2454
+ changed = true;
2455
+ }
2456
+ }
2457
+ }
2458
+ if (changed) {
2459
+ this.edgePropagator.clearArrayBuckets(nodeId);
2460
+ if (this.runMode === "auto" &&
2461
+ this.graph.allInboundHaveValue(nodeId)) {
2462
+ this.execute(nodeId);
2463
+ }
2464
+ }
2465
+ }
2466
+ // Propagate changes on edges added
2467
+ const afterOutTargets = new Map();
2468
+ for (const e of afterEdges) {
2469
+ const targetMap = afterOutTargets.get(e.source.nodeId) ??
2470
+ new Map();
2471
+ const targetSet = targetMap.get(e.source.handle) ?? new Set();
2472
+ targetSet.add(`${e.target.nodeId}.${e.target.handle}`);
2473
+ targetMap.set(e.source.handle, targetSet);
2474
+ afterOutTargets.set(e.source.nodeId, targetMap);
2475
+ }
2476
+ const setsEqual = (a, b) => {
2477
+ if (!a && !b)
2478
+ return true;
2479
+ if (!a || !b)
2480
+ return false;
2481
+ if (a.size !== b.size)
2482
+ return false;
2483
+ for (const v of a)
2484
+ if (!b.has(v))
2485
+ return false;
2486
+ return true;
2487
+ };
2488
+ const nodesToCheck = new Set([
2489
+ ...Array.from(beforeOutTargets.keys()),
2490
+ ...Array.from(afterOutTargets.keys()),
2491
+ ]);
2492
+ for (const nodeId of nodesToCheck) {
2493
+ const beforeMap = beforeOutTargets.get(nodeId) ?? new Map();
2494
+ const afterMap = afterOutTargets.get(nodeId) ?? new Map();
2495
+ const handles = new Set([
2496
+ ...Array.from(beforeMap.keys()),
2497
+ ...Array.from(afterMap.keys()),
2498
+ ]);
2499
+ for (const handle of handles) {
2500
+ const beforeTargetSet = beforeMap.get(handle) ?? new Set();
2501
+ const afterTargetSet = afterMap.get(handle) ?? new Set();
2502
+ if (!setsEqual(beforeTargetSet, afterTargetSet)) {
2503
+ const val = this.getOutput(nodeId, handle);
2504
+ if (val !== undefined)
2505
+ this.propagate(nodeId, handle, val);
2506
+ }
2507
+ }
2348
2508
  }
2349
2509
  }
2350
2510
  }
2351
- // Prune array bucket contributions for edges that no longer exist
2352
- // This is handled by ValuePropagator - array buckets are managed there
2353
- // The buckets will be cleaned up automatically when edges are removed
2354
- // Schedule async recompute for nodes that indicated Promise-based resolveHandles in this update
2355
- // Emit event for changed handles (if any)
2356
- if (Object.keys(changedHandles).length > 0) {
2357
- this.eventEmitter.emit("invalidate", {
2358
- reason: "graph-updated",
2359
- resolvedHandles: changedHandles,
2511
+ }
2512
+ dispose() {
2513
+ this.runContextManager.resolveAll();
2514
+ for (const node of this.graph.getNodes().values()) {
2515
+ node.runtime.onDeactivated?.();
2516
+ node.runtime.dispose?.();
2517
+ node.lifecycle?.dispose?.({
2518
+ state: node.state,
2519
+ setState: (next) => Object.assign(node.state, next),
2360
2520
  });
2361
2521
  }
2362
- for (const nodeId of resolved.pending) {
2363
- this.handleResolver.scheduleRecomputeHandles(nodeId);
2364
- }
2522
+ this.graph.clear();
2523
+ }
2524
+ execute(nodeId, runContextIds) {
2525
+ this.nodeExecutor.execute(nodeId, runContextIds);
2526
+ }
2527
+ propagate(srcNodeId, srcHandle, value, runContextIds) {
2528
+ this.edgePropagator.propagate(srcNodeId, srcHandle, value, runContextIds);
2529
+ }
2530
+ invalidateDownstream(nodeId) {
2531
+ this.edgePropagator.invalidateDownstream(nodeId);
2365
2532
  }
2366
2533
  }
2367
2534
 
@@ -2583,21 +2750,25 @@ class GraphBuilder {
2583
2750
  }
2584
2751
  }
2585
2752
 
2586
- class AbstractEngine {
2587
- constructor(graphRuntime) {
2753
+ /**
2754
+ * Unified Engine implementation that handles both manual and auto run modes.
2755
+ * - Manual mode: Nodes execute only when explicitly called via computeNode/runFromHere (unless paused)
2756
+ * - Auto mode: Nodes automatically execute when inputs change (unless paused)
2757
+ */
2758
+ class LocalEngine {
2759
+ constructor(graphRuntime, runMode) {
2588
2760
  this.graphRuntime = graphRuntime;
2761
+ this.setRunMode(runMode ?? "manual");
2589
2762
  }
2590
2763
  setInputs(nodeId, inputs, options) {
2591
2764
  if (options?.dry) {
2592
- const wasPaused = this.graphRuntime.isPaused();
2593
- if (!wasPaused)
2594
- this.graphRuntime.pause();
2765
+ // Use requestPause to temporarily pause without affecting base run mode
2766
+ const releasePause = this.graphRuntime.requestPause();
2595
2767
  try {
2596
2768
  this.graphRuntime.setInputs(nodeId, inputs);
2597
2769
  }
2598
2770
  finally {
2599
- if (!wasPaused)
2600
- this.graphRuntime.resume();
2771
+ releasePause();
2601
2772
  }
2602
2773
  }
2603
2774
  else {
@@ -2606,15 +2777,13 @@ class AbstractEngine {
2606
2777
  }
2607
2778
  triggerExternal(nodeId, event, options) {
2608
2779
  if (options?.dry) {
2609
- const wasPaused = this.graphRuntime.isPaused();
2610
- if (!wasPaused)
2611
- this.graphRuntime.pause();
2780
+ // Use requestPause to temporarily pause without affecting base run mode
2781
+ const releasePause = this.graphRuntime.requestPause();
2612
2782
  try {
2613
2783
  this.graphRuntime.triggerExternal(nodeId, event);
2614
2784
  }
2615
2785
  finally {
2616
- if (!wasPaused)
2617
- this.graphRuntime.resume();
2786
+ releasePause();
2618
2787
  }
2619
2788
  }
2620
2789
  else {
@@ -2639,19 +2808,6 @@ class AbstractEngine {
2639
2808
  dispose() {
2640
2809
  // this.graphRuntime.dispose();
2641
2810
  }
2642
- }
2643
-
2644
- /**
2645
- * Unified Engine implementation that handles both manual and auto run modes.
2646
- * - Manual mode: Runtime is paused, nodes execute only when explicitly called via computeNode/runFromHere
2647
- * - Auto mode: Runtime is resumed, nodes automatically execute when inputs change
2648
- */
2649
- class UnifiedEngine extends AbstractEngine {
2650
- constructor(graphRuntime, runMode) {
2651
- super(graphRuntime);
2652
- this.runMode = "manual";
2653
- this.setRunMode(runMode ?? "manual");
2654
- }
2655
2811
  launch(invalidate, runMode) {
2656
2812
  if (runMode)
2657
2813
  this.setRunMode(runMode);
@@ -2678,20 +2834,8 @@ class UnifiedEngine extends AbstractEngine {
2678
2834
  async runFromHere(nodeId) {
2679
2835
  await this.graphRuntime.runFromHereContext(nodeId);
2680
2836
  }
2681
- getRunMode() {
2682
- return this.runMode;
2683
- }
2684
2837
  setRunMode(runMode) {
2685
- if (this.runMode === runMode)
2686
- return;
2687
- this.runMode = runMode;
2688
- // Update runtime pause/resume state based on new mode
2689
- if (runMode === "manual") {
2690
- this.graphRuntime.pause();
2691
- }
2692
- else {
2693
- this.graphRuntime.resume();
2694
- }
2838
+ this.graphRuntime.setRunMode(runMode);
2695
2839
  }
2696
2840
  }
2697
2841
 
@@ -4238,6 +4382,58 @@ function setValueAtPath(obj, pathSegments, newValue) {
4238
4382
  }
4239
4383
  return true;
4240
4384
  }
4385
+ /**
4386
+ * Sets a value at a path, creating intermediate objects as needed.
4387
+ * Mutates the root object in place.
4388
+ * @param root - The root object to modify (must be an object, will be initialized if needed)
4389
+ * @param pathSegments - The path segments to traverse
4390
+ * @param value - The value to set, or null to delete the path
4391
+ * @throws Error if path cannot be created (e.g., array indices not supported, invalid parent types)
4392
+ */
4393
+ function setValueAtPathWithCreation(root, pathSegments, value) {
4394
+ if (value === null) {
4395
+ const result = getValueAtPath(root, pathSegments);
4396
+ if (result && result.parent !== null && !Array.isArray(result.parent)) {
4397
+ delete result.parent[result.key];
4398
+ }
4399
+ return;
4400
+ }
4401
+ if (!root || typeof root !== "object" || Array.isArray(root)) {
4402
+ throw new Error("Root must be an object");
4403
+ }
4404
+ let current = root;
4405
+ for (let i = 0; i < pathSegments.length - 1; i++) {
4406
+ const segment = pathSegments[i];
4407
+ if (typeof segment === "string") {
4408
+ if (!current ||
4409
+ typeof current !== "object" ||
4410
+ Array.isArray(current) ||
4411
+ !(segment in current) ||
4412
+ typeof current[segment] !== "object" ||
4413
+ current[segment] === null ||
4414
+ Array.isArray(current[segment])) {
4415
+ if (!current || typeof current !== "object" || Array.isArray(current)) {
4416
+ throw new Error(`Cannot create path: parent at segment ${i} is not an object`);
4417
+ }
4418
+ current[segment] = {};
4419
+ }
4420
+ current = current[segment];
4421
+ }
4422
+ else {
4423
+ throw new Error("Array indices not supported in extData paths");
4424
+ }
4425
+ }
4426
+ const lastSegment = pathSegments[pathSegments.length - 1];
4427
+ if (typeof lastSegment === "string") {
4428
+ if (!current || typeof current !== "object" || Array.isArray(current)) {
4429
+ throw new Error(`Cannot set value: parent at final segment is not an object`);
4430
+ }
4431
+ current[lastSegment] = value;
4432
+ }
4433
+ else {
4434
+ throw new Error("Array indices not supported in extData paths");
4435
+ }
4436
+ }
4241
4437
  function findMatchingPaths(obj, pathSegments, currentPath = []) {
4242
4438
  if (pathSegments.length === 0) {
4243
4439
  return [{ path: currentPath, value: obj }];
@@ -4772,5 +4968,5 @@ function buildValueConverter(config) {
4772
4968
  };
4773
4969
  }
4774
4970
 
4775
- export { BaseCompareOperation, BaseLogicOperation, BaseMathOperation, CompositeCategory, ComputeCategory, GraphBuilder, GraphRuntime, Registry, UnifiedEngine, buildValueConverter, computeGraphCenter, createAsyncGraphDef, createAsyncGraphRegistry, createProgressGraphDef, createProgressGraphRegistry, createSimpleGraphDef, createSimpleGraphRegistry, createValidationGraphDef, createValidationGraphRegistry, findMatchingPaths, generateId, getInputTypeId, getTypedOutputTypeId, getTypedOutputValue, getValueAtPath, installLogging, isInputPrivate, isTypedOutput, mergeGraphDefinitions, mergeInputsOutputs, mergeRuntimeState, mergeSnapshotData, offsetImportedPositions, parseJsonPath, registerDelayNode, registerProgressNodes, setValueAtPath, typed };
4971
+ export { BaseCompareOperation, BaseLogicOperation, BaseMathOperation, CompositeCategory, ComputeCategory, GraphBuilder, GraphRuntime, LocalEngine, Registry, buildValueConverter, computeGraphCenter, createAsyncGraphDef, createAsyncGraphRegistry, createProgressGraphDef, createProgressGraphRegistry, createSimpleGraphDef, createSimpleGraphRegistry, createValidationGraphDef, createValidationGraphRegistry, findMatchingPaths, generateId, getInputTypeId, getTypedOutputTypeId, getTypedOutputValue, getValueAtPath, installLogging, isInputPrivate, isTypedOutput, mergeGraphDefinitions, mergeInputsOutputs, mergeRuntimeState, mergeSnapshotData, offsetImportedPositions, parseJsonPath, registerDelayNode, registerProgressNodes, setValueAtPath, setValueAtPathWithCreation, typed };
4776
4972
  //# sourceMappingURL=index.js.map