@bian-womp/spark-graph 0.2.9 → 0.2.11

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
@@ -504,6 +504,7 @@ class GraphRuntime {
504
504
  srcUnionTypes: Array.isArray(srcDeclared)
505
505
  ? [...srcDeclared]
506
506
  : undefined,
507
+ dstDeclared,
507
508
  stats: { runs: 0, inFlight: false, progress: 0 },
508
509
  };
509
510
  });
@@ -839,17 +840,26 @@ class GraphRuntime {
839
840
  const dstNode = this.nodes.get(e.target.nodeId);
840
841
  if (!dstNode)
841
842
  return;
843
+ const dstIsArray = typeof e.dstDeclared === "string" && e.dstDeclared.endsWith("[]");
844
+ let next = v;
845
+ // If target input is an array type, append incoming values instead of last-write wins
846
+ if (dstIsArray) {
847
+ const toArray = (x) => Array.isArray(x) ? x : x === undefined ? [] : [x];
848
+ const prev = dstNode.inputs[e.target.handle];
849
+ const merged = [...toArray(prev), ...toArray(v)];
850
+ next = merged;
851
+ }
842
852
  const prev = dstNode.inputs[e.target.handle];
843
- const same = this.valuesEqual(prev, v);
853
+ const same = this.valuesEqual(prev, next);
844
854
  if (!same) {
845
- dstNode.inputs[e.target.handle] = v;
855
+ dstNode.inputs[e.target.handle] = next;
846
856
  // Emit value event for input updates
847
857
  this.emit("value", {
848
858
  nodeId: e.target.nodeId,
849
859
  handle: e.target.handle,
850
- value: v,
860
+ value: next,
851
861
  io: "input",
852
- runtimeTypeId: getTypedOutputTypeId(v),
862
+ runtimeTypeId: getTypedOutputTypeId(next),
853
863
  });
854
864
  if (!this.paused && this.allInboundHaveValue(e.target.nodeId))
855
865
  this.scheduleInputsChanged(e.target.nodeId);
@@ -1044,6 +1054,60 @@ class GraphRuntime {
1044
1054
  __unsafe_scheduleInputsChanged(nodeId) {
1045
1055
  this.scheduleInputsChanged(nodeId);
1046
1056
  }
1057
+ // Hydrate inputs/outputs without triggering computation; optionally re-emit outputs downstream
1058
+ hydrate(payload, opts) {
1059
+ const prevPaused = this.paused;
1060
+ this.paused = true;
1061
+ try {
1062
+ const ins = payload?.inputs || {};
1063
+ for (const [nodeId, map] of Object.entries(ins)) {
1064
+ const node = this.nodes.get(nodeId);
1065
+ if (!node)
1066
+ continue;
1067
+ for (const [h, v] of Object.entries(map || {})) {
1068
+ node.inputs[h] =
1069
+ typeof structuredClone === "function"
1070
+ ? structuredClone(v)
1071
+ : JSON.parse(JSON.stringify(v));
1072
+ // emit input value event
1073
+ this.emit("value", {
1074
+ nodeId,
1075
+ handle: h,
1076
+ value: node.inputs[h],
1077
+ io: "input",
1078
+ runtimeTypeId: getTypedOutputTypeId(node.inputs[h]),
1079
+ });
1080
+ }
1081
+ }
1082
+ const outs = payload?.outputs || {};
1083
+ for (const [nodeId, map] of Object.entries(outs)) {
1084
+ const node = this.nodes.get(nodeId);
1085
+ if (!node)
1086
+ continue;
1087
+ for (const [h, v] of Object.entries(map || {})) {
1088
+ node.outputs[h] =
1089
+ typeof structuredClone === "function"
1090
+ ? structuredClone(v)
1091
+ : JSON.parse(JSON.stringify(v));
1092
+ // emit output value event
1093
+ this.emit("value", {
1094
+ nodeId,
1095
+ handle: h,
1096
+ value: node.outputs[h],
1097
+ io: "output",
1098
+ runtimeTypeId: getTypedOutputTypeId(node.outputs[h]),
1099
+ });
1100
+ }
1101
+ }
1102
+ if (opts?.reemit) {
1103
+ for (const nodeId of this.nodes.keys())
1104
+ this.reemitNodeOutputs(nodeId);
1105
+ }
1106
+ }
1107
+ finally {
1108
+ this.paused = prevPaused;
1109
+ }
1110
+ }
1047
1111
  // Incrementally update nodes/edges to match new definition without full rebuild
1048
1112
  update(def, registry) {
1049
1113
  // Handle node additions and removals
@@ -1182,6 +1246,7 @@ class GraphRuntime {
1182
1246
  typeId: effectiveTypeId ?? "untyped",
1183
1247
  convert,
1184
1248
  convertAsync,
1249
+ dstDeclared,
1185
1250
  stats: { runs: 0, inFlight: false, progress: 0 },
1186
1251
  };
1187
1252
  });
@@ -1327,6 +1392,8 @@ class GraphBuilder {
1327
1392
  }
1328
1393
  // edges validation: nodes exist, handles exist, type exists
1329
1394
  const inboundCounts = new Map();
1395
+ // Track which inbound (nodeId::handle) are declared as array inputs, to allow multi-inbound without warning
1396
+ const inboundArrayOk = new Set();
1330
1397
  for (const e of def.edges) {
1331
1398
  if (edgeIds.has(e.id)) {
1332
1399
  issues.push({
@@ -1367,7 +1434,7 @@ class GraphBuilder {
1367
1434
  if (dstNode) {
1368
1435
  const dstType = this.registry.nodes.get(dstNode.typeId);
1369
1436
  if (dstType)
1370
- _dstDeclared = dstType.inputs[e.target.handle];
1437
+ _dstDeclared = getInputTypeId(dstType.inputs, e.target.handle);
1371
1438
  }
1372
1439
  if (!effectiveTypeId) {
1373
1440
  if (Array.isArray(_srcDeclared) && _dstDeclared) {
@@ -1514,9 +1581,19 @@ class GraphBuilder {
1514
1581
  // Track multiple inbound edges targeting the same input handle
1515
1582
  const inboundKey = `${e.target.nodeId}::${e.target.handle}`;
1516
1583
  inboundCounts.set(inboundKey, (inboundCounts.get(inboundKey) ?? 0) + 1);
1584
+ // If the target input is declared as an array type, allow multi-inbound (runtime will append)
1585
+ if (dstNode) {
1586
+ const dstType = this.registry.nodes.get(dstNode.typeId);
1587
+ const declaredIn = dstType
1588
+ ? getInputTypeId(dstType.inputs, e.target.handle)
1589
+ : undefined;
1590
+ if (typeof declaredIn === "string" && declaredIn.endsWith("[]")) {
1591
+ inboundArrayOk.add(inboundKey);
1592
+ }
1593
+ }
1517
1594
  }
1518
1595
  for (const [key, count] of inboundCounts) {
1519
- if (count > 1) {
1596
+ if (count > 1 && !inboundArrayOk.has(key)) {
1520
1597
  issues.push({
1521
1598
  level: "warning",
1522
1599
  code: "MULTI_INBOUND",