@bian-womp/spark-graph 0.2.8 → 0.2.10

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
@@ -342,7 +342,7 @@ class Registry {
342
342
  }
343
343
  // Enum support
344
344
  registerEnum(desc) {
345
- const { id, displayName, options, opts } = desc;
345
+ const { id, displayName, options, opts, bakeTarget } = desc;
346
346
  const labelType = opts?.labelType ?? "base.string";
347
347
  const valueType = opts?.valueType ?? "base.float";
348
348
  const valueToLabel = new Map();
@@ -361,6 +361,7 @@ class Registry {
361
361
  id,
362
362
  displayName,
363
363
  validate: (v) => typeof v === "number" && valueToLabel.has(Number(v)),
364
+ bakeTarget,
364
365
  }, opts);
365
366
  this.registerSerializer(id, {
366
367
  serialize: (v) => v,
@@ -503,6 +504,7 @@ class GraphRuntime {
503
504
  srcUnionTypes: Array.isArray(srcDeclared)
504
505
  ? [...srcDeclared]
505
506
  : undefined,
507
+ dstDeclared,
506
508
  stats: { runs: 0, inFlight: false, progress: 0 },
507
509
  };
508
510
  });
@@ -838,17 +840,26 @@ class GraphRuntime {
838
840
  const dstNode = this.nodes.get(e.target.nodeId);
839
841
  if (!dstNode)
840
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
+ }
841
852
  const prev = dstNode.inputs[e.target.handle];
842
- const same = this.valuesEqual(prev, v);
853
+ const same = this.valuesEqual(prev, next);
843
854
  if (!same) {
844
- dstNode.inputs[e.target.handle] = v;
855
+ dstNode.inputs[e.target.handle] = next;
845
856
  // Emit value event for input updates
846
857
  this.emit("value", {
847
858
  nodeId: e.target.nodeId,
848
859
  handle: e.target.handle,
849
- value: v,
860
+ value: next,
850
861
  io: "input",
851
- runtimeTypeId: getTypedOutputTypeId(v),
862
+ runtimeTypeId: getTypedOutputTypeId(next),
852
863
  });
853
864
  if (!this.paused && this.allInboundHaveValue(e.target.nodeId))
854
865
  this.scheduleInputsChanged(e.target.nodeId);
@@ -963,6 +974,15 @@ class GraphRuntime {
963
974
  node.stats.progress = Math.max(0, Math.min(1, Number(p) || 0));
964
975
  },
965
976
  };
977
+ // Built-in support: invalidate event reruns or re-emits without per-node wiring
978
+ if (event &&
979
+ typeof event === "object" &&
980
+ event.type === "invalidate") {
981
+ if (this.allInboundHaveValue(nodeId))
982
+ this.scheduleInputsChanged(nodeId);
983
+ else
984
+ this.invalidateDownstream(nodeId);
985
+ }
966
986
  node.runtime.onExternalEvent?.(event, ctx);
967
987
  }
968
988
  dispose() {
@@ -1131,12 +1151,14 @@ class GraphRuntime {
1131
1151
  set.add(e.target.handle);
1132
1152
  prevInbound.set(e.target.nodeId, set);
1133
1153
  }
1134
- // Capture previous outgoing map before rebuilding edges
1135
- const prevOutgoing = new Map();
1154
+ // Capture previous per-handle target sets before rebuilding edges
1155
+ const prevOutTargets = new Map();
1136
1156
  for (const e of this.edges) {
1137
- const set = prevOutgoing.get(e.source.nodeId) ?? new Set();
1138
- set.add(e.source.handle);
1139
- prevOutgoing.set(e.source.nodeId, set);
1157
+ const tmap = prevOutTargets.get(e.source.nodeId) ?? new Map();
1158
+ const tset = tmap.get(e.source.handle) ?? new Set();
1159
+ tset.add(`${e.target.nodeId}.${e.target.handle}`);
1160
+ tmap.set(e.source.handle, tset);
1161
+ prevOutTargets.set(e.source.nodeId, tmap);
1140
1162
  }
1141
1163
  // Rebuild edges mapping with coercions
1142
1164
  this.edges = def.edges.map((e) => {
@@ -1170,6 +1192,7 @@ class GraphRuntime {
1170
1192
  typeId: effectiveTypeId ?? "untyped",
1171
1193
  convert,
1172
1194
  convertAsync,
1195
+ dstDeclared,
1173
1196
  stats: { runs: 0, inFlight: false, progress: 0 },
1174
1197
  };
1175
1198
  });
@@ -1227,14 +1250,16 @@ class GraphRuntime {
1227
1250
  this.scheduleInputsChanged(nodeId);
1228
1251
  }
1229
1252
  }
1230
- // Re-emit outputs only for sources whose outgoing edge set changed
1231
- const nextOutgoing = new Map();
1253
+ // Re-emit outputs when per-handle target sets change (precise and simple)
1254
+ const nextOutTargets = new Map();
1232
1255
  for (const e of this.edges) {
1233
- const set = nextOutgoing.get(e.source.nodeId) ?? new Set();
1234
- set.add(e.source.handle);
1235
- nextOutgoing.set(e.source.nodeId, set);
1256
+ const tmap = nextOutTargets.get(e.source.nodeId) ?? new Map();
1257
+ const tset = tmap.get(e.source.handle) ?? new Set();
1258
+ tset.add(`${e.target.nodeId}.${e.target.handle}`);
1259
+ tmap.set(e.source.handle, tset);
1260
+ nextOutTargets.set(e.source.nodeId, tmap);
1236
1261
  }
1237
- const setsEqual = (a, b) => {
1262
+ const setsEqualStr = (a, b) => {
1238
1263
  if (!a && !b)
1239
1264
  return true;
1240
1265
  if (!a || !b)
@@ -1246,15 +1271,28 @@ class GraphRuntime {
1246
1271
  return false;
1247
1272
  return true;
1248
1273
  };
1249
- const reemitCandidates = new Set([
1250
- ...Array.from(prevOutgoing.keys()),
1251
- ...Array.from(nextOutgoing.keys()),
1274
+ const nodesToCheck = new Set([
1275
+ ...Array.from(prevOutTargets.keys()),
1276
+ ...Array.from(nextOutTargets.keys()),
1252
1277
  ]);
1253
- for (const nodeId of reemitCandidates) {
1254
- const prev = prevOutgoing.get(nodeId);
1255
- const next = nextOutgoing.get(nodeId);
1256
- if (!setsEqual(prev, next))
1257
- this.reemitNodeOutputs(nodeId);
1278
+ for (const nodeId of nodesToCheck) {
1279
+ const pmap = prevOutTargets.get(nodeId) ?? new Map();
1280
+ const nmap = nextOutTargets.get(nodeId) ?? new Map();
1281
+ const handles = new Set([
1282
+ ...Array.from(pmap.keys()),
1283
+ ...Array.from(nmap.keys()),
1284
+ ]);
1285
+ for (const handle of handles) {
1286
+ const pset = pmap.get(handle) ?? new Set();
1287
+ const nset = nmap.get(handle) ?? new Set();
1288
+ if (!setsEqualStr(pset, nset)) {
1289
+ const val = this.getOutput(nodeId, handle);
1290
+ if (val !== undefined)
1291
+ this.propagate(nodeId, handle, val);
1292
+ else if (this.allInboundHaveValue(nodeId))
1293
+ this.scheduleInputsChanged(nodeId);
1294
+ }
1295
+ }
1258
1296
  }
1259
1297
  }
1260
1298
  }
@@ -1300,6 +1338,8 @@ class GraphBuilder {
1300
1338
  }
1301
1339
  // edges validation: nodes exist, handles exist, type exists
1302
1340
  const inboundCounts = new Map();
1341
+ // Track which inbound (nodeId::handle) are declared as array inputs, to allow multi-inbound without warning
1342
+ const inboundArrayOk = new Set();
1303
1343
  for (const e of def.edges) {
1304
1344
  if (edgeIds.has(e.id)) {
1305
1345
  issues.push({
@@ -1340,7 +1380,7 @@ class GraphBuilder {
1340
1380
  if (dstNode) {
1341
1381
  const dstType = this.registry.nodes.get(dstNode.typeId);
1342
1382
  if (dstType)
1343
- _dstDeclared = dstType.inputs[e.target.handle];
1383
+ _dstDeclared = getInputTypeId(dstType.inputs, e.target.handle);
1344
1384
  }
1345
1385
  if (!effectiveTypeId) {
1346
1386
  if (Array.isArray(_srcDeclared) && _dstDeclared) {
@@ -1487,9 +1527,19 @@ class GraphBuilder {
1487
1527
  // Track multiple inbound edges targeting the same input handle
1488
1528
  const inboundKey = `${e.target.nodeId}::${e.target.handle}`;
1489
1529
  inboundCounts.set(inboundKey, (inboundCounts.get(inboundKey) ?? 0) + 1);
1530
+ // If the target input is declared as an array type, allow multi-inbound (runtime will append)
1531
+ if (dstNode) {
1532
+ const dstType = this.registry.nodes.get(dstNode.typeId);
1533
+ const declaredIn = dstType
1534
+ ? getInputTypeId(dstType.inputs, e.target.handle)
1535
+ : undefined;
1536
+ if (typeof declaredIn === "string" && declaredIn.endsWith("[]")) {
1537
+ inboundArrayOk.add(inboundKey);
1538
+ }
1539
+ }
1490
1540
  }
1491
1541
  for (const [key, count] of inboundCounts) {
1492
- if (count > 1) {
1542
+ if (count > 1 && !inboundArrayOk.has(key)) {
1493
1543
  issues.push({
1494
1544
  level: "warning",
1495
1545
  code: "MULTI_INBOUND",
@@ -2002,25 +2052,30 @@ function setupBasicGraphRegistry() {
2002
2052
  registry.registerType({
2003
2053
  id: "base.float",
2004
2054
  validate: (v) => typeof v === "number" && !Number.isNaN(v),
2055
+ bakeTarget: { nodeTypeId: "base.input.number", inputHandle: "Value" },
2005
2056
  }, { withArray: true, arrayPickFirstDefined: true });
2006
2057
  registry.registerType({
2007
2058
  id: "base.bool",
2008
2059
  validate: (v) => typeof v === "boolean",
2060
+ bakeTarget: { nodeTypeId: "base.input.bool", inputHandle: "Value" },
2009
2061
  }, { withArray: true, arrayPickFirstDefined: true });
2010
2062
  registry.registerType({
2011
2063
  id: "base.string",
2012
2064
  validate: (v) => typeof v === "string",
2065
+ bakeTarget: { nodeTypeId: "base.input.string", inputHandle: "Value" },
2013
2066
  }, { withArray: true, arrayPickFirstDefined: true });
2014
2067
  // Generic object value (JSON-compatible; object/array/primitive/null)
2015
2068
  registry.registerType({
2016
2069
  id: "base.object",
2017
2070
  validate: (v) => isJson(v),
2071
+ bakeTarget: { nodeTypeId: "base.input.object", inputHandle: "Value" },
2018
2072
  }, { withArray: false });
2019
2073
  registry.registerType({
2020
2074
  id: "base.vec3",
2021
2075
  validate: (v) => Array.isArray(v) &&
2022
2076
  v.length === 3 &&
2023
2077
  v.every((x) => typeof x === "number"),
2078
+ bakeTarget: { nodeTypeId: "base.input.vec3", inputHandle: "Value" },
2024
2079
  }, { withArray: true, arrayPickFirstDefined: true });
2025
2080
  // float -> vec3 : map x to [x,0,0]
2026
2081
  registry.registerCoercion("base.float", "base.vec3", (v) => {
@@ -2162,6 +2217,20 @@ function setupBasicGraphRegistry() {
2162
2217
  outputs: { Result: "base.bool" },
2163
2218
  impl: (ins) => ({ Result: Boolean(ins.Value) }),
2164
2219
  });
2220
+ registry.registerNode({
2221
+ id: "base.input.object",
2222
+ categoryId: "compute",
2223
+ inputs: { Value: "base.object" },
2224
+ outputs: { Result: "base.object" },
2225
+ impl: (ins) => ({ Result: ins.Value }),
2226
+ });
2227
+ registry.registerNode({
2228
+ id: "base.input.vec3",
2229
+ categoryId: "compute",
2230
+ inputs: { Value: "base.vec3" },
2231
+ outputs: { Result: "base.vec3" },
2232
+ impl: (ins) => ({ Result: ins.Value }),
2233
+ });
2165
2234
  // JSON parser node: base.stringToObject
2166
2235
  registry.registerNode({
2167
2236
  id: "base.string.toObject",