@bian-womp/spark-graph 0.3.13 → 0.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cjs/index.cjs CHANGED
@@ -446,6 +446,7 @@ Registry.idCounter = 0;
446
446
 
447
447
  /**
448
448
  * Graph component - manages nodes, edges, and handle resolution
449
+ * This is the ONLY place where nodes, edges, and resolvedByNode are directly updated.
449
450
  */
450
451
  class Graph {
451
452
  constructor(registry) {
@@ -454,46 +455,423 @@ class Graph {
454
455
  this.edges = [];
455
456
  this.resolvedByNode = new Map();
456
457
  }
457
- // Node accessors
458
+ // ==================== Node Accessors ====================
459
+ /**
460
+ * Get a node by ID (readonly to prevent accidental modifications)
461
+ */
458
462
  getNode(nodeId) {
459
463
  return this.nodes.get(nodeId);
460
464
  }
461
- getNodes() {
462
- return this.nodes;
465
+ /**
466
+ * Get a node by ID (mutable, for internal use only)
467
+ * @internal
468
+ */
469
+ getNodeMutable(nodeId) {
470
+ return this.nodes.get(nodeId);
471
+ }
472
+ /**
473
+ * Iterate over all nodes safely (readonly to prevent accidental modifications)
474
+ */
475
+ forEachNode(callback) {
476
+ for (const [nodeId, node] of this.nodes.entries()) {
477
+ callback(node, nodeId);
478
+ }
479
+ }
480
+ /**
481
+ * Get all node IDs
482
+ */
483
+ getNodeIds() {
484
+ return Array.from(this.nodes.keys());
485
+ }
486
+ /**
487
+ * Check if a node exists
488
+ */
489
+ hasNode(nodeId) {
490
+ return this.nodes.has(nodeId);
463
491
  }
492
+ // ==================== Node Mutators ====================
493
+ /**
494
+ * Set a node (creates or replaces)
495
+ */
464
496
  setNode(nodeId, node) {
465
497
  this.nodes.set(nodeId, node);
466
498
  }
499
+ /**
500
+ * Delete a node
501
+ */
467
502
  deleteNode(nodeId) {
468
503
  this.nodes.delete(nodeId);
469
504
  }
470
- hasNode(nodeId) {
471
- return this.nodes.has(nodeId);
505
+ // ==================== Node Property Updates ====================
506
+ /**
507
+ * Update node inputs
508
+ */
509
+ updateNodeInput(nodeId, handle, value) {
510
+ const node = this.getNodeMutable(nodeId);
511
+ if (!node)
512
+ return;
513
+ if (value === undefined) {
514
+ delete node.inputs[handle];
515
+ }
516
+ else {
517
+ node.inputs[handle] = value;
518
+ }
519
+ }
520
+ /**
521
+ * Delete a node input handle
522
+ */
523
+ deleteNodeInput(nodeId, handle) {
524
+ const node = this.nodes.get(nodeId);
525
+ if (!node)
526
+ return;
527
+ delete node.inputs[handle];
528
+ }
529
+ /**
530
+ * Update node outputs
531
+ */
532
+ updateNodeOutput(nodeId, handle, value) {
533
+ const node = this.nodes.get(nodeId);
534
+ if (!node)
535
+ return;
536
+ node.outputs[handle] = value;
537
+ }
538
+ /**
539
+ * Update node state
540
+ */
541
+ updateNodeState(nodeId, updates) {
542
+ const node = this.nodes.get(nodeId);
543
+ if (!node)
544
+ return;
545
+ Object.assign(node.state, updates);
546
+ }
547
+ /**
548
+ * Update node params
549
+ */
550
+ updateNodeParams(nodeId, params) {
551
+ const node = this.nodes.get(nodeId);
552
+ if (!node)
553
+ return;
554
+ node.params = params;
555
+ }
556
+ /**
557
+ * Update node policy
558
+ */
559
+ updateNodePolicy(nodeId, policy) {
560
+ const node = this.nodes.get(nodeId);
561
+ if (!node)
562
+ return;
563
+ node.policy = { ...node.policy, ...policy };
564
+ }
565
+ /**
566
+ * Update node stats
567
+ */
568
+ updateNodeStats(nodeId, updates) {
569
+ const node = this.nodes.get(nodeId);
570
+ if (!node)
571
+ return;
572
+ Object.assign(node.stats, updates);
573
+ }
574
+ /**
575
+ * Increment node runSeq
576
+ */
577
+ incrementNodeRunSeq(nodeId) {
578
+ const node = this.nodes.get(nodeId);
579
+ if (!node)
580
+ return 0;
581
+ node.runSeq += 1;
582
+ return node.runSeq;
583
+ }
584
+ /**
585
+ * Set node latestRunId
586
+ */
587
+ setNodeLatestRunId(nodeId, runId) {
588
+ const node = this.nodes.get(nodeId);
589
+ if (!node)
590
+ return;
591
+ node.latestRunId = runId;
592
+ }
593
+ /**
594
+ * Set node lastScheduledAt
595
+ */
596
+ setNodeLastScheduledAt(nodeId, timestamp) {
597
+ const node = this.nodes.get(nodeId);
598
+ if (!node)
599
+ return;
600
+ node.lastScheduledAt = timestamp;
601
+ }
602
+ /**
603
+ * Update node lastInputAt timestamp for a handle
604
+ */
605
+ updateNodeLastInputAt(nodeId, handle, timestamp) {
606
+ const node = this.nodes.get(nodeId);
607
+ if (!node)
608
+ return;
609
+ if (!node.lastInputAt) {
610
+ node.lastInputAt = {};
611
+ }
612
+ node.lastInputAt[handle] = timestamp;
613
+ }
614
+ /**
615
+ * Set node lastSuccessAt timestamp
616
+ */
617
+ setNodeLastSuccessAt(nodeId, timestamp) {
618
+ const node = this.nodes.get(nodeId);
619
+ if (!node)
620
+ return;
621
+ node.lastSuccessAt = timestamp;
622
+ }
623
+ // ==================== Node Queue Operations ====================
624
+ /**
625
+ * Add item to node queue
626
+ */
627
+ addToNodeQueue(nodeId, item) {
628
+ const node = this.nodes.get(nodeId);
629
+ if (!node)
630
+ return;
631
+ node.queue.push(item);
632
+ }
633
+ /**
634
+ * Remove first item from node queue
635
+ */
636
+ shiftNodeQueue(nodeId) {
637
+ const node = this.nodes.get(nodeId);
638
+ if (!node)
639
+ return undefined;
640
+ return node.queue.shift();
641
+ }
642
+ /**
643
+ * Clear node queue
644
+ */
645
+ clearNodeQueue(nodeId) {
646
+ const node = this.nodes.get(nodeId);
647
+ if (!node)
648
+ return;
649
+ node.queue = [];
650
+ }
651
+ /**
652
+ * Replace node queue
653
+ */
654
+ replaceNodeQueue(nodeId, items) {
655
+ const node = this.nodes.get(nodeId);
656
+ if (!node)
657
+ return;
658
+ node.queue = items;
659
+ }
660
+ // ==================== Node Controller Operations ====================
661
+ /**
662
+ * Add controller to node
663
+ */
664
+ addNodeController(nodeId, controller, runId) {
665
+ const node = this.nodes.get(nodeId);
666
+ if (!node)
667
+ return;
668
+ node.activeControllers.add(controller);
669
+ node.controllerRunIds.set(controller, runId);
670
+ }
671
+ /**
672
+ * Remove controller from node
673
+ */
674
+ removeNodeController(nodeId, controller) {
675
+ const node = this.nodes.get(nodeId);
676
+ if (!node)
677
+ return;
678
+ node.activeControllers.delete(controller);
679
+ node.controllerRunIds.delete(controller);
680
+ }
681
+ /**
682
+ * Clear all controllers from node
683
+ */
684
+ clearNodeControllers(nodeId) {
685
+ const node = this.nodes.get(nodeId);
686
+ if (!node)
687
+ return;
688
+ node.activeControllers.clear();
689
+ node.controllerRunIds.clear();
690
+ }
691
+ /**
692
+ * Get all controllers for a node
693
+ */
694
+ getNodeControllers(nodeId) {
695
+ const node = this.nodes.get(nodeId);
696
+ if (!node)
697
+ return new Set();
698
+ return new Set(node.activeControllers);
699
+ }
700
+ // ==================== Node Run Context Operations ====================
701
+ /**
702
+ * Add run context ID to node
703
+ */
704
+ addNodeRunContextId(nodeId, runContextId) {
705
+ const node = this.nodes.get(nodeId);
706
+ if (!node)
707
+ return;
708
+ node.activeRunContextIds.add(runContextId);
709
+ }
710
+ /**
711
+ * Add multiple run context IDs to node
712
+ */
713
+ addNodeRunContextIds(nodeId, runContextIds) {
714
+ const node = this.nodes.get(nodeId);
715
+ if (!node)
716
+ return;
717
+ for (const id of runContextIds) {
718
+ node.activeRunContextIds.add(id);
719
+ }
720
+ }
721
+ /**
722
+ * Remove run context ID from node
723
+ */
724
+ removeNodeRunContextId(nodeId, runContextId) {
725
+ const node = this.nodes.get(nodeId);
726
+ if (!node)
727
+ return;
728
+ node.activeRunContextIds.delete(runContextId);
729
+ }
730
+ /**
731
+ * Clear all run context IDs from node
732
+ */
733
+ clearNodeRunContextIds(nodeId) {
734
+ const node = this.nodes.get(nodeId);
735
+ if (!node)
736
+ return;
737
+ node.activeRunContextIds.clear();
738
+ }
739
+ /**
740
+ * Get run context IDs for a node
741
+ */
742
+ getNodeRunContextIds(nodeId) {
743
+ const node = this.nodes.get(nodeId);
744
+ if (!node)
745
+ return new Set();
746
+ return new Set(node.activeRunContextIds);
747
+ }
748
+ // ==================== Node Snapshot Cancelled Run IDs ====================
749
+ /**
750
+ * Add snapshot cancelled run ID to node
751
+ */
752
+ addSnapshotCancelledRunId(nodeId, runId) {
753
+ const node = this.nodes.get(nodeId);
754
+ if (!node)
755
+ return;
756
+ if (!node.snapshotCancelledRunIds) {
757
+ node.snapshotCancelledRunIds = new Set();
758
+ }
759
+ node.snapshotCancelledRunIds.add(runId);
760
+ }
761
+ // ==================== Edge Accessors ====================
762
+ /**
763
+ * Iterate over all edges safely
764
+ */
765
+ forEachEdge(callback) {
766
+ this.edges.forEach(callback);
472
767
  }
473
- // Edge accessors
474
- getEdges() {
475
- return this.edges;
768
+ /**
769
+ * Find edges matching a predicate
770
+ */
771
+ findEdges(predicate) {
772
+ return this.edges.filter(predicate);
773
+ }
774
+ /**
775
+ * Get edges by source node and handle
776
+ */
777
+ getEdgesBySource(srcNodeId, srcHandle) {
778
+ return this.edges.filter((e) => e.source.nodeId === srcNodeId && e.source.handle === srcHandle);
779
+ }
780
+ /**
781
+ * Get edges by target node and handle
782
+ */
783
+ getEdgesByTarget(targetNodeId, targetHandle) {
784
+ return this.edges.filter((e) => e.target.nodeId === targetNodeId && e.target.handle === targetHandle);
785
+ }
786
+ /**
787
+ * Get inbound edges for a node
788
+ */
789
+ getInboundEdges(nodeId) {
790
+ return this.edges.filter((e) => e.target.nodeId === nodeId);
476
791
  }
792
+ /**
793
+ * Get outbound edges for a node
794
+ */
795
+ getOutboundEdges(nodeId) {
796
+ return this.edges.filter((e) => e.source.nodeId === nodeId);
797
+ }
798
+ // ==================== Edge Mutators ====================
799
+ /**
800
+ * Set all edges (replaces existing)
801
+ */
477
802
  setEdges(edges) {
478
- this.edges = edges;
803
+ this.edges = [...edges];
479
804
  }
480
- // Registry accessor
805
+ /**
806
+ * Update an edge by ID
807
+ */
808
+ updateEdge(edgeId, updates) {
809
+ const edge = this.edges.find((e) => e.id === edgeId);
810
+ if (!edge)
811
+ return;
812
+ Object.assign(edge, updates);
813
+ }
814
+ /**
815
+ * Update edge properties (convert, convertAsync, types, etc.)
816
+ */
817
+ updateEdgeProperties(edgeId, updates) {
818
+ const edge = this.edges.find((e) => e.id === edgeId);
819
+ if (!edge)
820
+ return;
821
+ if (updates.effectiveTypeId !== undefined) {
822
+ edge.effectiveTypeId = updates.effectiveTypeId;
823
+ }
824
+ if (updates.dstDeclared !== undefined) {
825
+ edge.dstDeclared = updates.dstDeclared;
826
+ }
827
+ if (updates.srcUnionTypes !== undefined) {
828
+ edge.srcUnionTypes = updates.srcUnionTypes;
829
+ }
830
+ if (updates.convert !== undefined) {
831
+ edge.convert = updates.convert;
832
+ }
833
+ if (updates.convertAsync !== undefined) {
834
+ edge.convertAsync = updates.convertAsync;
835
+ }
836
+ }
837
+ /**
838
+ * Update edge stats
839
+ */
840
+ updateEdgeStats(edgeId, updates) {
841
+ const edge = this.edges.find((e) => e.id === edgeId);
842
+ if (!edge)
843
+ return;
844
+ Object.assign(edge.stats, updates);
845
+ }
846
+ /**
847
+ * Get edge by ID
848
+ */
849
+ getEdge(edgeId) {
850
+ return this.edges.find((e) => e.id === edgeId);
851
+ }
852
+ // ==================== Registry Accessors ====================
481
853
  getRegistry() {
482
854
  return this.registry;
483
855
  }
484
856
  setRegistry(registry) {
485
857
  this.registry = registry;
486
858
  }
487
- // Resolved handles accessors
859
+ // ==================== Resolved Handles Accessors ====================
488
860
  getResolvedHandles(nodeId) {
489
861
  return this.resolvedByNode.get(nodeId);
490
862
  }
491
863
  setResolvedHandles(nodeId, handles) {
492
864
  this.resolvedByNode.set(nodeId, handles);
493
865
  }
494
- getResolvedHandlesMap() {
495
- return this.resolvedByNode;
866
+ /**
867
+ * Iterate over resolved handles safely
868
+ */
869
+ forEachResolvedHandles(callback) {
870
+ for (const [nodeId, handles] of this.resolvedByNode.entries()) {
871
+ callback(handles, nodeId);
872
+ }
496
873
  }
874
+ // ==================== Utility Methods ====================
497
875
  /**
498
876
  * Check if all inbound edges for a node have values
499
877
  */
@@ -510,7 +888,10 @@ class Graph {
510
888
  }
511
889
  return true;
512
890
  }
513
- // Clear all data
891
+ // ==================== Clear Operations ====================
892
+ /**
893
+ * Clear all data
894
+ */
514
895
  clear() {
515
896
  this.nodes.clear();
516
897
  this.edges = [];
@@ -650,9 +1031,9 @@ class RunContextManager {
650
1031
  return; // Still has pending work
651
1032
  }
652
1033
  // Clean up activeRunContexts from all nodes
653
- for (const node of this.graph.getNodes().values()) {
654
- node.activeRunContextIds.delete(id);
655
- }
1034
+ this.graph.forEachNode((node) => {
1035
+ this.graph.removeNodeRunContextId(node.nodeId, id);
1036
+ });
656
1037
  this.runContexts.delete(id);
657
1038
  if (ctx.resolve)
658
1039
  ctx.resolve();
@@ -675,7 +1056,7 @@ class RunContextManager {
675
1056
  if (visited.has(cur))
676
1057
  continue;
677
1058
  visited.add(cur);
678
- for (const e of this.graph.getEdges()) {
1059
+ this.graph.forEachEdge((e) => {
679
1060
  if (e.source.nodeId === cur) {
680
1061
  const targetId = e.target.nodeId;
681
1062
  if (!visited.has(targetId)) {
@@ -683,7 +1064,7 @@ class RunContextManager {
683
1064
  queue.push(targetId);
684
1065
  }
685
1066
  }
686
- }
1067
+ });
687
1068
  }
688
1069
  }
689
1070
  // Mark nodes as cancelled in all run-contexts
@@ -694,9 +1075,7 @@ class RunContextManager {
694
1075
  }
695
1076
  // Clear activeRunContexts for cancelled nodes
696
1077
  for (const id of toCancel) {
697
- const node = this.graph.getNode(id);
698
- if (node)
699
- node.activeRunContextIds.clear();
1078
+ this.graph.clearNodeRunContextIds(id);
700
1079
  }
701
1080
  }
702
1081
  /**
@@ -974,13 +1353,14 @@ class HandleResolver {
974
1353
  if (!node)
975
1354
  return;
976
1355
  // Track resolver start for all active run-contexts
977
- if (node.activeRunContextIds && node.activeRunContextIds.size > 0) {
978
- for (const runContextId of node.activeRunContextIds) {
1356
+ const activeRunContextIds = this.graph.getNodeRunContextIds(nodeId);
1357
+ if (activeRunContextIds.size > 0) {
1358
+ for (const runContextId of activeRunContextIds) {
979
1359
  this.runContextManager.startHandleResolution(runContextId, nodeId);
980
1360
  }
981
1361
  }
982
1362
  setTimeout(() => {
983
- void this.recomputeHandlesForNode(nodeId, node.activeRunContextIds);
1363
+ void this.recomputeHandlesForNode(nodeId, activeRunContextIds.size > 0 ? activeRunContextIds : undefined);
984
1364
  }, 0);
985
1365
  }
986
1366
  // Update resolved handles for a single node and refresh edge converters/types that touch it
@@ -991,30 +1371,33 @@ class HandleResolver {
991
1371
  if (!node)
992
1372
  return;
993
1373
  this.graph.setResolvedHandles(nodeId, handles);
994
- const edges = this.graph.getEdges();
995
- const resolvedByNode = this.graph.getResolvedHandlesMap();
996
- for (const e of edges) {
1374
+ const resolvedByNode = new Map();
1375
+ this.graph.forEachResolvedHandles((handles, nodeId) => {
1376
+ resolvedByNode.set(nodeId, handles);
1377
+ });
1378
+ const registry = this.registry; // Store for use in callback
1379
+ this.graph.forEachEdge((e, _index) => {
997
1380
  // Only update edges that touch the changed node
998
1381
  const touchesChangedNode = e.source.nodeId === nodeId || e.target.nodeId === nodeId;
999
1382
  if (!touchesChangedNode)
1000
- continue;
1383
+ return;
1001
1384
  const srcNode = this.graph.getNode(e.source.nodeId);
1002
1385
  const dstNode = this.graph.getNode(e.target.nodeId);
1003
1386
  const oldDstDeclared = e.dstDeclared;
1004
1387
  // Extract edge types using shared helper (handles both source and target updates)
1005
1388
  const { srcDeclared, dstDeclared, effectiveTypeId } = extractEdgeTypes(e.source.nodeId, e.source.handle, e.target.nodeId, e.target.handle, resolvedByNode, e.typeId);
1006
- // Update edge properties
1007
- if (!e.typeId) {
1008
- e.effectiveTypeId = effectiveTypeId;
1009
- }
1010
- e.dstDeclared = dstDeclared;
1011
- e.srcUnionTypes = Array.isArray(srcDeclared)
1012
- ? [...srcDeclared]
1013
- : undefined;
1014
1389
  // Update converters
1015
- const conv = buildEdgeConverters(srcDeclared, dstDeclared, this.registry, `updateNodeHandles: ${srcNode?.typeId || ""}.${e.source.nodeId}.${e.source.handle} -> ${dstNode?.typeId || ""}.${e.target.nodeId}.${e.target.handle}`);
1016
- e.convert = conv.convert;
1017
- e.convertAsync = conv.convertAsync;
1390
+ const conv = buildEdgeConverters(srcDeclared, dstDeclared, registry, `updateNodeHandles: ${srcNode?.typeId || ""}.${e.source.nodeId}.${e.source.handle} -> ${dstNode?.typeId || ""}.${e.target.nodeId}.${e.target.handle}`);
1391
+ // Update edge properties via Graph
1392
+ this.graph.updateEdgeProperties(e.id, {
1393
+ effectiveTypeId: !e.typeId ? effectiveTypeId : undefined,
1394
+ dstDeclared,
1395
+ srcUnionTypes: Array.isArray(srcDeclared)
1396
+ ? [...srcDeclared]
1397
+ : undefined,
1398
+ convert: conv.convert,
1399
+ convertAsync: conv.convertAsync,
1400
+ });
1018
1401
  if (e.target.nodeId === nodeId &&
1019
1402
  oldDstDeclared === undefined &&
1020
1403
  dstDeclared !== undefined) {
@@ -1022,11 +1405,12 @@ class HandleResolver {
1022
1405
  if (srcNode) {
1023
1406
  const srcValue = srcNode.outputs[e.source.handle];
1024
1407
  if (srcValue !== undefined) {
1025
- this.edgePropagator.propagate(e.source.nodeId, e.source.handle, srcValue, srcNode.activeRunContextIds);
1408
+ const activeRunContextIds = this.graph.getNodeRunContextIds(e.source.nodeId);
1409
+ this.edgePropagator.propagate(e.source.nodeId, e.source.handle, srcValue, activeRunContextIds.size > 0 ? activeRunContextIds : undefined);
1026
1410
  }
1027
1411
  }
1028
1412
  }
1029
- }
1413
+ });
1030
1414
  this.edgePropagator.invalidateDownstream(nodeId);
1031
1415
  }
1032
1416
  /**
@@ -1165,12 +1549,11 @@ class EdgePropagator {
1165
1549
  * Set source output value and emit event
1166
1550
  */
1167
1551
  setSourceOutput(srcNodeId, srcHandle, value) {
1168
- const srcNode = this.graph.getNode(srcNodeId);
1169
- if (!srcNode) {
1552
+ if (!this.graph.hasNode(srcNodeId)) {
1170
1553
  // Node was removed (e.g., graph updated) but an async emit arrived late; ignore
1171
1554
  return false;
1172
1555
  }
1173
- srcNode.outputs[srcHandle] = value;
1556
+ this.graph.updateNodeOutput(srcNodeId, srcHandle, value);
1174
1557
  this.eventEmitter.emit("value", {
1175
1558
  nodeId: srcNodeId,
1176
1559
  handle: srcHandle,
@@ -1184,8 +1567,7 @@ class EdgePropagator {
1184
1567
  * Find all outgoing edges from a source node handle
1185
1568
  */
1186
1569
  findOutgoingEdges(srcNodeId, srcHandle) {
1187
- const edges = this.graph.getEdges();
1188
- return edges.filter((e) => e.source.nodeId === srcNodeId && e.source.handle === srcHandle);
1570
+ return this.graph.getEdgesBySource(srcNodeId, srcHandle);
1189
1571
  }
1190
1572
  /**
1191
1573
  * Propagate value to a single edge
@@ -1284,9 +1666,12 @@ class EdgePropagator {
1284
1666
  });
1285
1667
  const controller = new AbortController();
1286
1668
  const startAt = Date.now();
1287
- edge.stats.runs += 1;
1288
- edge.stats.inFlight = true;
1289
- edge.stats.progress = 0;
1669
+ const currentStats = edge.stats;
1670
+ this.graph.updateEdgeStats(edge.id, {
1671
+ runs: currentStats.runs + 1,
1672
+ inFlight: true,
1673
+ progress: 0,
1674
+ });
1290
1675
  edge
1291
1676
  .convertAsync(value, controller.signal)
1292
1677
  .then((converted) => {
@@ -1342,10 +1727,7 @@ class EdgePropagator {
1342
1727
  else if (shouldSetValue && !valueChanged) {
1343
1728
  // Even if value didn't change, update timestamp if we're forcing execution
1344
1729
  const now = Date.now();
1345
- if (!dstNode.lastInputAt) {
1346
- dstNode.lastInputAt = {};
1347
- }
1348
- dstNode.lastInputAt[edge.target.handle] = now;
1730
+ this.graph.updateNodeLastInputAt(edge.target.nodeId, edge.target.handle, now);
1349
1731
  }
1350
1732
  // Schedule downstream execution
1351
1733
  this.executeDownstream(edge.target.nodeId, effectiveRunContexts);
@@ -1371,15 +1753,12 @@ class EdgePropagator {
1371
1753
  }
1372
1754
  forHandle.set(edge.id, toArray(value));
1373
1755
  // Merge all parts for this handle
1374
- const edges = this.graph.getEdges();
1756
+ const targetEdges = this.graph.getEdgesByTarget(edge.target.nodeId, edge.target.handle);
1375
1757
  const merged = [];
1376
- for (const ed of edges) {
1377
- if (ed.target.nodeId === edge.target.nodeId &&
1378
- ed.target.handle === edge.target.handle) {
1379
- const part = forHandle.get(ed.id);
1380
- if (part && part.length)
1381
- merged.push(...part);
1382
- }
1758
+ for (const ed of targetEdges) {
1759
+ const part = forHandle.get(ed.id);
1760
+ if (part && part.length)
1761
+ merged.push(...part);
1383
1762
  }
1384
1763
  return merged;
1385
1764
  }
@@ -1404,12 +1783,8 @@ class EdgePropagator {
1404
1783
  */
1405
1784
  setTargetInput(edge, dstNode, value) {
1406
1785
  const now = Date.now();
1407
- dstNode.inputs[edge.target.handle] = value;
1408
- // Track when this input was set
1409
- if (!dstNode.lastInputAt) {
1410
- dstNode.lastInputAt = {};
1411
- }
1412
- dstNode.lastInputAt[edge.target.handle] = now;
1786
+ this.graph.updateNodeInput(edge.target.nodeId, edge.target.handle, value);
1787
+ this.graph.updateNodeLastInputAt(edge.target.nodeId, edge.target.handle, now);
1413
1788
  this.eventEmitter.emit("value", {
1414
1789
  nodeId: edge.target.nodeId,
1415
1790
  handle: edge.target.handle,
@@ -1449,9 +1824,13 @@ class EdgePropagator {
1449
1824
  * Update edge stats on successful conversion
1450
1825
  */
1451
1826
  updateEdgeStatsOnSuccess(edge, startAt) {
1452
- edge.stats.inFlight = false;
1453
1827
  const duration = Date.now() - startAt;
1454
- edge.stats.lastDurationMs = duration;
1828
+ this.graph.updateEdgeStats(edge.id, {
1829
+ inFlight: false,
1830
+ lastDurationMs: duration,
1831
+ lastEndAt: Date.now(),
1832
+ lastError: undefined,
1833
+ });
1455
1834
  this.eventEmitter.emit("stats", {
1456
1835
  kind: "edge-done",
1457
1836
  edgeId: edge.id,
@@ -1460,15 +1839,15 @@ class EdgePropagator {
1460
1839
  target: { nodeId: edge.target.nodeId, handle: edge.target.handle },
1461
1840
  durationMs: duration,
1462
1841
  });
1463
- edge.stats.lastEndAt = Date.now();
1464
- edge.stats.lastError = undefined;
1465
1842
  }
1466
1843
  /**
1467
1844
  * Handle edge conversion error
1468
1845
  */
1469
1846
  handleEdgeConversionError(edge, err) {
1470
- edge.stats.inFlight = false;
1471
- edge.stats.lastError = err;
1847
+ this.graph.updateEdgeStats(edge.id, {
1848
+ inFlight: false,
1849
+ lastError: err,
1850
+ });
1472
1851
  this.eventEmitter.emit("error", {
1473
1852
  kind: "edge-convert",
1474
1853
  edgeId: edge.id,
@@ -1501,10 +1880,11 @@ class EdgePropagator {
1501
1880
  ? new Set(Object.keys(resolved.outputs))
1502
1881
  : new Set();
1503
1882
  // Use node's activeRunContexts to propagate to new nodes that were added
1883
+ const activeRunContextIds = this.graph.getNodeRunContextIds(nodeId);
1504
1884
  for (const [handle, value] of Object.entries(node.outputs)) {
1505
1885
  // Only re-emit if this handle is still valid
1506
1886
  if (validOutputHandles.has(handle)) {
1507
- this.propagate(nodeId, handle, value, node.activeRunContextIds);
1887
+ this.propagate(nodeId, handle, value, activeRunContextIds.size > 0 ? activeRunContextIds : undefined);
1508
1888
  }
1509
1889
  }
1510
1890
  }
@@ -1565,10 +1945,8 @@ class NodeExecutor {
1565
1945
  // Start with real inputs only (no defaults)
1566
1946
  const effective = { ...node.inputs };
1567
1947
  // Build set of inbound handles (wired inputs)
1568
- const edges = this.graph.getEdges();
1569
- const inbound = new Set(edges
1570
- .filter((e) => e.target.nodeId === nodeId)
1571
- .map((e) => e.target.handle));
1948
+ const inboundEdges = this.graph.getInboundEdges(nodeId);
1949
+ const inbound = new Set(inboundEdges.map((e) => e.target.handle));
1572
1950
  // Apply defaults only for:
1573
1951
  // 1. Unbound handles that have no explicit value
1574
1952
  // 2. Static handles (not dynamically resolved)
@@ -1596,7 +1974,9 @@ class NodeExecutor {
1596
1974
  });
1597
1975
  const reportProgress = options?.reportProgress ??
1598
1976
  ((p) => {
1599
- node.stats.progress = Math.max(0, Math.min(1, Number(p) || 0));
1977
+ this.graph.updateNodeStats(nodeId, {
1978
+ progress: Math.max(0, Math.min(1, Number(p) || 0)),
1979
+ });
1600
1980
  });
1601
1981
  // Create log function that respects node's logLevel
1602
1982
  const log = (level, message, context) => {
@@ -1630,7 +2010,7 @@ class NodeExecutor {
1630
2010
  return {
1631
2011
  nodeId,
1632
2012
  state: node.state,
1633
- setState: (next) => Object.assign(node.state, next),
2013
+ setState: (next) => this.graph.updateNodeState(nodeId, next),
1634
2014
  emit: emitHandler,
1635
2015
  invalidateDownstream: () => {
1636
2016
  this.edgePropagator.invalidateDownstream(nodeId);
@@ -1690,10 +2070,12 @@ class NodeExecutor {
1690
2070
  if (this.runtime.isPaused())
1691
2071
  return;
1692
2072
  // Attach run-context IDs if provided
1693
- this.attachRunContexts(node, runContextIds);
2073
+ if (runContextIds) {
2074
+ this.graph.addNodeRunContextIds(nodeId, runContextIds);
2075
+ }
1694
2076
  // Handle debouncing
1695
2077
  const now = Date.now();
1696
- if (this.shouldDebounce(node, now)) {
2078
+ if (this.shouldDebounce(nodeId, node, now)) {
1697
2079
  this.handleDebouncedSchedule(node, nodeId, now);
1698
2080
  return;
1699
2081
  }
@@ -1702,44 +2084,35 @@ class NodeExecutor {
1702
2084
  // Route to appropriate concurrency handler
1703
2085
  this.routeToConcurrencyHandler(node, nodeId, executionPlan);
1704
2086
  }
1705
- /**
1706
- * Attach run-context IDs to the node
1707
- */
1708
- attachRunContexts(node, runContextIds) {
1709
- if (!runContextIds)
1710
- return;
1711
- node.activeRunContextIds = new Set([
1712
- ...node.activeRunContextIds,
1713
- ...runContextIds,
1714
- ]);
1715
- }
1716
2087
  /**
1717
2088
  * Check if execution should be debounced
1718
2089
  */
1719
- shouldDebounce(node, now) {
2090
+ shouldDebounce(nodeId, node, now) {
1720
2091
  const policy = node.policy ?? {};
2092
+ const lastScheduledAt = node.lastScheduledAt;
1721
2093
  return !!(policy.debounceMs &&
1722
- node.lastScheduledAt &&
1723
- now - node.lastScheduledAt < policy.debounceMs);
2094
+ lastScheduledAt &&
2095
+ now - lastScheduledAt < policy.debounceMs);
1724
2096
  }
1725
2097
  /**
1726
2098
  * Handle debounced scheduling by replacing the latest queued item
1727
2099
  */
1728
2100
  handleDebouncedSchedule(node, nodeId, now) {
1729
2101
  const effectiveInputs = this.getEffectiveInputs(nodeId);
1730
- node.queue.splice(0, node.queue.length);
1731
- node.runSeq += 1;
1732
- const rid = `${nodeId}:${node.runSeq}:${now}`;
1733
- node.queue.push({ runId: rid, inputs: effectiveInputs });
2102
+ const runSeq = this.graph.incrementNodeRunSeq(nodeId);
2103
+ const rid = `${nodeId}:${runSeq}:${now}`;
2104
+ this.graph.replaceNodeQueue(nodeId, [
2105
+ { runId: rid, inputs: effectiveInputs },
2106
+ ]);
1734
2107
  }
1735
2108
  /**
1736
2109
  * Prepare execution plan with all necessary information
1737
2110
  */
1738
2111
  prepareExecutionPlan(node, nodeId, runContextIds, now) {
1739
- node.lastScheduledAt = now;
1740
- node.runSeq += 1;
1741
- const runId = `${nodeId}:${node.runSeq}:${now}`;
1742
- node.latestRunId = runId;
2112
+ this.graph.setNodeLastScheduledAt(nodeId, now);
2113
+ const runSeq = this.graph.incrementNodeRunSeq(nodeId);
2114
+ const runId = `${nodeId}:${runSeq}:${now}`;
2115
+ this.graph.setNodeLatestRunId(nodeId, runId);
1743
2116
  const effectiveInputs = this.getEffectiveInputs(nodeId);
1744
2117
  // Take a shallow snapshot of the current policy for this run
1745
2118
  const policySnapshot = node.policy ? { ...node.policy } : undefined;
@@ -1786,9 +2159,16 @@ class NodeExecutor {
1786
2159
  */
1787
2160
  handleQueueMode(node, nodeId, plan) {
1788
2161
  const maxQ = plan.policy?.maxQueue ?? 8;
1789
- node.queue.push({ runId: plan.runId, inputs: plan.effectiveInputs });
1790
- if (node.queue.length > maxQ)
1791
- node.queue.shift();
2162
+ const currentNode = this.graph.getNode(nodeId);
2163
+ if (!currentNode)
2164
+ return;
2165
+ this.graph.addToNodeQueue(nodeId, {
2166
+ runId: plan.runId,
2167
+ inputs: plan.effectiveInputs,
2168
+ });
2169
+ if (currentNode.queue.length > maxQ) {
2170
+ this.graph.shiftNodeQueue(nodeId);
2171
+ }
1792
2172
  this.processQueue(node, nodeId);
1793
2173
  }
1794
2174
  /**
@@ -1796,17 +2176,21 @@ class NodeExecutor {
1796
2176
  */
1797
2177
  processQueue(node, nodeId) {
1798
2178
  const processNext = () => {
2179
+ const node = this.graph.getNode(nodeId);
2180
+ if (!node)
2181
+ return;
1799
2182
  if (node.activeControllers.size > 0)
1800
2183
  return;
1801
- const next = node.queue.shift();
2184
+ const next = this.graph.shiftNodeQueue(nodeId);
1802
2185
  if (!next)
1803
2186
  return;
1804
- node.latestRunId = next.runId;
2187
+ this.graph.setNodeLatestRunId(nodeId, next.runId);
1805
2188
  const policySnapshot = node.policy ? { ...node.policy } : undefined;
2189
+ const activeRunContextIds = this.graph.getNodeRunContextIds(nodeId);
1806
2190
  const plan = {
1807
2191
  runId: next.runId,
1808
2192
  effectiveInputs: next.inputs,
1809
- runContextIdsForRun: node.activeRunContextIds,
2193
+ runContextIdsForRun: activeRunContextIds.size > 0 ? activeRunContextIds : undefined,
1810
2194
  timestamp: Date.now(),
1811
2195
  policy: policySnapshot,
1812
2196
  };
@@ -1823,9 +2207,9 @@ class NodeExecutor {
1823
2207
  // Track run-contexts
1824
2208
  this.trackRunContextStart(nodeId, plan.runContextIdsForRun);
1825
2209
  // Setup execution controller
1826
- const controller = this.createExecutionController(node, plan.runId);
2210
+ const controller = this.createExecutionController(nodeId, node, plan.runId);
1827
2211
  // Handle concurrency mode
1828
- this.applyConcurrencyMode(node, controller, plan);
2212
+ this.applyConcurrencyMode(nodeId, node, controller, plan);
1829
2213
  // Setup timeout if needed
1830
2214
  const timeoutId = this.setupTimeout(node, controller, plan);
1831
2215
  // Create execution context
@@ -1846,23 +2230,26 @@ class NodeExecutor {
1846
2230
  /**
1847
2231
  * Create execution controller and update node stats
1848
2232
  */
1849
- createExecutionController(node, runId) {
2233
+ createExecutionController(nodeId, node, runId) {
1850
2234
  const controller = new AbortController();
1851
- node.stats.runs += 1;
1852
- node.stats.active += 1;
1853
- node.stats.lastStartAt = Date.now();
1854
- node.stats.progress = 0;
1855
- node.activeControllers.add(controller);
1856
- node.controllerRunIds.set(controller, runId);
2235
+ const now = Date.now();
2236
+ this.graph.updateNodeStats(nodeId, {
2237
+ runs: node.stats.runs + 1,
2238
+ active: node.stats.active + 1,
2239
+ lastStartAt: now,
2240
+ progress: 0,
2241
+ });
2242
+ this.graph.addNodeController(nodeId, controller, runId);
1857
2243
  return controller;
1858
2244
  }
1859
2245
  /**
1860
2246
  * Apply concurrency mode (switch mode aborts other controllers)
1861
2247
  */
1862
- applyConcurrencyMode(node, controller, plan) {
2248
+ applyConcurrencyMode(nodeId, node, controller, plan) {
1863
2249
  const mode = plan.policy?.asyncConcurrency ?? "switch";
1864
2250
  if (mode === "switch") {
1865
- for (const c of Array.from(node.activeControllers)) {
2251
+ const controllers = this.graph.getNodeControllers(nodeId);
2252
+ for (const c of controllers) {
1866
2253
  if (c !== controller)
1867
2254
  c.abort("switch");
1868
2255
  }
@@ -1885,6 +2272,9 @@ class NodeExecutor {
1885
2272
  const policy = plan.policy ?? {};
1886
2273
  return {
1887
2274
  emitHandler: (handle, value) => {
2275
+ const node = this.graph.getNode(nodeId);
2276
+ if (!node)
2277
+ return;
1888
2278
  const m = policy.asyncConcurrency ?? "switch";
1889
2279
  // Drop emits from runs that were explicitly cancelled due to a
1890
2280
  // snapshot/undo/redo operation, regardless of asyncConcurrency.
@@ -1895,13 +2285,17 @@ class NodeExecutor {
1895
2285
  this.edgePropagator.propagate(nodeId, handle, value, plan.runContextIdsForRun);
1896
2286
  },
1897
2287
  reportProgress: (p) => {
1898
- node.stats.progress = Math.max(0, Math.min(1, Number(p) || 0));
2288
+ const progress = Math.max(0, Math.min(1, Number(p) || 0));
2289
+ this.graph.updateNodeStats(nodeId, { progress });
2290
+ const node = this.graph.getNode(nodeId);
2291
+ if (!node)
2292
+ return;
1899
2293
  this.eventEmitter.emit("stats", {
1900
2294
  kind: "node-progress",
1901
2295
  nodeId,
1902
2296
  typeId: node.typeId,
1903
2297
  runId: plan.runId,
1904
- progress: node.stats.progress,
2298
+ progress,
1905
2299
  });
1906
2300
  },
1907
2301
  };
@@ -1940,7 +2334,7 @@ class NodeExecutor {
1940
2334
  }
1941
2335
  }
1942
2336
  hadError = true;
1943
- node.stats.lastError = err;
2337
+ this.graph.updateNodeStats(nodeId, { lastError: err });
1944
2338
  const retry = plan.policy?.retry;
1945
2339
  if (retry && attempt < (retry.attempts ?? 0)) {
1946
2340
  const delay = retry.backoffMs ? retry.backoffMs(attempt) : 0;
@@ -1976,40 +2370,53 @@ class NodeExecutor {
1976
2370
  }
1977
2371
  if (timeoutId)
1978
2372
  clearTimeout(timeoutId);
1979
- node.activeControllers.delete(controller);
1980
- node.controllerRunIds.delete(controller);
1981
- node.stats.active = Math.max(0, node.activeControllers.size);
1982
- node.stats.lastEndAt = Date.now();
1983
- node.stats.lastDurationMs =
1984
- node.stats.lastStartAt && node.stats.lastEndAt
1985
- ? node.stats.lastEndAt - node.stats.lastStartAt
1986
- : undefined;
1987
- if (!hadError)
1988
- node.stats.lastError = undefined;
2373
+ this.graph.removeNodeController(nodeId, controller);
2374
+ const currentNode = this.graph.getNode(nodeId);
2375
+ if (!currentNode)
2376
+ return;
2377
+ const controllers = this.graph.getNodeControllers(nodeId);
2378
+ const lastEndAt = Date.now();
2379
+ const lastDurationMs = currentNode.stats.lastStartAt && lastEndAt
2380
+ ? lastEndAt - currentNode.stats.lastStartAt
2381
+ : undefined;
2382
+ this.graph.updateNodeStats(nodeId, {
2383
+ active: Math.max(0, controllers.size),
2384
+ lastEndAt,
2385
+ lastDurationMs,
2386
+ lastError: hadError ? currentNode.stats.lastError : undefined,
2387
+ });
1989
2388
  // Track successful completion time (for detecting stale inputs)
1990
2389
  const isCancelled = controller.signal.aborted &&
1991
2390
  (controller.signal.reason === "snapshot" ||
1992
2391
  controller.signal.reason === "node-deleted" ||
1993
2392
  controller.signal.reason === "user-cancelled");
1994
2393
  if (!hadError && !isCancelled) {
1995
- node.lastSuccessAt = Date.now();
2394
+ this.graph.setNodeLastSuccessAt(nodeId, Date.now());
1996
2395
  }
1997
2396
  // Only emit node-done if not cancelled (cancellation events emitted separately)
1998
2397
  if (!isCancelled) {
1999
- this.eventEmitter.emit("stats", {
2000
- kind: "node-done",
2001
- nodeId,
2002
- typeId: node.typeId,
2003
- runId: plan.runId,
2004
- durationMs: node.stats.lastDurationMs,
2398
+ if (currentNode) {
2399
+ this.eventEmitter.emit("stats", {
2400
+ kind: "node-done",
2401
+ nodeId,
2402
+ typeId: currentNode.typeId,
2403
+ runId: plan.runId,
2404
+ durationMs: currentNode.stats.lastDurationMs,
2405
+ });
2406
+ }
2407
+ }
2408
+ if (currentNode) {
2409
+ ctx.log("debug", "node-done", {
2410
+ durationMs: currentNode.stats.lastDurationMs,
2411
+ hadError,
2412
+ inputs: currentNode.inputs
2413
+ ? structuredClone(currentNode.inputs)
2414
+ : undefined,
2415
+ outputs: currentNode.outputs
2416
+ ? structuredClone(currentNode.outputs)
2417
+ : undefined,
2005
2418
  });
2006
2419
  }
2007
- ctx.log("debug", "node-done", {
2008
- durationMs: node.stats.lastDurationMs,
2009
- hadError,
2010
- inputs: node.inputs ? structuredClone(node.inputs) : undefined,
2011
- outputs: node.outputs ? structuredClone(node.outputs) : undefined,
2012
- });
2013
2420
  if (onDone)
2014
2421
  onDone();
2015
2422
  }
@@ -2017,22 +2424,24 @@ class NodeExecutor {
2017
2424
  * Cancel all active runs for a node
2018
2425
  */
2019
2426
  cancelNodeActiveRuns(node, reason) {
2020
- for (const controller of Array.from(node.activeControllers)) {
2021
- const runId = node.controllerRunIds.get(controller);
2427
+ const nodeId = node.nodeId;
2428
+ const controllers = this.graph.getNodeControllers(nodeId);
2429
+ for (const controller of controllers) {
2430
+ const currentNode = this.graph.getNode(nodeId);
2431
+ if (!currentNode)
2432
+ continue;
2433
+ const runId = currentNode.controllerRunIds.get(controller);
2022
2434
  if (runId) {
2023
2435
  // Track cancelled runIds for snapshot and user-cancelled operations
2024
2436
  // (to drop emits from cancelled runs)
2025
2437
  if (reason === "snapshot" || reason === "user-cancelled") {
2026
- if (!node.snapshotCancelledRunIds) {
2027
- node.snapshotCancelledRunIds = new Set();
2028
- }
2029
- node.snapshotCancelledRunIds.add(runId);
2438
+ this.graph.addSnapshotCancelledRunId(nodeId, runId);
2030
2439
  }
2031
2440
  // Emit cancellation event
2032
2441
  this.eventEmitter.emit("stats", {
2033
2442
  kind: "node-done",
2034
- nodeId: node.nodeId,
2035
- typeId: node.typeId,
2443
+ nodeId: currentNode.nodeId,
2444
+ typeId: currentNode.typeId,
2036
2445
  runId,
2037
2446
  cancelled: true,
2038
2447
  });
@@ -2044,10 +2453,9 @@ class NodeExecutor {
2044
2453
  // ignore abort errors
2045
2454
  }
2046
2455
  }
2047
- node.activeControllers.clear();
2048
- node.controllerRunIds.clear();
2049
- node.stats.active = 0;
2050
- node.queue = [];
2456
+ this.graph.clearNodeControllers(nodeId);
2457
+ this.graph.updateNodeStats(nodeId, { active: 0 });
2458
+ this.graph.clearNodeQueue(nodeId);
2051
2459
  }
2052
2460
  /**
2053
2461
  * Cancel runs for multiple nodes.
@@ -2059,14 +2467,13 @@ class NodeExecutor {
2059
2467
  const toCancel = new Set(nodeIds);
2060
2468
  const visited = new Set();
2061
2469
  const queue = [...nodeIds];
2062
- const edges = this.graph.getEdges();
2063
2470
  // Collect all downstream nodes to cancel
2064
2471
  for (let i = 0; i < queue.length; i++) {
2065
2472
  const nodeId = queue[i];
2066
2473
  if (visited.has(nodeId))
2067
2474
  continue;
2068
2475
  visited.add(nodeId);
2069
- for (const edge of edges) {
2476
+ this.graph.forEachEdge((edge) => {
2070
2477
  if (edge.source.nodeId === nodeId) {
2071
2478
  const targetId = edge.target.nodeId;
2072
2479
  if (!visited.has(targetId)) {
@@ -2074,7 +2481,7 @@ class NodeExecutor {
2074
2481
  queue.push(targetId);
2075
2482
  }
2076
2483
  }
2077
- }
2484
+ });
2078
2485
  }
2079
2486
  // Cancel runs for all affected nodes
2080
2487
  for (const nodeId of toCancel) {
@@ -2082,10 +2489,10 @@ class NodeExecutor {
2082
2489
  if (!node)
2083
2490
  continue;
2084
2491
  this.cancelNodeActiveRuns(node, reason);
2085
- node.runSeq += 1;
2492
+ const runSeq = this.graph.incrementNodeRunSeq(nodeId);
2086
2493
  const now = Date.now();
2087
2494
  const suffix = reason === "snapshot" ? "snapshot" : "cancelled";
2088
- node.latestRunId = `${nodeId}:${node.runSeq}:${now}:${suffix}`;
2495
+ this.graph.setNodeLatestRunId(nodeId, `${nodeId}:${runSeq}:${now}:${suffix}`);
2089
2496
  }
2090
2497
  // Cancel nodes in run-contexts (exclude them from active run-contexts)
2091
2498
  for (const nodeId of toCancel) {
@@ -2170,7 +2577,11 @@ class GraphRuntime {
2170
2577
  gr.graph.setNode(n.nodeId, rn);
2171
2578
  }
2172
2579
  // Instantiate edges
2173
- const edges = buildEdges(def, registry, gr.graph.getResolvedHandlesMap());
2580
+ const resolvedByNode = new Map();
2581
+ gr.graph.forEachResolvedHandles((handles, nodeId) => {
2582
+ resolvedByNode.set(nodeId, handles);
2583
+ });
2584
+ const edges = buildEdges(def, registry, resolvedByNode);
2174
2585
  gr.graph.setEdges(edges);
2175
2586
  // Schedule async recompute only for nodes that indicated Promise-based resolveHandles
2176
2587
  for (const nodeId of initial.pending) {
@@ -2186,10 +2597,14 @@ class GraphRuntime {
2186
2597
  if (!node)
2187
2598
  throw new Error(`Node not found: ${nodeId}`);
2188
2599
  let anyChanged = false;
2189
- const edges = this.graph.getEdges();
2190
2600
  const registry = this.graph.getRegistry();
2191
2601
  for (const [handle, value] of Object.entries(inputs)) {
2192
- const hasInbound = edges.some((e) => e.target.nodeId === nodeId && e.target.handle === handle);
2602
+ let hasInbound = false;
2603
+ this.graph.forEachEdge((e) => {
2604
+ if (e.target.nodeId === nodeId && e.target.handle === handle) {
2605
+ hasInbound = true;
2606
+ }
2607
+ });
2193
2608
  if (hasInbound)
2194
2609
  continue;
2195
2610
  // Validate input value against declared type
@@ -2223,12 +2638,7 @@ class GraphRuntime {
2223
2638
  const prev = node.inputs[handle];
2224
2639
  const same = valuesEqual(prev, value);
2225
2640
  if (!same) {
2226
- if (value === undefined) {
2227
- delete node.inputs[handle];
2228
- }
2229
- else {
2230
- node.inputs[handle] = value;
2231
- }
2641
+ this.graph.updateNodeInput(nodeId, handle, value);
2232
2642
  // Emit value event for input updates
2233
2643
  this.eventEmitter.emit("value", { nodeId, handle, value, io: "input" });
2234
2644
  anyChanged = true;
@@ -2250,7 +2660,7 @@ class GraphRuntime {
2250
2660
  return node?.outputs[output];
2251
2661
  }
2252
2662
  launch(invalidate = false) {
2253
- for (const node of this.graph.getNodes().values()) {
2663
+ this.graph.forEachNode((node) => {
2254
2664
  const effectiveInputs = this.nodeExecutor.getEffectiveInputs(node.nodeId);
2255
2665
  const ctrl = new AbortController();
2256
2666
  const execCtx = this.nodeExecutor.createExecutionContext(node.nodeId, node, effectiveInputs, `${node.nodeId}:init`, ctrl.signal);
@@ -2260,9 +2670,9 @@ class GraphRuntime {
2260
2670
  execCtx.log("debug", "prepare-done");
2261
2671
  }
2262
2672
  node.runtime.onActivated?.();
2263
- }
2673
+ });
2264
2674
  if (this.runMode === "auto" && invalidate) {
2265
- for (const nodeId of this.graph.getNodes().keys()) {
2675
+ for (const nodeId of this.graph.getNodeIds()) {
2266
2676
  if (this.graph.allInboundHaveValue(nodeId))
2267
2677
  this.execute(nodeId);
2268
2678
  }
@@ -2297,7 +2707,7 @@ class GraphRuntime {
2297
2707
  this.nodeExecutor.cancelNodeRuns(nodeIds);
2298
2708
  }
2299
2709
  getNodeIds() {
2300
- return Array.from(this.graph.getNodes().keys());
2710
+ return this.graph.getNodeIds();
2301
2711
  }
2302
2712
  getNodeData(nodeId) {
2303
2713
  const node = this.graph.getNode(nodeId);
@@ -2318,26 +2728,30 @@ class GraphRuntime {
2318
2728
  this.environment = { ...env };
2319
2729
  this.handleResolver.setEnvironment(this.environment);
2320
2730
  this.nodeExecutor.setEnvironment(this.environment);
2321
- for (const nodeId of this.graph.getNodes().keys()) {
2731
+ for (const nodeId of this.graph.getNodeIds()) {
2322
2732
  this.handleResolver.scheduleRecomputeHandles(nodeId);
2323
2733
  }
2324
2734
  }
2325
2735
  getGraphDef() {
2326
- const nodes = Array.from(this.graph.getNodes().values()).map((n) => {
2736
+ const nodes = [];
2737
+ this.graph.forEachNode((n) => {
2327
2738
  const resolved = this.graph.getResolvedHandles(n.nodeId);
2328
- return {
2739
+ nodes.push({
2329
2740
  nodeId: n.nodeId,
2330
2741
  typeId: n.typeId,
2331
2742
  params: n.params ? { ...n.params } : undefined,
2332
2743
  resolvedHandles: resolved ? { ...resolved } : undefined,
2333
- };
2744
+ });
2745
+ });
2746
+ const edges = [];
2747
+ this.graph.forEachEdge((e) => {
2748
+ edges.push({
2749
+ id: e.id,
2750
+ source: { nodeId: e.source.nodeId, handle: e.source.handle },
2751
+ target: { nodeId: e.target.nodeId, handle: e.target.handle },
2752
+ typeId: e.typeId,
2753
+ });
2334
2754
  });
2335
- const edges = this.graph.getEdges().map((e) => ({
2336
- id: e.id,
2337
- source: { nodeId: e.source.nodeId, handle: e.source.handle },
2338
- target: { nodeId: e.target.nodeId, handle: e.target.handle },
2339
- typeId: e.typeId,
2340
- }));
2341
2755
  return { nodes, edges };
2342
2756
  }
2343
2757
  async whenIdle() {
@@ -2356,13 +2770,13 @@ class GraphRuntime {
2356
2770
  });
2357
2771
  }
2358
2772
  const isIdle = () => {
2359
- for (const n of this.graph.getNodes().values()) {
2360
- if (n.activeControllers.size > 0)
2361
- return false;
2362
- if (n.queue.length > 0)
2363
- return false;
2364
- }
2365
- return true;
2773
+ let idle = true;
2774
+ this.graph.forEachNode((n) => {
2775
+ if (n.activeControllers.size > 0 || n.queue.length > 0) {
2776
+ idle = false;
2777
+ }
2778
+ });
2779
+ return idle;
2366
2780
  };
2367
2781
  if (isIdle())
2368
2782
  return;
@@ -2382,7 +2796,7 @@ class GraphRuntime {
2382
2796
  return;
2383
2797
  return new Promise((resolve) => {
2384
2798
  const id = this.runContextManager.createRunContext(startNodeId, resolve, options);
2385
- node.activeRunContextIds.add(id);
2799
+ this.graph.addNodeRunContextId(startNodeId, id);
2386
2800
  this.execute(startNodeId, new Set([id]));
2387
2801
  });
2388
2802
  }
@@ -2416,38 +2830,38 @@ class GraphRuntime {
2416
2830
  try {
2417
2831
  const ins = payload?.inputs || {};
2418
2832
  for (const [nodeId, map] of Object.entries(ins)) {
2419
- const node = this.graph.getNode(nodeId);
2420
- if (!node)
2833
+ if (!this.graph.hasNode(nodeId))
2421
2834
  continue;
2422
2835
  for (const [h, v] of Object.entries(map || {})) {
2423
- node.inputs[h] = structuredClone(v);
2836
+ const clonedValue = structuredClone(v);
2837
+ this.graph.updateNodeInput(nodeId, h, clonedValue);
2424
2838
  this.eventEmitter.emit("value", {
2425
2839
  nodeId,
2426
2840
  handle: h,
2427
- value: node.inputs[h],
2841
+ value: clonedValue,
2428
2842
  io: "input",
2429
- runtimeTypeId: getTypedOutputTypeId(node.inputs[h]),
2843
+ runtimeTypeId: getTypedOutputTypeId(clonedValue),
2430
2844
  });
2431
2845
  }
2432
2846
  }
2433
2847
  const outs = payload?.outputs || {};
2434
2848
  for (const [nodeId, map] of Object.entries(outs)) {
2435
- const node = this.graph.getNode(nodeId);
2436
- if (!node)
2849
+ if (!this.graph.hasNode(nodeId))
2437
2850
  continue;
2438
2851
  for (const [h, v] of Object.entries(map || {})) {
2439
- node.outputs[h] = structuredClone(v);
2852
+ const clonedValue = structuredClone(v);
2853
+ this.graph.updateNodeOutput(nodeId, h, clonedValue);
2440
2854
  this.eventEmitter.emit("value", {
2441
2855
  nodeId,
2442
2856
  handle: h,
2443
- value: node.outputs[h],
2857
+ value: clonedValue,
2444
2858
  io: "output",
2445
- runtimeTypeId: getTypedOutputTypeId(node.outputs[h]),
2859
+ runtimeTypeId: getTypedOutputTypeId(clonedValue),
2446
2860
  });
2447
2861
  }
2448
2862
  }
2449
2863
  if (opts?.invalidate) {
2450
- for (const nodeId of this.graph.getNodes().keys()) {
2864
+ for (const nodeId of this.graph.getNodeIds()) {
2451
2865
  this.invalidateDownstream(nodeId);
2452
2866
  }
2453
2867
  }
@@ -2460,17 +2874,19 @@ class GraphRuntime {
2460
2874
  {
2461
2875
  // Delete nodes that are no longer in the definition
2462
2876
  const afterIds = new Set(def.nodes.map((n) => n.nodeId));
2463
- const beforeIds = new Set(this.graph.getNodes().keys());
2877
+ const beforeIds = new Set(this.graph.getNodeIds());
2464
2878
  for (const nodeId of Array.from(beforeIds)) {
2465
2879
  if (!afterIds.has(nodeId)) {
2466
2880
  const node = this.graph.getNode(nodeId);
2881
+ if (!node)
2882
+ continue;
2467
2883
  this.nodeExecutor.cancelNodeActiveRuns(node, "node-deleted");
2468
2884
  this.runContextManager.cancelNodeInRunContexts(nodeId, true);
2469
2885
  node.runtime.onDeactivated?.();
2470
2886
  node.runtime.dispose?.();
2471
2887
  node.lifecycle?.dispose?.({
2472
2888
  state: node.state,
2473
- setState: (next) => Object.assign(node.state, next),
2889
+ setState: (next) => this.graph.updateNodeState(node.nodeId, next),
2474
2890
  });
2475
2891
  this.graph.deleteNode(nodeId);
2476
2892
  this.edgePropagator.clearArrayBuckets(nodeId);
@@ -2534,43 +2950,43 @@ class GraphRuntime {
2534
2950
  newNode.runtime.onActivated?.();
2535
2951
  }
2536
2952
  else {
2537
- existing.params = n.params;
2953
+ this.graph.updateNodeParams(n.nodeId, n.params);
2538
2954
  // Re-merge policy when params change (params.policy can override descriptor/category policy)
2539
2955
  const desc = registry.nodes.get(existing.typeId);
2540
2956
  const cat = registry.categories.get(desc?.categoryId ?? "");
2541
- existing.policy = {
2957
+ this.graph.updateNodePolicy(n.nodeId, {
2542
2958
  ...cat?.policy,
2543
2959
  ...desc?.policy,
2544
2960
  ...n.params?.policy,
2545
- };
2961
+ });
2962
+ // Initialize stats if missing
2546
2963
  if (!existing.stats) {
2547
- existing.stats = {
2964
+ this.graph.updateNodeStats(n.nodeId, {
2548
2965
  runs: 0,
2549
2966
  active: 0,
2550
2967
  queued: 0,
2551
2968
  progress: 0,
2552
- };
2969
+ });
2553
2970
  }
2554
2971
  }
2555
2972
  }
2556
2973
  }
2557
2974
  {
2558
- const beforeEdges = this.graph.getEdges();
2559
2975
  const beforeInbound = new Map();
2560
- for (const e of beforeEdges) {
2976
+ const beforeOutTargets = new Map();
2977
+ this.graph.forEachEdge((e) => {
2978
+ // Build beforeInbound map
2561
2979
  const set = beforeInbound.get(e.target.nodeId) ?? new Set();
2562
2980
  set.add(e.target.handle);
2563
2981
  beforeInbound.set(e.target.nodeId, set);
2564
- }
2565
- const beforeOutTargets = new Map();
2566
- for (const e of beforeEdges) {
2982
+ // Build beforeOutTargets map
2567
2983
  const tmap = beforeOutTargets.get(e.source.nodeId) ??
2568
2984
  new Map();
2569
2985
  const tset = tmap.get(e.source.handle) ?? new Set();
2570
2986
  tset.add(`${e.target.nodeId}.${e.target.handle}`);
2571
2987
  tmap.set(e.source.handle, tset);
2572
2988
  beforeOutTargets.set(e.source.nodeId, tmap);
2573
- }
2989
+ });
2574
2990
  {
2575
2991
  // Update handles and edges
2576
2992
  const result = tryHandleResolving(def, registry, this.environment);
@@ -2585,7 +3001,11 @@ class GraphRuntime {
2585
3001
  for (const [nodeId, handles] of result.resolved) {
2586
3002
  this.graph.setResolvedHandles(nodeId, handles);
2587
3003
  }
2588
- const afterEdges = buildEdges(def, registry, this.graph.getResolvedHandlesMap());
3004
+ const resolvedByNode = new Map();
3005
+ this.graph.forEachResolvedHandles((handles, nodeId) => {
3006
+ resolvedByNode.set(nodeId, handles);
3007
+ });
3008
+ const afterEdges = buildEdges(def, registry, resolvedByNode);
2589
3009
  this.graph.setEdges(afterEdges);
2590
3010
  for (const nodeId of result.pending) {
2591
3011
  this.handleResolver.scheduleRecomputeHandles(nodeId);
@@ -2600,23 +3020,22 @@ class GraphRuntime {
2600
3020
  {
2601
3021
  // Update inputs and propagate changes
2602
3022
  const afterInbound = new Map();
2603
- const afterEdges = this.graph.getEdges();
2604
- for (const e of afterEdges) {
3023
+ this.graph.forEachEdge((e) => {
2605
3024
  const set = afterInbound.get(e.target.nodeId) ?? new Set();
2606
3025
  set.add(e.target.handle);
2607
3026
  afterInbound.set(e.target.nodeId, set);
2608
- }
3027
+ });
2609
3028
  // Propagate changes on edges removed
2610
3029
  for (const [nodeId, beforeSet] of beforeInbound) {
2611
3030
  const currSet = afterInbound.get(nodeId) ?? new Set();
2612
- const node = this.graph.getNode(nodeId);
2613
- if (!node)
3031
+ if (!this.graph.hasNode(nodeId))
2614
3032
  continue;
2615
3033
  let changed = false;
2616
3034
  for (const handle of Array.from(beforeSet)) {
2617
3035
  if (!currSet.has(handle)) {
2618
- if (handle in node.inputs) {
2619
- delete node.inputs[handle];
3036
+ const node = this.graph.getNode(nodeId);
3037
+ if (node && handle in node.inputs) {
3038
+ this.graph.deleteNodeInput(nodeId, handle);
2620
3039
  changed = true;
2621
3040
  }
2622
3041
  }
@@ -2631,14 +3050,14 @@ class GraphRuntime {
2631
3050
  }
2632
3051
  // Propagate changes on edges added
2633
3052
  const afterOutTargets = new Map();
2634
- for (const e of afterEdges) {
3053
+ this.graph.forEachEdge((e) => {
2635
3054
  const targetMap = afterOutTargets.get(e.source.nodeId) ??
2636
3055
  new Map();
2637
3056
  const targetSet = targetMap.get(e.source.handle) ?? new Set();
2638
3057
  targetSet.add(`${e.target.nodeId}.${e.target.handle}`);
2639
3058
  targetMap.set(e.source.handle, targetSet);
2640
3059
  afterOutTargets.set(e.source.nodeId, targetMap);
2641
- }
3060
+ });
2642
3061
  const setsEqual = (a, b) => {
2643
3062
  if (!a && !b)
2644
3063
  return true;
@@ -2686,14 +3105,14 @@ class GraphRuntime {
2686
3105
  }
2687
3106
  dispose() {
2688
3107
  this.runContextManager.resolveAll();
2689
- for (const node of this.graph.getNodes().values()) {
3108
+ this.graph.forEachNode((node) => {
2690
3109
  node.runtime.onDeactivated?.();
2691
3110
  node.runtime.dispose?.();
2692
3111
  node.lifecycle?.dispose?.({
2693
3112
  state: node.state,
2694
- setState: (next) => Object.assign(node.state, next),
3113
+ setState: (next) => this.graph.updateNodeState(node.nodeId, next),
2695
3114
  });
2696
- }
3115
+ });
2697
3116
  this.graph.clear();
2698
3117
  }
2699
3118
  execute(nodeId, runContextIds) {