@bian-womp/spark-graph 0.2.29 → 0.2.31

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
@@ -573,57 +573,67 @@ class GraphRuntime {
573
573
  return node?.outputs[output];
574
574
  }
575
575
  static buildEdgeConverters(srcDeclared, dstDeclared, registry) {
576
- let convert;
577
- let convertAsync;
578
- if (dstDeclared && srcDeclared) {
579
- if (Array.isArray(srcDeclared)) {
580
- const srcTypes = srcDeclared;
581
- const anyAsync = srcTypes.some((s) => {
582
- const res = registry.resolveCoercion(s, dstDeclared);
583
- return res?.kind === "async";
584
- });
585
- if (anyAsync) {
586
- convertAsync = async (v, signal) => {
587
- const typeId = getTypedOutputTypeId(v);
588
- if (!typeId)
589
- throw new Error(`Typed output required for union source; allowed: ${srcTypes.join("|")}`);
590
- if (!srcTypes.includes(typeId))
591
- throw new Error(`Invalid typed output ${typeId}; allowed: ${srcTypes.join("|")}`);
592
- const payload = getTypedOutputValue(v);
593
- const res = registry.resolveCoercion(typeId, dstDeclared);
594
- if (!res)
595
- return payload;
596
- if (res.kind === "async")
597
- return await res.convertAsync(payload, signal);
598
- return res.convert(payload);
599
- };
576
+ if (!dstDeclared || !srcDeclared) {
577
+ return {};
578
+ }
579
+ const isUnion = Array.isArray(srcDeclared);
580
+ const srcTypes = isUnion ? srcDeclared : [srcDeclared];
581
+ // Helper to get the coercion for a specific type
582
+ const getCoercion = (typeId) => {
583
+ return registry.resolveCoercion(typeId, dstDeclared);
584
+ };
585
+ // Resolve coercions for all source types
586
+ const coercions = srcTypes.map(getCoercion);
587
+ const hasAsync = coercions.some((r) => r?.kind === "async");
588
+ // Helper to extract and validate typed output for unions
589
+ const extractPayload = (v) => {
590
+ const typeId = getTypedOutputTypeId(v);
591
+ const payload = getTypedOutputValue(v);
592
+ if (isUnion) {
593
+ if (!typeId) {
594
+ throw new Error(`Typed output required for union source; allowed: ${srcTypes.join("|")}`);
600
595
  }
601
- else {
602
- convert = (v) => {
603
- const typeId = getTypedOutputTypeId(v);
604
- if (!typeId)
605
- throw new Error(`Typed output required for union source; allowed: ${srcTypes.join("|")}`);
606
- if (!srcTypes.includes(typeId))
607
- throw new Error(`Invalid typed output ${typeId}; allowed: ${srcTypes.join("|")}`);
608
- const payload = getTypedOutputValue(v);
609
- const res = registry.resolveCoercion(typeId, dstDeclared);
610
- if (!res)
611
- return payload;
612
- if (res.kind === "async")
613
- throw new Error("Async coercion required but convert used");
614
- return res.convert(payload);
615
- };
596
+ if (!srcTypes.includes(typeId)) {
597
+ throw new Error(`Invalid typed output ${typeId}; allowed: ${srcTypes.join("|")}`);
616
598
  }
617
599
  }
618
- else {
619
- const res = registry.resolveCoercion(srcDeclared, dstDeclared);
620
- if (res?.kind === "async")
621
- convertAsync = res.convertAsync;
622
- else if (res?.kind === "sync")
623
- convert = res.convert;
600
+ else if (typeId) {
601
+ // Warn if typed output is used for non-union source
602
+ console.warn(`Typed output ${typeId} is fed even though source is not union: ${srcDeclared} -> ${dstDeclared}`);
624
603
  }
604
+ return { typeId: typeId || srcTypes[0], payload };
605
+ };
606
+ if (hasAsync) {
607
+ return {
608
+ convertAsync: async (v, signal) => {
609
+ const { typeId, payload } = extractPayload(v);
610
+ const res = getCoercion(typeId);
611
+ if (!res)
612
+ return payload;
613
+ if (res.kind === "async") {
614
+ return await res.convertAsync(payload, signal);
615
+ }
616
+ return res.convert(payload);
617
+ },
618
+ };
619
+ }
620
+ // Sync path
621
+ const firstCoercion = coercions.find((r) => r?.kind === "sync");
622
+ if (!firstCoercion) {
623
+ return {};
625
624
  }
626
- return { convert, convertAsync };
625
+ return {
626
+ convert: (v) => {
627
+ const { typeId, payload } = extractPayload(v);
628
+ const res = getCoercion(typeId);
629
+ if (!res)
630
+ return payload;
631
+ if (res.kind === "async") {
632
+ throw new Error("Async coercion required but convert used");
633
+ }
634
+ return res.convert(payload);
635
+ },
636
+ };
627
637
  }
628
638
  scheduleInputsChanged(nodeId) {
629
639
  const node = this.nodes.get(nodeId);
@@ -827,6 +837,9 @@ class GraphRuntime {
827
837
  const dstNode = this.nodes.get(e.target.nodeId);
828
838
  if (!dstNode)
829
839
  return;
840
+ // Skip writing to unresolved handles - wait for handle resolution
841
+ if (e.dstDeclared === undefined)
842
+ return;
830
843
  const dstIsArray = typeof e.dstDeclared === "string" && e.dstDeclared.endsWith("[]");
831
844
  let next = v;
832
845
  // If target input is an array type, merge per-edge contributions deterministically
@@ -984,7 +997,7 @@ class GraphRuntime {
984
997
  return def.edges.map((e) => {
985
998
  const srcNode = def.nodes.find((n) => n.nodeId === e.source.nodeId);
986
999
  const dstNode = def.nodes.find((n) => n.nodeId === e.target.nodeId);
987
- let effectiveTypeId = e.typeId;
1000
+ let effectiveTypeId = e.typeId; // Start with original
988
1001
  let srcDeclared;
989
1002
  let dstDeclared;
990
1003
  if (srcNode) {
@@ -993,6 +1006,7 @@ class GraphRuntime {
993
1006
  srcDeclared = resolved.outputs[e.source.handle];
994
1007
  }
995
1008
  if (!effectiveTypeId) {
1009
+ // Infer if not explicitly set
996
1010
  effectiveTypeId = Array.isArray(srcDeclared)
997
1011
  ? srcDeclared[0]
998
1012
  : srcDeclared;
@@ -1007,7 +1021,8 @@ class GraphRuntime {
1007
1021
  id: e.id,
1008
1022
  source: { ...e.source },
1009
1023
  target: { ...e.target },
1010
- typeId: effectiveTypeId ?? "untyped",
1024
+ typeId: e.typeId, // Preserve original (may be undefined)
1025
+ effectiveTypeId: effectiveTypeId ?? "untyped", // Always present
1011
1026
  convert,
1012
1027
  convertAsync,
1013
1028
  srcUnionTypes: Array.isArray(srcDeclared)
@@ -1031,18 +1046,19 @@ class GraphRuntime {
1031
1046
  this.resolvedByNode.set(nodeId, handles);
1032
1047
  // Recompute edge converter/type for edges where this node is source or target
1033
1048
  for (const e of this.edges) {
1034
- let srcDeclared = e.typeId;
1049
+ let srcDeclared = e.effectiveTypeId; // Use effectiveTypeId as fallback
1035
1050
  let dstDeclared = e.dstDeclared;
1051
+ const oldDstDeclared = dstDeclared; // Track old value to detect resolution
1036
1052
  if (e.source.nodeId === nodeId) {
1037
1053
  const resolved = this.resolvedByNode.get(nodeId);
1038
1054
  srcDeclared = resolved
1039
1055
  ? resolved.outputs[e.source.handle]
1040
1056
  : srcDeclared;
1041
- // If edge had no explicit typeId, infer from updated src
1057
+ // Update effectiveTypeId if original wasn't explicit
1042
1058
  if (!e.typeId) {
1043
- e.typeId = Array.isArray(srcDeclared)
1044
- ? srcDeclared?.[0]
1045
- : srcDeclared;
1059
+ e.effectiveTypeId = Array.isArray(srcDeclared)
1060
+ ? srcDeclared?.[0] ?? "untyped"
1061
+ : srcDeclared ?? "untyped";
1046
1062
  }
1047
1063
  }
1048
1064
  if (e.target.nodeId === nodeId) {
@@ -1055,6 +1071,19 @@ class GraphRuntime {
1055
1071
  const conv = GraphRuntime.buildEdgeConverters(srcDeclared, dstDeclared, registry);
1056
1072
  e.convert = conv.convert;
1057
1073
  e.convertAsync = conv.convertAsync;
1074
+ // If target handle was just resolved (was undefined, now has a type), re-propagate values
1075
+ if (e.target.nodeId === nodeId &&
1076
+ oldDstDeclared === undefined &&
1077
+ dstDeclared !== undefined) {
1078
+ const srcNode = this.nodes.get(e.source.nodeId);
1079
+ if (srcNode) {
1080
+ const srcValue = srcNode.outputs[e.source.handle];
1081
+ if (srcValue !== undefined) {
1082
+ // Re-propagate through the now-resolved edge converter
1083
+ this.propagate(e.source.nodeId, e.source.handle, srcValue);
1084
+ }
1085
+ }
1086
+ }
1058
1087
  }
1059
1088
  // Invalidate downstream for this node so UI refreshes
1060
1089
  this.invalidateDownstream(nodeId);
@@ -1165,10 +1194,23 @@ class GraphRuntime {
1165
1194
  getGraphDef() {
1166
1195
  const nodes = Array.from(this.nodes.values()).map((n) => {
1167
1196
  const resolved = this.resolvedByNode.get(n.nodeId);
1197
+ // Collect user-provided inputs (inputs without inbound edges)
1198
+ const initialInputs = {};
1199
+ for (const [handle, value] of Object.entries(n.inputs)) {
1200
+ const hasInbound = this.edges.some((e) => e.target.nodeId === n.nodeId && e.target.handle === handle);
1201
+ if (!hasInbound && value !== undefined) {
1202
+ // Clone to avoid shared references
1203
+ initialInputs[handle] =
1204
+ typeof structuredClone === "function"
1205
+ ? structuredClone(value)
1206
+ : JSON.parse(JSON.stringify(value));
1207
+ }
1208
+ }
1168
1209
  return {
1169
1210
  nodeId: n.nodeId,
1170
1211
  typeId: n.typeId,
1171
1212
  params: n.params ? { ...n.params } : undefined,
1213
+ initialInputs: Object.keys(initialInputs).length > 0 ? initialInputs : undefined,
1172
1214
  resolvedHandles: resolved ? { ...resolved } : undefined,
1173
1215
  };
1174
1216
  });
@@ -1176,7 +1218,7 @@ class GraphRuntime {
1176
1218
  id: e.id,
1177
1219
  source: { nodeId: e.source.nodeId, handle: e.source.handle },
1178
1220
  target: { nodeId: e.target.nodeId, handle: e.target.handle },
1179
- typeId: e.typeId && e.typeId !== "untyped" ? e.typeId : undefined,
1221
+ typeId: e.typeId, // Only export original typeId (may be undefined)
1180
1222
  }));
1181
1223
  return { nodes, edges };
1182
1224
  }