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