@bian-womp/spark-graph 0.2.21 → 0.2.22

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
@@ -434,9 +434,10 @@ class GraphRuntime {
434
434
  }
435
435
  static create(def, registry, opts) {
436
436
  const gr = new GraphRuntime();
437
+ gr.registry = registry;
437
438
  gr.environment = opts?.environment ?? {};
438
439
  // Precompute per-node resolved handles (use def-provided overrides; do not compute dynamically here)
439
- gr.resolvedByNode = GraphRuntime.computeResolvedHandleMap(def, registry);
440
+ gr.resolvedByNode = GraphRuntime.computeResolvedHandleMap(def, registry, gr.environment);
440
441
  // Instantiate nodes
441
442
  for (const n of def.nodes) {
442
443
  const desc = registry.nodes.get(n.typeId);
@@ -462,8 +463,10 @@ class GraphRuntime {
462
463
  params: n.params,
463
464
  policy: {
464
465
  ...cat.policy,
465
- ...(n.params && n.params.policy ? n.params.policy : {}),
466
+ ...desc.policy,
467
+ ...n.params?.policy,
466
468
  },
469
+ runSeq: 0,
467
470
  activeControllers: new Set(),
468
471
  queue: [],
469
472
  stats: {
@@ -547,6 +550,9 @@ class GraphRuntime {
547
550
  // Only schedule if all inbound inputs are present (or there are none)
548
551
  if (anyChanged && this.allInboundHaveValue(nodeId))
549
552
  this.scheduleInputsChanged(nodeId);
553
+ // Recompute dynamic handles for this node when its direct inputs change
554
+ if (anyChanged)
555
+ this.scheduleRecomputeHandles(nodeId);
550
556
  }
551
557
  }
552
558
  getOutput(nodeId, output) {
@@ -619,12 +625,14 @@ class GraphRuntime {
619
625
  now - node.lastScheduledAt < policy.debounceMs) {
620
626
  // debounce: replace latest queued
621
627
  node.queue.splice(0, node.queue.length);
622
- const rid = `${nodeId}:${now}`;
628
+ node.runSeq += 1;
629
+ const rid = `${nodeId}:${node.runSeq}:${now}`;
623
630
  node.queue.push({ runId: rid, inputs: { ...node.inputs } });
624
631
  return;
625
632
  }
626
633
  node.lastScheduledAt = now;
627
- const rid = `${nodeId}:${now}:${Math.random().toString(36).slice(2, 8)}`;
634
+ node.runSeq += 1;
635
+ const rid = `${nodeId}:${node.runSeq}:${now}`;
628
636
  node.latestRunId = rid;
629
637
  const startRun = (runId, capturedInputs, onDone) => {
630
638
  const controller = new AbortController();
@@ -847,6 +855,8 @@ class GraphRuntime {
847
855
  io: "input",
848
856
  runtimeTypeId: getTypedOutputTypeId(next),
849
857
  });
858
+ // Recompute dynamic handles for the destination node on input change
859
+ this.scheduleRecomputeHandles(e.target.nodeId);
850
860
  if (!this.paused && this.allInboundHaveValue(e.target.nodeId))
851
861
  this.scheduleInputsChanged(e.target.nodeId);
852
862
  }
@@ -904,7 +914,7 @@ class GraphRuntime {
904
914
  }
905
915
  }
906
916
  // Helper: build map of resolved handles per node from def (prefer def.resolvedHandles, otherwise registry statics)
907
- static computeResolvedHandleMap(def, registry) {
917
+ static computeResolvedHandleMap(def, registry, environment) {
908
918
  const out = new Map();
909
919
  for (const n of def.nodes) {
910
920
  const desc = registry.nodes.get(n.typeId);
@@ -913,10 +923,36 @@ class GraphRuntime {
913
923
  const overrideInputs = n.resolvedHandles?.inputs;
914
924
  const overrideOutputs = n.resolvedHandles?.outputs;
915
925
  const overrideDefaults = n.resolvedHandles?.inputDefaults;
916
- // Merge base with overrides (allow partial resolvedHandles)
917
- const inputs = { ...desc.inputs, ...overrideInputs };
918
- const outputs = { ...desc.outputs, ...overrideOutputs };
919
- const inputDefaults = { ...desc.inputDefaults, ...overrideDefaults };
926
+ // Resolve dynamic handles if available (initial pass: inputs may be undefined)
927
+ let dyn = {};
928
+ try {
929
+ if (typeof desc.resolveHandles === "function") {
930
+ dyn = desc.resolveHandles({
931
+ environment: environment || {},
932
+ params: n.params,
933
+ inputs: undefined,
934
+ });
935
+ }
936
+ }
937
+ catch {
938
+ // ignore dynamic resolution errors at this stage
939
+ }
940
+ // Merge base with dynamic and overrides (allow partial resolvedHandles)
941
+ const inputs = {
942
+ ...desc.inputs,
943
+ ...dyn.inputs,
944
+ ...overrideInputs,
945
+ };
946
+ const outputs = {
947
+ ...desc.outputs,
948
+ ...dyn.outputs,
949
+ ...overrideOutputs,
950
+ };
951
+ const inputDefaults = {
952
+ ...desc.inputDefaults,
953
+ ...dyn.inputDefaults,
954
+ ...overrideDefaults,
955
+ };
920
956
  out.set(n.nodeId, { inputs, outputs, inputDefaults });
921
957
  }
922
958
  return out;
@@ -1098,6 +1134,10 @@ class GraphRuntime {
1098
1134
  }
1099
1135
  setEnvironment(env) {
1100
1136
  this.environment = { ...env };
1137
+ // Recompute dynamic handles for all nodes when environment changes
1138
+ for (const nodeId of this.nodes.keys()) {
1139
+ this.scheduleRecomputeHandles(nodeId);
1140
+ }
1101
1141
  }
1102
1142
  // Export a GraphDefinition reflecting the current runtime view
1103
1143
  getGraphDef() {
@@ -1254,8 +1294,10 @@ class GraphRuntime {
1254
1294
  params: n.params,
1255
1295
  policy: {
1256
1296
  ...cat.policy,
1257
- ...(n.params && n.params.policy ? n.params.policy : {}),
1297
+ ...desc.policy,
1298
+ ...n.params?.policy,
1258
1299
  },
1300
+ runSeq: 0,
1259
1301
  activeControllers: new Set(),
1260
1302
  queue: [],
1261
1303
  stats: {
@@ -1314,8 +1356,8 @@ class GraphRuntime {
1314
1356
  tmap.set(e.source.handle, tset);
1315
1357
  prevOutTargets.set(e.source.nodeId, tmap);
1316
1358
  }
1317
- // Precompute per-node resolved handles for updated graph
1318
- this.resolvedByNode = GraphRuntime.computeResolvedHandleMap(def, registry);
1359
+ // Precompute per-node resolved handles for updated graph (include dynamic)
1360
+ this.resolvedByNode = GraphRuntime.computeResolvedHandleMap(def, registry, this.environment);
1319
1361
  // Rebuild edges mapping with coercions
1320
1362
  this.edges = GraphRuntime.buildEdges(def, registry, this.resolvedByNode);
1321
1363
  // Build new inbound map
@@ -1475,6 +1517,83 @@ class GraphRuntime {
1475
1517
  this.arrayInputBuckets.delete(nodeId);
1476
1518
  }
1477
1519
  }
1520
+ // Schedule a recomputation of dynamic handles for a node (async to avoid mutating during propagation)
1521
+ scheduleRecomputeHandles(nodeId) {
1522
+ // If no registry or node not found, skip
1523
+ if (!this.registry)
1524
+ return;
1525
+ const node = this.nodes.get(nodeId);
1526
+ if (!node)
1527
+ return;
1528
+ setTimeout(() => {
1529
+ try {
1530
+ this.recomputeHandlesForNode(nodeId);
1531
+ }
1532
+ catch {
1533
+ // ignore recompute errors
1534
+ }
1535
+ }, 0);
1536
+ }
1537
+ // Recompute dynamic handles for a single node using current inputs/environment
1538
+ recomputeHandlesForNode(nodeId) {
1539
+ const registry = this.registry;
1540
+ const node = this.nodes.get(nodeId);
1541
+ if (!node)
1542
+ return;
1543
+ const desc = registry.nodes.get(node.typeId);
1544
+ if (!desc)
1545
+ return;
1546
+ const resolveHandles = desc.resolveHandles;
1547
+ if (typeof resolveHandles !== "function")
1548
+ return;
1549
+ let r;
1550
+ try {
1551
+ r = resolveHandles({
1552
+ environment: this.environment || {},
1553
+ params: node.params,
1554
+ inputs: node.inputs || {},
1555
+ });
1556
+ }
1557
+ catch {
1558
+ return;
1559
+ }
1560
+ const inputs = { ...desc.inputs, ...r?.inputs };
1561
+ const outputs = { ...desc.outputs, ...r?.outputs };
1562
+ const inputDefaults = { ...desc.inputDefaults, ...r?.inputDefaults };
1563
+ const next = { inputs, outputs, inputDefaults };
1564
+ const before = this.resolvedByNode.get(nodeId);
1565
+ // Compare shallow-structurally via JSON
1566
+ if (JSON.stringify(before) === JSON.stringify(next))
1567
+ return;
1568
+ this.resolvedByNode.set(nodeId, next);
1569
+ this.updateNodeHandles(nodeId, next, registry);
1570
+ // Seed defaults for newly introduced inputs that are not inbound
1571
+ const inbound = this.edges
1572
+ .filter((e) => e.target.nodeId === nodeId)
1573
+ .map((e) => e.target.handle);
1574
+ for (const [handle, value] of Object.entries(inputDefaults)) {
1575
+ if (value === undefined)
1576
+ continue;
1577
+ if (inbound.includes(handle))
1578
+ continue;
1579
+ if (node.inputs[handle] === undefined) {
1580
+ node.inputs[handle] =
1581
+ typeof structuredClone === "function"
1582
+ ? structuredClone(value)
1583
+ : JSON.parse(JSON.stringify(value));
1584
+ // Emit input value event for seeded defaults
1585
+ this.emit("value", {
1586
+ nodeId,
1587
+ handle,
1588
+ value: node.inputs[handle],
1589
+ io: "input",
1590
+ runtimeTypeId: getTypedOutputTypeId(node.inputs[handle]),
1591
+ });
1592
+ }
1593
+ }
1594
+ // Notify graph updated for UI parity
1595
+ this.emit("invalidate", { reason: "graph-updated" });
1596
+ }
1478
1597
  }
1479
1598
 
1480
1599
  class GraphBuilder {
@@ -1950,7 +2069,7 @@ const ComputeCategory = {
1950
2069
  }
1951
2070
  },
1952
2071
  }),
1953
- policy: { mode: "push", asyncConcurrency: "switch" },
2072
+ policy: { asyncConcurrency: "switch" },
1954
2073
  };
1955
2074
 
1956
2075
  const CompositeCategory = (registry) => ({
@@ -2006,7 +2125,6 @@ const CompositeCategory = (registry) => ({
2006
2125
  },
2007
2126
  };
2008
2127
  },
2009
- policy: { mode: "hybrid" },
2010
2128
  });
2011
2129
 
2012
2130
  // Helpers
@@ -3100,6 +3218,39 @@ function registerProgressNodes(registry) {
3100
3218
  });
3101
3219
  }
3102
3220
 
3221
+ function installLogging(engine) {
3222
+ engine.on("value", (e) => {
3223
+ const t = e.runtimeTypeId ? ` <${e.runtimeTypeId}>` : "";
3224
+ console.log(`[value:${e.io}]`, `${e.nodeId}.${e.handle}`, e.value, t);
3225
+ });
3226
+ engine.on("stats", (s) => {
3227
+ if (s.kind === "node-progress") {
3228
+ const pct = Math.round((s.progress ?? 0) * 100);
3229
+ console.log(`[progress] ${s.runId || s.nodeId}: ${pct}%`);
3230
+ }
3231
+ else if (s.kind === "node-done") {
3232
+ console.log(`[done] ${s.runId || s.nodeId} in ${s.durationMs ?? 0}ms`);
3233
+ }
3234
+ else if (s.kind === "node-start") {
3235
+ console.log(`[start] ${s.runId || s.nodeId}`);
3236
+ }
3237
+ else if (s.kind === "edge-start") {
3238
+ console.log(`[edge] ${s.source.nodeId}.${s.source.handle} -> ${s.target.nodeId}.${s.target.handle}`);
3239
+ }
3240
+ else if (s.kind === "edge-done") {
3241
+ console.log(`[edge] ${s.source.nodeId}.${s.source.handle} -> ${s.target.nodeId}.${s.target.handle} in ${s.durationMs ?? 0}ms`);
3242
+ }
3243
+ });
3244
+ engine.on("error", (e) => {
3245
+ if (e.kind === "node-run") {
3246
+ console.warn(`[error] ${e.runId || e.nodeId}`, e.err?.message ?? e.err);
3247
+ }
3248
+ else if (e.kind === "edge-convert") {
3249
+ console.warn(`[error] ${e.edgeId} ${e.source.nodeId}.${e.source.handle} -> ${e.target.nodeId}.${e.target.handle}`, e.err?.message ?? e.err);
3250
+ }
3251
+ });
3252
+ }
3253
+
3103
3254
  function makeBasicGraphDefinition() {
3104
3255
  return {
3105
3256
  nodes: [
@@ -3303,6 +3454,7 @@ exports.createValidationGraphRegistry = createValidationGraphRegistry;
3303
3454
  exports.getInputTypeId = getInputTypeId;
3304
3455
  exports.getTypedOutputTypeId = getTypedOutputTypeId;
3305
3456
  exports.getTypedOutputValue = getTypedOutputValue;
3457
+ exports.installLogging = installLogging;
3306
3458
  exports.isInputPrivate = isInputPrivate;
3307
3459
  exports.isTypedOutput = isTypedOutput;
3308
3460
  exports.registerDelayNode = registerDelayNode;