@bian-womp/spark-graph 0.1.14 → 0.1.16

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.
Files changed (41) hide show
  1. package/lib/cjs/index.cjs +244 -69
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/builder/GraphBuilder.d.ts.map +1 -1
  4. package/lib/cjs/src/builder/Registry.d.ts +3 -3
  5. package/lib/cjs/src/builder/Registry.d.ts.map +1 -1
  6. package/lib/cjs/src/core/types.d.ts +7 -1
  7. package/lib/cjs/src/core/types.d.ts.map +1 -1
  8. package/lib/cjs/src/examples/async.d.ts.map +1 -1
  9. package/lib/cjs/src/examples/engine.d.ts.map +1 -1
  10. package/lib/cjs/src/examples/progress.d.ts.map +1 -1
  11. package/lib/cjs/src/examples/shared.d.ts +2 -6
  12. package/lib/cjs/src/examples/shared.d.ts.map +1 -1
  13. package/lib/cjs/src/examples/simple.d.ts.map +1 -1
  14. package/lib/cjs/src/index.d.ts +3 -1
  15. package/lib/cjs/src/index.d.ts.map +1 -1
  16. package/lib/cjs/src/misc/base.d.ts +7 -0
  17. package/lib/cjs/src/misc/base.d.ts.map +1 -0
  18. package/lib/cjs/src/plugins/composite.d.ts.map +1 -1
  19. package/lib/cjs/src/runtime/GraphRuntime.d.ts +6 -5
  20. package/lib/cjs/src/runtime/GraphRuntime.d.ts.map +1 -1
  21. package/lib/esm/index.js +243 -70
  22. package/lib/esm/index.js.map +1 -1
  23. package/lib/esm/src/builder/GraphBuilder.d.ts.map +1 -1
  24. package/lib/esm/src/builder/Registry.d.ts +3 -3
  25. package/lib/esm/src/builder/Registry.d.ts.map +1 -1
  26. package/lib/esm/src/core/types.d.ts +7 -1
  27. package/lib/esm/src/core/types.d.ts.map +1 -1
  28. package/lib/esm/src/examples/async.d.ts.map +1 -1
  29. package/lib/esm/src/examples/engine.d.ts.map +1 -1
  30. package/lib/esm/src/examples/progress.d.ts.map +1 -1
  31. package/lib/esm/src/examples/shared.d.ts +2 -6
  32. package/lib/esm/src/examples/shared.d.ts.map +1 -1
  33. package/lib/esm/src/examples/simple.d.ts.map +1 -1
  34. package/lib/esm/src/index.d.ts +3 -1
  35. package/lib/esm/src/index.d.ts.map +1 -1
  36. package/lib/esm/src/misc/base.d.ts +7 -0
  37. package/lib/esm/src/misc/base.d.ts.map +1 -0
  38. package/lib/esm/src/plugins/composite.d.ts.map +1 -1
  39. package/lib/esm/src/runtime/GraphRuntime.d.ts +6 -5
  40. package/lib/esm/src/runtime/GraphRuntime.d.ts.map +1 -1
  41. package/package.json +2 -2
package/lib/cjs/index.cjs CHANGED
@@ -372,6 +372,16 @@ class Registry {
372
372
  }
373
373
  }
374
374
 
375
+ function typed(typeId, value) {
376
+ return { __spark_type: typeId, __spark_value: value };
377
+ }
378
+ function isTypedOutput(v) {
379
+ return (!!v &&
380
+ typeof v === "object" &&
381
+ Object.prototype.hasOwnProperty.call(v, "__spark_type") &&
382
+ Object.prototype.hasOwnProperty.call(v, "__spark_value"));
383
+ }
384
+
375
385
  class GraphRuntime {
376
386
  constructor() {
377
387
  this.nodes = new Map();
@@ -435,25 +445,19 @@ class GraphRuntime {
435
445
  srcDeclared = srcDesc.outputs[e.source.handle];
436
446
  }
437
447
  }
438
- if (!effectiveTypeId)
439
- effectiveTypeId = srcDeclared;
448
+ if (!effectiveTypeId) {
449
+ effectiveTypeId = Array.isArray(srcDeclared)
450
+ ? srcDeclared[0]
451
+ : srcDeclared;
452
+ }
440
453
  if (dstNode) {
441
454
  const dstDesc = registry.nodes.get(dstNode.typeId);
442
455
  if (dstDesc) {
443
456
  dstDeclared = dstDesc.inputs[e.target.handle];
444
457
  }
445
458
  }
446
- // Attach convert if source/target differ but coercible
447
- let convert = undefined;
448
- let convertAsync = undefined;
449
- if (srcDeclared && dstDeclared && srcDeclared !== dstDeclared) {
450
- const fn = registry.getCoercion(srcDeclared, dstDeclared);
451
- if (fn)
452
- convert = convert ?? fn;
453
- const afn = registry.getAsyncCoercion(srcDeclared, dstDeclared);
454
- if (afn)
455
- convertAsync = convertAsync ?? afn;
456
- }
459
+ // Attach dynamic convert/convertAsync aware of union sources and typed outputs
460
+ const { convert, convertAsync } = GraphRuntime.buildEdgeConverters(srcDeclared, dstDeclared, registry);
457
461
  return {
458
462
  id: e.id,
459
463
  source: { ...e.source },
@@ -461,6 +465,9 @@ class GraphRuntime {
461
465
  typeId: effectiveTypeId ?? "untyped",
462
466
  convert,
463
467
  convertAsync,
468
+ srcUnionTypes: Array.isArray(srcDeclared)
469
+ ? [...srcDeclared]
470
+ : undefined,
464
471
  stats: { runs: 0, inFlight: false, progress: 0 },
465
472
  };
466
473
  });
@@ -537,6 +544,59 @@ class GraphRuntime {
537
544
  const node = this.nodes.get(nodeId);
538
545
  return node?.outputs[output];
539
546
  }
547
+ static buildEdgeConverters(srcDeclared, dstDeclared, registry) {
548
+ let convert;
549
+ let convertAsync;
550
+ if (dstDeclared && srcDeclared) {
551
+ if (Array.isArray(srcDeclared)) {
552
+ const srcTypes = srcDeclared;
553
+ const anyAsync = srcTypes.some((s) => {
554
+ const res = registry.resolveCoercion(s, dstDeclared);
555
+ return res?.kind === "async";
556
+ });
557
+ if (anyAsync) {
558
+ convertAsync = async (v, signal) => {
559
+ if (!isTypedOutput(v))
560
+ throw new Error(`Typed output required for union source; allowed: ${srcTypes.join("|")}`);
561
+ const typeId = String(v.__spark_type);
562
+ if (!srcTypes.includes(typeId))
563
+ throw new Error(`Invalid typed output ${typeId}; allowed: ${srcTypes.join("|")}`);
564
+ const payload = v.__spark_value;
565
+ const res = registry.resolveCoercion(typeId, dstDeclared);
566
+ if (!res)
567
+ return payload;
568
+ if (res.kind === "async")
569
+ return await res.convertAsync(payload, signal);
570
+ return res.convert(payload);
571
+ };
572
+ }
573
+ else {
574
+ convert = (v) => {
575
+ if (!isTypedOutput(v))
576
+ throw new Error(`Typed output required for union source; allowed: ${srcTypes.join("|")}`);
577
+ const typeId = String(v.__spark_type);
578
+ if (!srcTypes.includes(typeId))
579
+ throw new Error(`Invalid typed output ${typeId}; allowed: ${srcTypes.join("|")}`);
580
+ const payload = v.__spark_value;
581
+ const res = registry.resolveCoercion(typeId, dstDeclared);
582
+ if (!res)
583
+ return payload;
584
+ if (res.kind === "async")
585
+ throw new Error("Async coercion required but convert used");
586
+ return res.convert(payload);
587
+ };
588
+ }
589
+ }
590
+ else {
591
+ const res = registry.resolveCoercion(srcDeclared, dstDeclared);
592
+ if (res?.kind === "async")
593
+ convertAsync = res.convertAsync;
594
+ else if (res?.kind === "sync")
595
+ convert = res.convert;
596
+ }
597
+ }
598
+ return { convert, convertAsync };
599
+ }
540
600
  scheduleInputsChanged(nodeId) {
541
601
  const node = this.nodes.get(nodeId);
542
602
  if (!node)
@@ -708,10 +768,27 @@ class GraphRuntime {
708
768
  handle: srcHandle,
709
769
  value,
710
770
  io: "output",
771
+ runtimeTypeId: isTypedOutput(value)
772
+ ? String(value.__spark_type)
773
+ : undefined,
711
774
  });
712
775
  // fan-out along all edges from this output
713
776
  const outEdges = this.edges.filter((e) => e.source.nodeId === srcNodeId && e.source.handle === srcHandle);
714
777
  for (const e of outEdges) {
778
+ // If source declares a union for this handle, require typed output
779
+ const isUnion = Array.isArray(e.srcUnionTypes);
780
+ const isTyped = isTypedOutput(value);
781
+ if (isUnion && !isTyped) {
782
+ const err = new Error(`Output ${srcNodeId}.${srcHandle} requires typed value for union output (allowed: ${e.srcUnionTypes.join("|")})`);
783
+ this.emit("error", {
784
+ kind: "edge-convert",
785
+ edgeId: e.id,
786
+ source: { nodeId: e.source.nodeId, handle: e.source.handle },
787
+ target: { nodeId: e.target.nodeId, handle: e.target.handle },
788
+ err,
789
+ });
790
+ continue;
791
+ }
715
792
  // Clone per edge to isolate conversions from mutating the shared source value
716
793
  let nextVal = structuredClone(value);
717
794
  const applyToTarget = (v) => {
@@ -725,6 +802,7 @@ class GraphRuntime {
725
802
  handle: e.target.handle,
726
803
  value: v,
727
804
  io: "input",
805
+ runtimeTypeId: isTypedOutput(v) ? String(v.__spark_type) : undefined,
728
806
  });
729
807
  if (!this.paused && this.allInboundHaveValue(e.target.nodeId))
730
808
  this.scheduleInputsChanged(e.target.nodeId);
@@ -743,7 +821,7 @@ class GraphRuntime {
743
821
  e.stats.inFlight = true;
744
822
  e.stats.progress = 0;
745
823
  const sig = controller.signal;
746
- // Fire async conversion
824
+ // Fire async conversion using edge's convertAsync (dynamic union aware)
747
825
  void e
748
826
  .convertAsync(nextVal, sig)
749
827
  .then((v) => {
@@ -1020,24 +1098,18 @@ class GraphRuntime {
1020
1098
  srcDeclared = srcDesc.outputs[e.source.handle];
1021
1099
  }
1022
1100
  }
1023
- if (!effectiveTypeId)
1024
- effectiveTypeId = srcDeclared;
1101
+ if (!effectiveTypeId) {
1102
+ effectiveTypeId = Array.isArray(srcDeclared)
1103
+ ? srcDeclared[0]
1104
+ : srcDeclared;
1105
+ }
1025
1106
  if (dstNode) {
1026
1107
  const dstDesc = registry.nodes.get(dstNode.typeId);
1027
1108
  if (dstDesc) {
1028
1109
  dstDeclared = dstDesc.inputs[e.target.handle];
1029
1110
  }
1030
1111
  }
1031
- let convert = undefined;
1032
- let convertAsync = undefined;
1033
- if (srcDeclared && dstDeclared && srcDeclared !== dstDeclared) {
1034
- const fn = registry.getCoercion(srcDeclared, dstDeclared);
1035
- if (fn)
1036
- convert = convert ?? fn;
1037
- const afn = registry.getAsyncCoercion(srcDeclared, dstDeclared);
1038
- if (afn)
1039
- convertAsync = convertAsync ?? afn;
1040
- }
1112
+ const { convert, convertAsync } = GraphRuntime.buildEdgeConverters(srcDeclared, dstDeclared, registry);
1041
1113
  return {
1042
1114
  id: e.id,
1043
1115
  source: { ...e.source },
@@ -1183,7 +1255,10 @@ class GraphBuilder {
1183
1255
  if (!effectiveTypeId && srcNode) {
1184
1256
  const srcType = this.registry.nodes.get(srcNode.typeId);
1185
1257
  if (srcType) {
1186
- effectiveTypeId = srcType.outputs[e.source.handle];
1258
+ const declared = srcType.outputs[e.source.handle];
1259
+ effectiveTypeId = Array.isArray(declared)
1260
+ ? declared[0]
1261
+ : declared;
1187
1262
  }
1188
1263
  }
1189
1264
  const type = effectiveTypeId
@@ -1213,22 +1288,28 @@ class GraphBuilder {
1213
1288
  }
1214
1289
  if (srcType) {
1215
1290
  const declared = srcType.outputs[e.source.handle];
1216
- if (declared &&
1217
- effectiveTypeId &&
1218
- declared !== effectiveTypeId &&
1219
- !this.registry.canCoerce(declared, effectiveTypeId)) {
1220
- issues.push({
1221
- level: "error",
1222
- code: "TYPE_MISMATCH_OUTPUT",
1223
- message: `Edge ${e.id} type ${effectiveTypeId} mismatches source output ${srcNode.typeId}.${e.source.handle} (${declared}) and no coercion exists`,
1224
- data: {
1225
- edgeId: e.id,
1226
- nodeId: srcNode.nodeId,
1227
- output: e.source.handle,
1228
- declared,
1229
- effectiveTypeId,
1230
- },
1231
- });
1291
+ const declaredArr = Array.isArray(declared)
1292
+ ? declared
1293
+ : declared
1294
+ ? [declared]
1295
+ : [];
1296
+ if (declaredArr.length > 0 && effectiveTypeId) {
1297
+ for (const s of declaredArr) {
1298
+ if (s !== effectiveTypeId && !this.registry.canCoerce(s, effectiveTypeId)) {
1299
+ issues.push({
1300
+ level: "error",
1301
+ code: "TYPE_MISMATCH_OUTPUT",
1302
+ message: `Edge ${e.id} type ${effectiveTypeId} mismatches source output ${srcNode.typeId}.${e.source.handle} (${s}) and no coercion exists`,
1303
+ data: {
1304
+ edgeId: e.id,
1305
+ nodeId: srcNode.nodeId,
1306
+ output: e.source.handle,
1307
+ declared: s,
1308
+ effectiveTypeId,
1309
+ },
1310
+ });
1311
+ }
1312
+ }
1232
1313
  }
1233
1314
  }
1234
1315
  }
@@ -1247,23 +1328,50 @@ class GraphBuilder {
1247
1328
  });
1248
1329
  }
1249
1330
  if (dstType) {
1250
- const declared = dstType.inputs[e.target.handle];
1251
- if (declared &&
1252
- effectiveTypeId &&
1253
- declared !== effectiveTypeId &&
1254
- !this.registry.canCoerce(effectiveTypeId, declared)) {
1255
- issues.push({
1256
- level: "error",
1257
- code: "TYPE_MISMATCH_INPUT",
1258
- message: `Edge ${e.id} type ${effectiveTypeId} mismatches target input ${dstNode.typeId}.${e.target.handle} (${declared}) and no coercion exists`,
1259
- data: {
1260
- edgeId: e.id,
1261
- nodeId: dstNode.nodeId,
1262
- input: e.target.handle,
1263
- declared,
1264
- effectiveTypeId,
1265
- },
1266
- });
1331
+ const declaredIn = dstType.inputs[e.target.handle];
1332
+ if (declaredIn && effectiveTypeId) {
1333
+ // If source is a union, ensure each variant can reach declaredIn
1334
+ if (srcNode) {
1335
+ const srcType = this.registry.nodes.get(srcNode.typeId);
1336
+ const srcDeclared = srcType?.outputs[e.source.handle];
1337
+ const srcArr = Array.isArray(srcDeclared)
1338
+ ? srcDeclared
1339
+ : srcDeclared
1340
+ ? [srcDeclared]
1341
+ : effectiveTypeId
1342
+ ? [effectiveTypeId]
1343
+ : [];
1344
+ for (const s of srcArr) {
1345
+ if (s !== declaredIn && !this.registry.canCoerce(s, declaredIn)) {
1346
+ issues.push({
1347
+ level: "error",
1348
+ code: "TYPE_MISMATCH_INPUT",
1349
+ message: `Edge ${e.id} output type ${s} not convertible to target input ${dstNode.typeId}.${e.target.handle} (${declaredIn})`,
1350
+ data: {
1351
+ edgeId: e.id,
1352
+ nodeId: dstNode.nodeId,
1353
+ input: e.target.handle,
1354
+ declared: declaredIn,
1355
+ effectiveTypeId: s,
1356
+ },
1357
+ });
1358
+ }
1359
+ }
1360
+ }
1361
+ else if (declaredIn !== effectiveTypeId && !this.registry.canCoerce(effectiveTypeId, declaredIn)) {
1362
+ issues.push({
1363
+ level: "error",
1364
+ code: "TYPE_MISMATCH_INPUT",
1365
+ message: `Edge ${e.id} type ${effectiveTypeId} mismatches target input ${dstNode.typeId}.${e.target.handle} (${declaredIn}) and no coercion exists`,
1366
+ data: {
1367
+ edgeId: e.id,
1368
+ nodeId: dstNode.nodeId,
1369
+ input: e.target.handle,
1370
+ declared: declaredIn,
1371
+ effectiveTypeId,
1372
+ },
1373
+ });
1374
+ }
1267
1375
  }
1268
1376
  }
1269
1377
  }
@@ -1304,7 +1412,8 @@ class GraphBuilder {
1304
1412
  ? this.registry.nodes.get(innerNode.typeId)
1305
1413
  : undefined;
1306
1414
  const typeId = innerDesc ? innerDesc.outputs[map.handle] : undefined;
1307
- outputTypes[outerOut] = typeId ?? "untyped";
1415
+ const single = Array.isArray(typeId) ? typeId[0] : typeId;
1416
+ outputTypes[outerOut] = single ?? "untyped";
1308
1417
  }
1309
1418
  return {
1310
1419
  id: nodeTypeId,
@@ -1576,10 +1685,16 @@ const CompositeCategory = (registry) => ({
1576
1685
  onInputsChanged: (inputs, ctx) => {
1577
1686
  if (!inner)
1578
1687
  return;
1579
- // map outer input => inner node input
1688
+ // map outer inputs => batch per inner node to avoid extra runs
1689
+ const grouped = {};
1580
1690
  for (const [inHandle, map] of Object.entries(impl.exposure.inputs)) {
1581
- if (inHandle in inputs)
1582
- inner.setInput(map.nodeId, map.handle, inputs[inHandle]);
1691
+ if (inHandle in inputs) {
1692
+ const nodeMap = (grouped[map.nodeId] = grouped[map.nodeId] || {});
1693
+ nodeMap[map.handle] = inputs[inHandle];
1694
+ }
1695
+ }
1696
+ for (const [nodeId, map] of Object.entries(grouped)) {
1697
+ inner.setInputs(nodeId, map);
1583
1698
  }
1584
1699
  // pull inner exposed outputs and emit
1585
1700
  for (const [outHandle, map] of Object.entries(impl.exposure.outputs)) {
@@ -1622,6 +1737,25 @@ const lcg = (seed) => {
1622
1737
  let s = seed >>> 0 || 1;
1623
1738
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 0xffffffff;
1624
1739
  };
1740
+ // JSON helpers
1741
+ const isPlainObject = (v) => {
1742
+ if (v === null || typeof v !== "object")
1743
+ return false;
1744
+ const proto = Object.getPrototypeOf(v);
1745
+ return proto === Object.prototype || proto === null;
1746
+ };
1747
+ const isJson = (v) => {
1748
+ if (v === null)
1749
+ return true;
1750
+ const t = typeof v;
1751
+ if (t === "string" || t === "number" || t === "boolean")
1752
+ return true;
1753
+ if (Array.isArray(v))
1754
+ return v.every(isJson);
1755
+ if (isPlainObject(v))
1756
+ return Object.values(v).every(isJson);
1757
+ return false;
1758
+ };
1625
1759
  function setupBasicGraphRegistry() {
1626
1760
  const registry = new Registry();
1627
1761
  registry.categories.register(ComputeCategory);
@@ -1637,6 +1771,11 @@ function setupBasicGraphRegistry() {
1637
1771
  id: "base.string",
1638
1772
  validate: (v) => typeof v === "string",
1639
1773
  }, { withArray: true, arrayPickFirstDefined: true });
1774
+ // Generic object value (JSON-compatible; object/array/primitive/null)
1775
+ registry.registerType({
1776
+ id: "base.object",
1777
+ validate: (v) => isJson(v),
1778
+ }, { withArray: true, arrayPickFirstDefined: true });
1640
1779
  registry.registerType({
1641
1780
  id: "base.vec3",
1642
1781
  validate: (v) => Array.isArray(v) &&
@@ -1647,18 +1786,52 @@ function setupBasicGraphRegistry() {
1647
1786
  registry.registerCoercion("base.float", "base.vec3", (v) => {
1648
1787
  return [Number(v) || 0, 0, 0];
1649
1788
  });
1650
- // Async coercion variant for vec3 -> float (chunked + abortable)
1651
- registry.registerAsyncCoercion("base.vec3", "base.float", async (value, signal) => {
1652
- if (signal.aborted)
1653
- throw new DOMException("Aborted", "AbortError");
1789
+ registry.registerCoercion("base.vec3", "base.float", (value) => {
1654
1790
  const v = value;
1655
- await new Promise((r) => setTimeout(r, 1000));
1656
1791
  return Math.hypot(Number(v[0] ?? 0), Number(v[1] ?? 0), Number(v[2] ?? 0));
1657
1792
  });
1658
1793
  registry.registerCoercion("base.bool", "base.float", (v) => (v ? 1 : 0));
1659
1794
  registry.registerCoercion("base.float", "base.bool", (v) => !!v);
1660
1795
  registry.registerCoercion("base.float", "base.string", (v) => String(v));
1661
1796
  registry.registerCoercion("base.string", "base.float", (v) => Number(v));
1797
+ // Object <-> String
1798
+ registry.registerCoercion("base.string", "base.object", (v) => {
1799
+ try {
1800
+ return JSON.parse(String(v));
1801
+ }
1802
+ catch {
1803
+ return undefined;
1804
+ }
1805
+ });
1806
+ registry.registerCoercion("base.object", "base.string", (v) => {
1807
+ try {
1808
+ return JSON.stringify(v);
1809
+ }
1810
+ catch {
1811
+ return String(v);
1812
+ }
1813
+ });
1814
+ registry.registerCoercion("base.vec3", "base.json", (v) => {
1815
+ try {
1816
+ return v ? JSON.stringify(v) : undefined;
1817
+ }
1818
+ catch {
1819
+ return undefined;
1820
+ }
1821
+ });
1822
+ registry.registerCoercion("base.json", "base.vec3", (v) => {
1823
+ try {
1824
+ const result = JSON.parse(v);
1825
+ if (result.length === 3 &&
1826
+ result.every((x) => typeof x === "number")) {
1827
+ return result;
1828
+ }
1829
+ return undefined;
1830
+ }
1831
+ catch {
1832
+ return undefined;
1833
+ }
1834
+ });
1662
1835
  // Enums: Math Operation
1663
1836
  registry.registerEnum({
1664
1837
  id: "base.enum:math.operation",
@@ -2204,6 +2377,8 @@ exports.createSimpleGraphDef = createSimpleGraphDef;
2204
2377
  exports.createSimpleGraphRegistry = createSimpleGraphRegistry;
2205
2378
  exports.createValidationGraphDef = createValidationGraphDef;
2206
2379
  exports.createValidationGraphRegistry = createValidationGraphRegistry;
2380
+ exports.isTypedOutput = isTypedOutput;
2207
2381
  exports.registerDelayNode = registerDelayNode;
2208
2382
  exports.registerProgressNodes = registerProgressNodes;
2383
+ exports.typed = typed;
2209
2384
  //# sourceMappingURL=index.cjs.map