@bian-womp/spark-graph 0.2.33 → 0.2.35

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
@@ -485,41 +485,12 @@ class GraphRuntime {
485
485
  queued: 0,
486
486
  progress: 0,
487
487
  },
488
+ initialInputs: n.initialInputs ?? {},
488
489
  };
489
490
  gr.nodes.set(n.nodeId, rn);
490
491
  }
491
492
  // Instantiate edges
492
493
  gr.edges = GraphRuntime.buildEdges(def, registry, gr.resolvedByNode);
493
- // After nodes and edges exist, seed registry-, dynamic- and graph-level defaults
494
- for (const n of def.nodes) {
495
- const node = gr.nodes.get(n.nodeId);
496
- const desc = registry.nodes.get(n.typeId);
497
- if (!node || !desc)
498
- continue;
499
- // Resolve registry-level defaults and dynamic (resolved) defaults
500
- const regDefaults = desc.inputDefaults ?? {};
501
- const dynDefaults = gr.resolvedByNode.get(n.nodeId)?.inputDefaults ?? {};
502
- const graphDefaults = n.initialInputs ?? {};
503
- // Apply precedence: graph-level overrides dynamic, which overrides registry-level
504
- const merged = {
505
- ...regDefaults,
506
- ...dynDefaults,
507
- ...graphDefaults,
508
- };
509
- for (const [handle, value] of Object.entries(merged)) {
510
- // Only seed if input has no inbound wiring
511
- const hasInbound = gr.edges.some((e) => e.target.nodeId === n.nodeId && e.target.handle === handle);
512
- if (hasInbound)
513
- continue;
514
- if (value === undefined)
515
- continue;
516
- // Clone to avoid accidental shared references
517
- node.inputs[handle] =
518
- typeof structuredClone === "function"
519
- ? structuredClone(value)
520
- : JSON.parse(JSON.stringify(value));
521
- }
522
- }
523
494
  // Schedule async recompute only for nodes that indicated Promise-based resolveHandles
524
495
  for (const nodeId of initial.pending)
525
496
  gr.scheduleRecomputeHandles(nodeId);
@@ -643,6 +614,8 @@ class GraphRuntime {
643
614
  return;
644
615
  const now = Date.now();
645
616
  const policy = node.policy ?? {};
617
+ // Compute effective inputs (real inputs + defaults) for this execution
618
+ const effectiveInputs = this.getEffectiveInputs(nodeId);
646
619
  if (policy.debounceMs &&
647
620
  node.lastScheduledAt &&
648
621
  now - node.lastScheduledAt < policy.debounceMs) {
@@ -650,7 +623,7 @@ class GraphRuntime {
650
623
  node.queue.splice(0, node.queue.length);
651
624
  node.runSeq += 1;
652
625
  const rid = `${nodeId}:${node.runSeq}:${now}`;
653
- node.queue.push({ runId: rid, inputs: { ...node.inputs } });
626
+ node.queue.push({ runId: rid, inputs: effectiveInputs });
654
627
  return;
655
628
  }
656
629
  node.lastScheduledAt = now;
@@ -756,7 +729,7 @@ class GraphRuntime {
756
729
  return;
757
730
  if (mode === "queue") {
758
731
  const maxQ = policy.maxQueue ?? 8;
759
- node.queue.push({ runId: rid, inputs: { ...node.inputs } });
732
+ node.queue.push({ runId: rid, inputs: effectiveInputs });
760
733
  if (node.queue.length > maxQ)
761
734
  node.queue.shift();
762
735
  const processNext = () => {
@@ -775,7 +748,7 @@ class GraphRuntime {
775
748
  return;
776
749
  }
777
750
  // switch or merge
778
- startRun(rid, { ...node.inputs });
751
+ startRun(rid, effectiveInputs);
779
752
  }
780
753
  // Returns true if all inbound handles for the node currently have a value
781
754
  allInboundHaveValue(nodeId) {
@@ -791,6 +764,62 @@ class GraphRuntime {
791
764
  }
792
765
  return true;
793
766
  }
767
+ // Computes effective inputs for a node by merging real inputs with defaults
768
+ // Defaults are applied only for unbound handles that have no explicit value
769
+ // This method does NOT mutate node.inputs - defaults remain virtual
770
+ // Dynamic handles (from resolveHandles) do NOT get defaults applied - they are metadata-only
771
+ getEffectiveInputs(nodeId) {
772
+ const node = this.nodes.get(nodeId);
773
+ if (!node)
774
+ return {};
775
+ const registry = this.registry;
776
+ if (!registry)
777
+ return {};
778
+ const desc = registry.nodes.get(node.typeId);
779
+ if (!desc)
780
+ return {};
781
+ const resolved = this.resolvedByNode.get(nodeId);
782
+ const regDefaults = desc.inputDefaults ?? {};
783
+ const dynDefaults = resolved?.inputDefaults ?? {};
784
+ const graphDefaults = node.initialInputs ?? {};
785
+ // Identify which handles are dynamically resolved (not in registry statics)
786
+ // Dynamic handles are metadata-only and should not receive defaults in execution
787
+ const staticHandles = new Set(Object.keys(desc.inputs ?? {}));
788
+ const dynamicHandles = new Set(Object.keys(resolved?.inputs ?? {}).filter((h) => !staticHandles.has(h)));
789
+ // Precedence: graph > dynamic > registry
790
+ const mergedDefaults = {
791
+ ...regDefaults,
792
+ ...dynDefaults,
793
+ ...graphDefaults,
794
+ };
795
+ // Start with real inputs only (no defaults)
796
+ const effective = { ...node.inputs };
797
+ // Build set of inbound handles (wired inputs)
798
+ const inbound = new Set(this.edges
799
+ .filter((e) => e.target.nodeId === nodeId)
800
+ .map((e) => e.target.handle));
801
+ // Apply defaults only for:
802
+ // 1. Unbound handles that have no explicit value
803
+ // 2. Static handles (not dynamically resolved)
804
+ // This prevents dynamic handles (like geo:*) from getting defaults in execution
805
+ // Dynamic handles are metadata-only for UI display, not execution values
806
+ for (const [handle, defaultValue] of Object.entries(mergedDefaults)) {
807
+ if (defaultValue === undefined)
808
+ continue;
809
+ if (inbound.has(handle))
810
+ continue; // Don't override wired inputs
811
+ if (effective[handle] !== undefined)
812
+ continue; // Already has value
813
+ if (dynamicHandles.has(handle))
814
+ continue; // Skip defaults for dynamic handles
815
+ // Clone to avoid shared references
816
+ effective[handle] =
817
+ typeof structuredClone === "function"
818
+ ? structuredClone(defaultValue)
819
+ : JSON.parse(JSON.stringify(defaultValue));
820
+ }
821
+ return effective;
822
+ }
794
823
  invalidateDownstream(nodeId) {
795
824
  // Notifies dependents; for now we propagate current outputs
796
825
  for (const e of this.edges.filter((e) => e.source.nodeId === nodeId)) {
@@ -1092,12 +1121,13 @@ class GraphRuntime {
1092
1121
  // call onActivated for nodes that implement it
1093
1122
  for (const node of this.nodes.values()) {
1094
1123
  const ctrl = new AbortController();
1124
+ const effectiveInputs = this.getEffectiveInputs(node.nodeId);
1095
1125
  const ctx = {
1096
1126
  state: node.state,
1097
1127
  setState: (next) => Object.assign(node.state, next),
1098
1128
  emit: (handle, value) => this.propagate(node.nodeId, handle, value),
1099
1129
  invalidateDownstream: () => this.invalidateDownstream(node.nodeId),
1100
- getInput: (handle) => node.inputs[handle],
1130
+ getInput: (handle) => effectiveInputs[handle],
1101
1131
  environment: this.environment,
1102
1132
  runId: `${node.nodeId}:activation`,
1103
1133
  abortSignal: ctrl.signal,
@@ -1125,12 +1155,13 @@ class GraphRuntime {
1125
1155
  if (!node)
1126
1156
  return;
1127
1157
  const ctrl = new AbortController();
1158
+ const effectiveInputs = this.getEffectiveInputs(nodeId);
1128
1159
  const ctx = {
1129
1160
  state: node.state,
1130
1161
  setState: (next) => Object.assign(node.state, next),
1131
1162
  emit: (handle, value) => this.propagate(nodeId, handle, value),
1132
1163
  invalidateDownstream: () => this.invalidateDownstream(nodeId),
1133
- getInput: (handle) => node.inputs[handle],
1164
+ getInput: (handle) => effectiveInputs[handle],
1134
1165
  environment: this.environment,
1135
1166
  runId: `${nodeId}:external`,
1136
1167
  abortSignal: ctrl.signal,
@@ -1387,16 +1418,18 @@ class GraphRuntime {
1387
1418
  queued: 0,
1388
1419
  progress: 0,
1389
1420
  },
1421
+ initialInputs: n.initialInputs ?? {},
1390
1422
  };
1391
1423
  this.nodes.set(n.nodeId, rn);
1392
1424
  // Activate new node
1393
1425
  const ctrl = new AbortController();
1426
+ const effectiveInputs = this.getEffectiveInputs(rn.nodeId);
1394
1427
  const ctx = {
1395
1428
  state: rn.state,
1396
1429
  setState: (next) => Object.assign(rn.state, next),
1397
1430
  emit: (handle, value) => this.propagate(rn.nodeId, handle, value),
1398
1431
  invalidateDownstream: () => this.invalidateDownstream(rn.nodeId),
1399
- getInput: (handle) => rn.inputs[handle],
1432
+ getInput: (handle) => effectiveInputs[handle],
1400
1433
  environment: this.environment,
1401
1434
  runId: `${rn.nodeId}:activation`,
1402
1435
  abortSignal: ctrl.signal,
@@ -1409,8 +1442,9 @@ class GraphRuntime {
1409
1442
  rn.runtime.onActivated?.(ctx);
1410
1443
  }
1411
1444
  else {
1412
- // update params/policy
1445
+ // update params/policy and initialInputs
1413
1446
  existing.params = n.params;
1447
+ existing.initialInputs = n.initialInputs ?? {};
1414
1448
  if (!existing.stats) {
1415
1449
  existing.stats = {
1416
1450
  runs: 0,
@@ -1464,32 +1498,7 @@ class GraphRuntime {
1464
1498
  }
1465
1499
  }
1466
1500
  }
1467
- // If input lost inbound, try to re-seed from defaults
1468
1501
  if (changed) {
1469
- const defNode = def.nodes.find((n) => n.nodeId === nodeId);
1470
- if (defNode) {
1471
- const desc = registry.nodes.get(defNode.typeId);
1472
- if (desc) {
1473
- const regDefaults = desc.inputDefaults ?? {};
1474
- const dynDefaults = this.resolvedByNode.get(defNode.nodeId)?.inputDefaults ?? {};
1475
- const graphDefaults = defNode.initialInputs ?? {};
1476
- const merged = {
1477
- ...regDefaults,
1478
- ...dynDefaults,
1479
- ...graphDefaults,
1480
- };
1481
- for (const h of Array.from(prevSet)) {
1482
- if (!currSet.has(h) && node.inputs[h] === undefined) {
1483
- const v = merged[h];
1484
- if (v !== undefined)
1485
- node.inputs[h] =
1486
- typeof structuredClone === "function"
1487
- ? structuredClone(v)
1488
- : JSON.parse(JSON.stringify(v));
1489
- }
1490
- }
1491
- }
1492
- }
1493
1502
  // Clear buckets for handles that lost inbound
1494
1503
  const bucketsForNode = this.arrayInputBuckets.get(nodeId);
1495
1504
  if (bucketsForNode) {
@@ -1547,34 +1556,6 @@ class GraphRuntime {
1547
1556
  }
1548
1557
  }
1549
1558
  }
1550
- // Seed defaults for nodes (new or existing) where inputs are still undefined and not inbound
1551
- for (const n of def.nodes) {
1552
- const node = this.nodes.get(n.nodeId);
1553
- const desc = registry.nodes.get(n.typeId);
1554
- if (!node || !desc)
1555
- continue;
1556
- const regDefaults = desc.inputDefaults ?? {};
1557
- const dynDefaults = this.resolvedByNode.get(n.nodeId)?.inputDefaults ?? {};
1558
- const graphDefaults = n.initialInputs ?? {};
1559
- const merged = {
1560
- ...regDefaults,
1561
- ...dynDefaults,
1562
- ...graphDefaults,
1563
- };
1564
- const inboundSet = nextInbound.get(n.nodeId) ?? new Set();
1565
- for (const [handle, value] of Object.entries(merged)) {
1566
- if (value === undefined)
1567
- continue;
1568
- if (inboundSet.has(handle))
1569
- continue;
1570
- if (node.inputs[handle] !== undefined)
1571
- continue;
1572
- node.inputs[handle] =
1573
- typeof structuredClone === "function"
1574
- ? structuredClone(value)
1575
- : JSON.parse(JSON.stringify(value));
1576
- }
1577
- }
1578
1559
  // Prune array bucket contributions for edges that no longer exist
1579
1560
  const validPerTarget = new Map();
1580
1561
  for (const ed of this.edges) {
@@ -1653,30 +1634,6 @@ class GraphRuntime {
1653
1634
  return;
1654
1635
  this.resolvedByNode.set(nodeId, next);
1655
1636
  this.updateNodeHandles(nodeId, next, registry);
1656
- // Seed defaults for newly introduced inputs that are not inbound
1657
- const inbound = this.edges
1658
- .filter((e) => e.target.nodeId === nodeId)
1659
- .map((e) => e.target.handle);
1660
- for (const [handle, value] of Object.entries(inputDefaults)) {
1661
- if (value === undefined)
1662
- continue;
1663
- if (inbound.includes(handle))
1664
- continue;
1665
- if (node.inputs[handle] === undefined) {
1666
- node.inputs[handle] =
1667
- typeof structuredClone === "function"
1668
- ? structuredClone(value)
1669
- : JSON.parse(JSON.stringify(value));
1670
- // Emit input value event for seeded defaults
1671
- this.emit("value", {
1672
- nodeId,
1673
- handle,
1674
- value: node.inputs[handle],
1675
- io: "input",
1676
- runtimeTypeId: getTypedOutputTypeId(node.inputs[handle]),
1677
- });
1678
- }
1679
- }
1680
1637
  // Notify graph updated for UI parity
1681
1638
  this.emit("invalidate", { reason: "graph-updated" });
1682
1639
  }