@bian-womp/spark-graph 0.2.15 → 0.2.17

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
@@ -406,6 +406,8 @@ class GraphRuntime {
406
406
  constructor() {
407
407
  this.nodes = new Map();
408
408
  this.edges = [];
409
+ // Current resolved handles per node (registry statics merged with per-node overrides)
410
+ this.resolvedByNode = new Map();
409
411
  this.listeners = new Map();
410
412
  this.environment = {};
411
413
  this.paused = false;
@@ -434,7 +436,7 @@ class GraphRuntime {
434
436
  const gr = new GraphRuntime();
435
437
  gr.environment = opts?.environment ?? {};
436
438
  // Precompute per-node resolved handles (use def-provided overrides; do not compute dynamically here)
437
- const resolvedByNode = GraphRuntime.computeResolvedHandleMap(def, registry);
439
+ gr.resolvedByNode = GraphRuntime.computeResolvedHandleMap(def, registry);
438
440
  // Instantiate nodes
439
441
  for (const n of def.nodes) {
440
442
  const desc = registry.nodes.get(n.typeId);
@@ -474,7 +476,7 @@ class GraphRuntime {
474
476
  gr.nodes.set(n.nodeId, rn);
475
477
  }
476
478
  // Instantiate edges
477
- gr.edges = GraphRuntime.buildEdges(def, registry, resolvedByNode);
479
+ gr.edges = GraphRuntime.buildEdges(def, registry, gr.resolvedByNode);
478
480
  // After nodes and edges exist, seed registry- and graph-level defaults
479
481
  for (const n of def.nodes) {
480
482
  const node = gr.nodes.get(n.nodeId);
@@ -967,6 +969,39 @@ class GraphRuntime {
967
969
  this.propagate(nodeId, handle, value);
968
970
  }
969
971
  }
972
+ // Update resolved handles for a single node and refresh edge converters/types that touch it
973
+ updateNodeHandles(nodeId, handles, registry) {
974
+ this.resolvedByNode.set(nodeId, handles);
975
+ // Recompute edge converter/type for edges where this node is source or target
976
+ for (const e of this.edges) {
977
+ let srcDeclared = e.typeId;
978
+ let dstDeclared = e.dstDeclared;
979
+ if (e.source.nodeId === nodeId) {
980
+ const resolved = this.resolvedByNode.get(nodeId);
981
+ srcDeclared = resolved
982
+ ? resolved.outputs[e.source.handle]
983
+ : srcDeclared;
984
+ // If edge had no explicit typeId, infer from updated src
985
+ if (!e.typeId) {
986
+ e.typeId = Array.isArray(srcDeclared)
987
+ ? srcDeclared?.[0]
988
+ : srcDeclared;
989
+ }
990
+ }
991
+ if (e.target.nodeId === nodeId) {
992
+ const resolved = this.resolvedByNode.get(nodeId);
993
+ if (resolved) {
994
+ dstDeclared = getInputTypeId(resolved.inputs, e.target.handle);
995
+ e.dstDeclared = dstDeclared;
996
+ }
997
+ }
998
+ const conv = GraphRuntime.buildEdgeConverters(srcDeclared, dstDeclared, registry);
999
+ e.convert = conv.convert;
1000
+ e.convertAsync = conv.convertAsync;
1001
+ }
1002
+ // Invalidate downstream for this node so UI refreshes
1003
+ this.invalidateDownstream(nodeId);
1004
+ }
970
1005
  launch() {
971
1006
  // call onActivated for nodes that implement it
972
1007
  for (const node of this.nodes.values()) {
@@ -1260,9 +1295,9 @@ class GraphRuntime {
1260
1295
  prevOutTargets.set(e.source.nodeId, tmap);
1261
1296
  }
1262
1297
  // Precompute per-node resolved handles for updated graph
1263
- const resolvedByNode = GraphRuntime.computeResolvedHandleMap(def, registry);
1298
+ this.resolvedByNode = GraphRuntime.computeResolvedHandleMap(def, registry);
1264
1299
  // Rebuild edges mapping with coercions
1265
- this.edges = GraphRuntime.buildEdges(def, registry, resolvedByNode);
1300
+ this.edges = GraphRuntime.buildEdges(def, registry, this.resolvedByNode);
1266
1301
  // Build new inbound map
1267
1302
  const nextInbound = new Map();
1268
1303
  for (const e of this.edges) {
@@ -1405,35 +1440,56 @@ class GraphBuilder {
1405
1440
  const issues = [];
1406
1441
  const nodeIds = new Set();
1407
1442
  const edgeIds = new Set();
1443
+ // Precompute effective handle maps (registry statics merged with per-node resolvedHandles)
1444
+ const nodeById = new Map();
1445
+ const effByNodeId = new Map();
1446
+ const getEffectiveHandles = (n) => {
1447
+ if (!n)
1448
+ return { inputs: {}, outputs: {} };
1449
+ const desc = this.registry.nodes.get(n.typeId);
1450
+ const resolved = n.resolvedHandles || {};
1451
+ const inputs = { ...desc?.inputs, ...resolved.inputs };
1452
+ const outputs = { ...desc?.outputs, ...resolved.outputs };
1453
+ return { inputs, outputs };
1454
+ };
1455
+ const normOut = (decl) => Array.isArray(decl) ? decl : decl ? [decl] : [];
1456
+ const inferEdgeType = (srcDeclared, dstDeclared, explicit) => {
1457
+ if (explicit)
1458
+ return explicit;
1459
+ if (Array.isArray(srcDeclared) && dstDeclared)
1460
+ return dstDeclared;
1461
+ if (srcDeclared)
1462
+ return Array.isArray(srcDeclared) ? srcDeclared[0] : srcDeclared;
1463
+ return undefined;
1464
+ };
1465
+ const canFlow = (from, to) => {
1466
+ if (!to || !from)
1467
+ return true;
1468
+ const arr = Array.isArray(from) ? from : [from];
1469
+ return arr.every((s) => s === to || !!this.registry.canCoerce(s, to));
1470
+ };
1471
+ const pushIssue = (level, code, message, data) => {
1472
+ issues.push({ level, code, message, data });
1473
+ };
1408
1474
  // nodes exist, ids unique, and categories registered
1409
1475
  for (const n of def.nodes) {
1410
1476
  if (nodeIds.has(n.nodeId)) {
1411
- issues.push({
1412
- level: "error",
1413
- code: "NODE_ID_DUP",
1414
- message: `Duplicate nodeId ${n.nodeId}`,
1415
- data: { nodeId: n.nodeId },
1477
+ pushIssue("error", "NODE_ID_DUP", `Duplicate nodeId ${n.nodeId}`, {
1478
+ nodeId: n.nodeId,
1416
1479
  });
1417
1480
  }
1418
1481
  else {
1419
1482
  nodeIds.add(n.nodeId);
1420
1483
  }
1484
+ nodeById.set(n.nodeId, n);
1485
+ effByNodeId.set(n.nodeId, getEffectiveHandles(n));
1421
1486
  const nodeType = this.registry.nodes.get(n.typeId);
1422
1487
  if (!nodeType) {
1423
- issues.push({
1424
- level: "error",
1425
- code: "NODE_TYPE_MISSING",
1426
- message: `Unknown node type ${n.typeId}`,
1427
- data: { typeId: n.typeId, nodeId: n.nodeId },
1428
- });
1488
+ pushIssue("error", "NODE_TYPE_MISSING", `Unknown node type ${n.typeId}`, { typeId: n.typeId, nodeId: n.nodeId });
1429
1489
  continue;
1430
1490
  }
1431
1491
  if (!this.registry.categories.has(nodeType.categoryId)) {
1432
- issues.push({
1433
- level: "error",
1434
- code: "CATEGORY_MISSING",
1435
- message: `Unknown category ${nodeType.categoryId} for node type ${n.typeId}`,
1436
- });
1492
+ pushIssue("error", "CATEGORY_MISSING", `Unknown category ${nodeType.categoryId} for node type ${n.typeId}`);
1437
1493
  }
1438
1494
  }
1439
1495
  // edges validation: nodes exist, handles exist, type exists
@@ -1442,186 +1498,96 @@ class GraphBuilder {
1442
1498
  const inboundArrayOk = new Set();
1443
1499
  for (const e of def.edges) {
1444
1500
  if (edgeIds.has(e.id)) {
1445
- issues.push({
1446
- level: "error",
1447
- code: "EDGE_ID_DUP",
1448
- message: `Duplicate edge id ${e.id}`,
1449
- data: { edgeId: e.id },
1501
+ pushIssue("error", "EDGE_ID_DUP", `Duplicate edge id ${e.id}`, {
1502
+ edgeId: e.id,
1450
1503
  });
1451
1504
  }
1452
1505
  else {
1453
1506
  edgeIds.add(e.id);
1454
1507
  }
1455
- const srcNode = def.nodes.find((nn) => nn.nodeId === e.source.nodeId);
1456
- const dstNode = def.nodes.find((nn) => nn.nodeId === e.target.nodeId);
1508
+ const srcNode = nodeById.get(e.source.nodeId);
1509
+ const dstNode = nodeById.get(e.target.nodeId);
1457
1510
  if (!srcNode)
1458
- issues.push({
1459
- level: "error",
1460
- code: "EDGE_SOURCE_MISSING",
1461
- message: `Edge ${e.id} source node missing`,
1462
- data: { edgeId: e.id },
1511
+ pushIssue("error", "EDGE_SOURCE_MISSING", `Edge ${e.id} source node missing`, {
1512
+ edgeId: e.id,
1463
1513
  });
1464
1514
  if (!dstNode)
1465
- issues.push({
1466
- level: "error",
1467
- code: "EDGE_TARGET_MISSING",
1468
- message: `Edge ${e.id} target node missing`,
1469
- data: { edgeId: e.id },
1515
+ pushIssue("error", "EDGE_TARGET_MISSING", `Edge ${e.id} target node missing`, {
1516
+ edgeId: e.id,
1470
1517
  });
1471
- // Infer edge type when missing. For union sources, prefer target input type if available.
1472
- let effectiveTypeId = e.typeId;
1473
- let _srcDeclared;
1474
- let _dstDeclared;
1475
- if (srcNode) {
1476
- const srcType = this.registry.nodes.get(srcNode.typeId);
1477
- if (srcType)
1478
- _srcDeclared = srcType.outputs[e.source.handle];
1479
- }
1480
- if (dstNode) {
1481
- const dstType = this.registry.nodes.get(dstNode.typeId);
1482
- if (dstType)
1483
- _dstDeclared = getInputTypeId(dstType.inputs, e.target.handle);
1484
- }
1485
- if (!effectiveTypeId) {
1486
- if (Array.isArray(_srcDeclared) && _dstDeclared) {
1487
- // When source is a union and target input type is known, adopt the input type
1488
- // so validation checks are performed against the target, not an arbitrary variant.
1489
- effectiveTypeId = _dstDeclared;
1490
- }
1491
- else if (_srcDeclared) {
1492
- effectiveTypeId = Array.isArray(_srcDeclared)
1493
- ? _srcDeclared[0]
1494
- : _srcDeclared;
1495
- }
1496
- }
1518
+ // Effective handle declarations
1519
+ const srcEff = effByNodeId.get(e.source.nodeId) || {
1520
+ outputs: {},
1521
+ };
1522
+ const dstEff = effByNodeId.get(e.target.nodeId) || {
1523
+ inputs: {}};
1524
+ const _srcDeclared = srcNode
1525
+ ? srcEff.outputs[e.source.handle]
1526
+ : undefined;
1527
+ const _dstDeclared = dstNode
1528
+ ? getInputTypeId(dstEff.inputs, e.target.handle)
1529
+ : undefined;
1530
+ // Effective edge type
1531
+ const effectiveTypeId = inferEdgeType(_srcDeclared, _dstDeclared, e.typeId);
1497
1532
  const type = effectiveTypeId
1498
1533
  ? this.registry.types.get(effectiveTypeId)
1499
1534
  : undefined;
1500
1535
  if (!type) {
1501
- issues.push({
1502
- level: "error",
1503
- code: "TYPE_MISSING",
1504
- message: `Edge ${e.id} type missing or unknown`,
1505
- data: { edgeId: e.id },
1536
+ pushIssue("error", "TYPE_MISSING", `Edge ${e.id} type missing or unknown`, {
1537
+ edgeId: e.id,
1506
1538
  });
1507
1539
  }
1508
1540
  if (srcNode) {
1509
- const srcType = this.registry.nodes.get(srcNode.typeId);
1510
- if (srcType && !(e.source.handle in srcType.outputs)) {
1511
- issues.push({
1512
- level: "error",
1513
- code: "OUTPUT_MISSING",
1514
- message: `Edge ${e.id} source output ${e.source.handle} missing on ${srcNode.typeId}`,
1515
- data: {
1516
- edgeId: e.id,
1517
- nodeId: srcNode.nodeId,
1518
- output: e.source.handle,
1519
- },
1520
- });
1541
+ if (!(e.source.handle in srcEff.outputs)) {
1542
+ pushIssue("error", "OUTPUT_MISSING", `Edge ${e.id} source output ${e.source.handle} missing on ${srcNode.typeId}`, { edgeId: e.id, nodeId: srcNode.nodeId, output: e.source.handle });
1521
1543
  }
1522
- if (srcType) {
1523
- const declared = srcType.outputs[e.source.handle];
1524
- const declaredArr = Array.isArray(declared)
1525
- ? declared
1526
- : declared
1527
- ? [declared]
1528
- : [];
1529
- if (declaredArr.length > 0 && effectiveTypeId) {
1530
- for (const s of declaredArr) {
1531
- if (s !== effectiveTypeId &&
1532
- !this.registry.canCoerce(s, effectiveTypeId)) {
1533
- issues.push({
1534
- level: "error",
1535
- code: "TYPE_MISMATCH_OUTPUT",
1536
- message: `Edge ${e.id} type ${effectiveTypeId} mismatches source output ${srcNode.typeId}.${e.source.handle} (${s}) and no coercion exists`,
1537
- data: {
1538
- edgeId: e.id,
1539
- nodeId: srcNode.nodeId,
1540
- output: e.source.handle,
1541
- declared: s,
1542
- effectiveTypeId,
1543
- },
1544
- });
1545
- }
1546
- }
1547
- }
1544
+ const declaredArr = normOut(srcEff.outputs[e.source.handle]);
1545
+ if (declaredArr.length > 0 &&
1546
+ effectiveTypeId &&
1547
+ !canFlow(declaredArr, effectiveTypeId)) {
1548
+ pushIssue("error", "TYPE_MISMATCH_OUTPUT", `Edge ${e.id} type ${effectiveTypeId} mismatches source output ${srcNode.typeId}.${e.source.handle} (${declaredArr.join("|")}) and no coercion exists`, {
1549
+ edgeId: e.id,
1550
+ nodeId: srcNode.nodeId,
1551
+ output: e.source.handle,
1552
+ declared: declaredArr.join("|"),
1553
+ effectiveTypeId,
1554
+ });
1548
1555
  }
1549
1556
  }
1550
1557
  if (dstNode) {
1551
- const dstType = this.registry.nodes.get(dstNode.typeId);
1552
- if (dstType && !(e.target.handle in dstType.inputs)) {
1553
- issues.push({
1554
- level: "error",
1555
- code: "INPUT_MISSING",
1556
- message: `Edge ${e.id} target input ${e.target.handle} missing on ${dstNode.typeId}`,
1557
- data: {
1558
- edgeId: e.id,
1559
- nodeId: dstNode.nodeId,
1560
- input: e.target.handle,
1561
- },
1562
- });
1558
+ if (!(e.target.handle in dstEff.inputs)) {
1559
+ pushIssue("error", "INPUT_MISSING", `Edge ${e.id} target input ${e.target.handle} missing on ${dstNode.typeId}`, { edgeId: e.id, nodeId: dstNode.nodeId, input: e.target.handle });
1563
1560
  }
1564
- if (dstType) {
1565
- // Private inputs should not accept edges
1566
- if (isInputPrivate(dstType.inputs, e.target.handle)) {
1567
- issues.push({
1568
- level: "error",
1569
- code: "INPUT_PRIVATE",
1570
- message: `Edge ${e.id} targets private input ${dstNode.typeId}.${e.target.handle}`,
1571
- data: {
1561
+ // Private inputs should not accept edges
1562
+ if (isInputPrivate(dstEff.inputs, e.target.handle)) {
1563
+ pushIssue("error", "INPUT_PRIVATE", `Edge ${e.id} targets private input ${dstNode.typeId}.${e.target.handle}`, { edgeId: e.id, nodeId: dstNode.nodeId, input: e.target.handle });
1564
+ }
1565
+ const declaredIn = getInputTypeId(dstEff.inputs, e.target.handle);
1566
+ if (declaredIn && effectiveTypeId) {
1567
+ if (srcNode) {
1568
+ const srcDeclared = srcEff.outputs[e.source.handle];
1569
+ const srcArr = normOut(srcDeclared).length
1570
+ ? normOut(srcDeclared)
1571
+ : [effectiveTypeId];
1572
+ if (!canFlow(srcArr, declaredIn)) {
1573
+ pushIssue("error", "TYPE_MISMATCH_INPUT", `Edge ${e.id} output type ${srcArr.join("|")} not convertible to target input ${dstNode.typeId}.${e.target.handle} (${declaredIn})`, {
1572
1574
  edgeId: e.id,
1573
1575
  nodeId: dstNode.nodeId,
1574
1576
  input: e.target.handle,
1575
- },
1576
- });
1577
- }
1578
- const declaredIn = getInputTypeId(dstType.inputs, e.target.handle);
1579
- if (declaredIn && effectiveTypeId) {
1580
- // If source is a union, ensure each variant can reach declaredIn
1581
- if (srcNode) {
1582
- const srcType = this.registry.nodes.get(srcNode.typeId);
1583
- const srcDeclared = srcType?.outputs[e.source.handle];
1584
- const srcArr = Array.isArray(srcDeclared)
1585
- ? srcDeclared
1586
- : srcDeclared
1587
- ? [srcDeclared]
1588
- : effectiveTypeId
1589
- ? [effectiveTypeId]
1590
- : [];
1591
- for (const s of srcArr) {
1592
- if (s !== declaredIn &&
1593
- !this.registry.canCoerce(s, declaredIn)) {
1594
- issues.push({
1595
- level: "error",
1596
- code: "TYPE_MISMATCH_INPUT",
1597
- message: `Edge ${e.id} output type ${s} not convertible to target input ${dstNode.typeId}.${e.target.handle} (${declaredIn})`,
1598
- data: {
1599
- edgeId: e.id,
1600
- nodeId: dstNode.nodeId,
1601
- input: e.target.handle,
1602
- declared: declaredIn,
1603
- effectiveTypeId: s,
1604
- },
1605
- });
1606
- }
1607
- }
1608
- }
1609
- else if (declaredIn !== effectiveTypeId &&
1610
- !this.registry.canCoerce(effectiveTypeId, declaredIn)) {
1611
- issues.push({
1612
- level: "error",
1613
- code: "TYPE_MISMATCH_INPUT",
1614
- message: `Edge ${e.id} type ${effectiveTypeId} mismatches target input ${dstNode.typeId}.${e.target.handle} (${declaredIn}) and no coercion exists`,
1615
- data: {
1616
- edgeId: e.id,
1617
- nodeId: dstNode.nodeId,
1618
- input: e.target.handle,
1619
- declared: declaredIn,
1620
- effectiveTypeId,
1621
- },
1577
+ declared: declaredIn,
1578
+ effectiveTypeId: srcArr.join("|"),
1622
1579
  });
1623
1580
  }
1624
1581
  }
1582
+ else if (!canFlow(effectiveTypeId, declaredIn)) {
1583
+ pushIssue("error", "TYPE_MISMATCH_INPUT", `Edge ${e.id} type ${effectiveTypeId} mismatches target input ${dstNode.typeId}.${e.target.handle} (${declaredIn}) and no coercion exists`, {
1584
+ edgeId: e.id,
1585
+ nodeId: dstNode.nodeId,
1586
+ input: e.target.handle,
1587
+ declared: declaredIn,
1588
+ effectiveTypeId,
1589
+ });
1590
+ }
1625
1591
  }
1626
1592
  }
1627
1593
  // Track multiple inbound edges targeting the same input handle
@@ -1629,10 +1595,7 @@ class GraphBuilder {
1629
1595
  inboundCounts.set(inboundKey, (inboundCounts.get(inboundKey) ?? 0) + 1);
1630
1596
  // If the target input is declared as an array type, allow multi-inbound (runtime will append)
1631
1597
  if (dstNode) {
1632
- const dstType = this.registry.nodes.get(dstNode.typeId);
1633
- const declaredIn = dstType
1634
- ? getInputTypeId(dstType.inputs, e.target.handle)
1635
- : undefined;
1598
+ const declaredIn = getInputTypeId((effByNodeId.get(dstNode.nodeId) || { inputs: {} }).inputs, e.target.handle);
1636
1599
  if (typeof declaredIn === "string" && declaredIn.endsWith("[]")) {
1637
1600
  inboundArrayOk.add(inboundKey);
1638
1601
  }
@@ -2635,9 +2598,9 @@ function setupBasicGraphRegistry() {
2635
2598
  categoryId: "compute",
2636
2599
  inputs: { Length: "base.float" },
2637
2600
  outputs: { Items: "base.object" },
2638
- resolveHandles: ({ params }) => {
2601
+ resolveHandles: ({ inputs }) => {
2639
2602
  const maxLen = 64;
2640
- const raw = params?.Length;
2603
+ const raw = inputs?.Length ?? 0;
2641
2604
  const n = Math.max(0, Math.min(maxLen, Math.trunc(Number(raw ?? 0))));
2642
2605
  if (!Number.isFinite(n))
2643
2606
  return { inputs: {} };