@bian-womp/spark-workbench 0.3.1 → 0.3.3

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 (45) hide show
  1. package/lib/cjs/index.cjs +265 -148
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/core/InMemoryWorkbench.d.ts +53 -0
  4. package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
  5. package/lib/cjs/src/core/contracts.d.ts +16 -0
  6. package/lib/cjs/src/core/contracts.d.ts.map +1 -1
  7. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  8. package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -1
  9. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +0 -3
  10. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  11. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  12. package/lib/cjs/src/misc/load.d.ts.map +1 -1
  13. package/lib/cjs/src/misc/mapping.d.ts +7 -0
  14. package/lib/cjs/src/misc/mapping.d.ts.map +1 -1
  15. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts +6 -4
  16. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
  17. package/lib/cjs/src/runtime/IGraphRunner.d.ts +5 -3
  18. package/lib/cjs/src/runtime/IGraphRunner.d.ts.map +1 -1
  19. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts +9 -2
  20. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts.map +1 -1
  21. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +4 -0
  22. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  23. package/lib/esm/index.js +266 -149
  24. package/lib/esm/index.js.map +1 -1
  25. package/lib/esm/src/core/InMemoryWorkbench.d.ts +53 -0
  26. package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
  27. package/lib/esm/src/core/contracts.d.ts +16 -0
  28. package/lib/esm/src/core/contracts.d.ts.map +1 -1
  29. package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  30. package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -1
  31. package/lib/esm/src/misc/context/WorkbenchContext.d.ts +0 -3
  32. package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  33. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  34. package/lib/esm/src/misc/load.d.ts.map +1 -1
  35. package/lib/esm/src/misc/mapping.d.ts +7 -0
  36. package/lib/esm/src/misc/mapping.d.ts.map +1 -1
  37. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts +6 -4
  38. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
  39. package/lib/esm/src/runtime/IGraphRunner.d.ts +5 -3
  40. package/lib/esm/src/runtime/IGraphRunner.d.ts.map +1 -1
  41. package/lib/esm/src/runtime/LocalGraphRunner.d.ts +9 -2
  42. package/lib/esm/src/runtime/LocalGraphRunner.d.ts.map +1 -1
  43. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +4 -0
  44. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  45. package/package.json +4 -4
package/lib/esm/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { generateId, createSimpleGraphRegistry, GraphBuilder, getTypedOutputValue, isTypedOutput, getInputTypeId, UnifiedEngine, getTypedOutputTypeId, isInputPrivate, offsetImportedPositions, createSimpleGraphDef, createAsyncGraphDef, createAsyncGraphRegistry, createProgressGraphDef, createProgressGraphRegistry, createValidationGraphDef, createValidationGraphRegistry } from '@bian-womp/spark-graph';
1
+ import { generateId, createSimpleGraphRegistry, GraphBuilder, getTypedOutputValue, isTypedOutput, getInputTypeId, LocalEngine, parseJsonPath, setValueAtPathWithCreation, getTypedOutputTypeId, isInputPrivate, offsetImportedPositions, createSimpleGraphDef, createAsyncGraphDef, createAsyncGraphRegistry, createProgressGraphDef, createProgressGraphRegistry, createValidationGraphDef, createValidationGraphRegistry } from '@bian-womp/spark-graph';
2
2
  import lod from 'lodash';
3
3
  import { RemoteRuntimeClient } from '@bian-womp/spark-remote';
4
4
  import { Position, Handle, NodeResizer, getBezierPath, BaseEdge, useReactFlow, ReactFlowProvider, ReactFlow, Background, BackgroundVariant, MiniMap, Controls } from '@xyflow/react';
@@ -144,6 +144,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
144
144
  edges: [],
145
145
  };
146
146
  this.nodeNames = {};
147
+ this.customData = {};
147
148
  this.runtimeState = null;
148
149
  this.viewport = null;
149
150
  this.historyState = undefined;
@@ -172,6 +173,17 @@ class InMemoryWorkbench extends AbstractWorkbench {
172
173
  const filteredSizes = Object.fromEntries(Object.entries(this.sizes).filter(([id]) => defNodeIds.has(id)));
173
174
  const filteredNodes = this.selection.nodes.filter((id) => defNodeIds.has(id));
174
175
  const filteredEdges = this.selection.edges.filter((id) => defEdgeIds.has(id));
176
+ // Clean up extData for removed nodes/edges
177
+ if (this.customData.nodes) {
178
+ const filteredExtNodes = Object.fromEntries(Object.entries(this.customData.nodes).filter(([id]) => defNodeIds.has(id)));
179
+ this.customData.nodes =
180
+ Object.keys(filteredExtNodes).length > 0 ? filteredExtNodes : undefined;
181
+ }
182
+ if (this.customData.edges) {
183
+ const filteredExtEdges = Object.fromEntries(Object.entries(this.customData.edges).filter(([id]) => defEdgeIds.has(id)));
184
+ this.customData.edges =
185
+ Object.keys(filteredExtEdges).length > 0 ? filteredExtEdges : undefined;
186
+ }
175
187
  this.positions = filteredPositions;
176
188
  this.sizes = filteredSizes;
177
189
  this.selection = { nodes: filteredNodes, edges: filteredEdges };
@@ -849,6 +861,107 @@ class InMemoryWorkbench extends AbstractWorkbench {
849
861
  ...options,
850
862
  });
851
863
  }
864
+ /**
865
+ * Get custom data for a specific node.
866
+ */
867
+ getCustomNodeData(nodeId) {
868
+ return this.customData.nodes?.[nodeId];
869
+ }
870
+ /**
871
+ * Set custom data for a specific node.
872
+ */
873
+ setCustomNodeData(nodeId, data, options) {
874
+ if (!this.customData.nodes) {
875
+ this.customData.nodes = {};
876
+ }
877
+ if (data === undefined) {
878
+ delete this.customData.nodes[nodeId];
879
+ if (Object.keys(this.customData.nodes).length === 0) {
880
+ delete this.customData.nodes;
881
+ }
882
+ }
883
+ else {
884
+ this.customData.nodes[nodeId] = data;
885
+ }
886
+ this.emit("graphUiChanged", {
887
+ change: { type: "customNodeData", nodeId, data },
888
+ ...options,
889
+ });
890
+ }
891
+ /**
892
+ * Get custom data for a specific edge.
893
+ */
894
+ getCustomEdgeData(edgeId) {
895
+ return this.customData.edges?.[edgeId];
896
+ }
897
+ /**
898
+ * Set custom data for a specific edge.
899
+ */
900
+ setCustomEdgeData(edgeId, data, options) {
901
+ if (!this.customData.edges) {
902
+ this.customData.edges = {};
903
+ }
904
+ if (data === undefined) {
905
+ delete this.customData.edges[edgeId];
906
+ if (Object.keys(this.customData.edges).length === 0) {
907
+ delete this.customData.edges;
908
+ }
909
+ }
910
+ else {
911
+ this.customData.edges[edgeId] = data;
912
+ }
913
+ this.emit("graphUiChanged", {
914
+ change: { type: "customEdgeData", edgeId, data },
915
+ ...options,
916
+ });
917
+ }
918
+ /**
919
+ * Get custom metadata.
920
+ */
921
+ getCustomMetaData() {
922
+ return this.customData.meta;
923
+ }
924
+ /**
925
+ * Set custom metadata.
926
+ */
927
+ setCustomMetaData(meta, options) {
928
+ if (meta === undefined) {
929
+ delete this.customData.meta;
930
+ }
931
+ else {
932
+ this.customData.meta = meta;
933
+ }
934
+ this.emit("graphUiChanged", {
935
+ change: { type: "customMetaData", meta },
936
+ ...options,
937
+ });
938
+ }
939
+ /**
940
+ * Get all custom data.
941
+ */
942
+ getCustomData() {
943
+ return { ...this.customData };
944
+ }
945
+ /**
946
+ * Set all custom data.
947
+ */
948
+ setCustomData(custom, options) {
949
+ if (custom === undefined) {
950
+ this.customData = {};
951
+ }
952
+ else {
953
+ this.customData = lod.pick(custom, ["nodes", "edges", "meta"]);
954
+ }
955
+ this.emit("graphUiChanged", {
956
+ change: {
957
+ type: "customData",
958
+ nodes: custom?.nodes,
959
+ edges: custom?.edges,
960
+ meta: custom?.meta,
961
+ },
962
+ ...options,
963
+ });
964
+ }
852
965
  }
853
966
 
854
967
  class CLIWorkbench {
@@ -909,36 +1022,14 @@ class AbstractGraphRunner {
909
1022
  this.stagedInputs = {};
910
1023
  this.runnerId = "";
911
1024
  }
912
- launch(def, opts) {
913
- // Auto-stop if engine is already running
914
- if (this.engine) {
915
- this.stop();
916
- }
917
- }
918
1025
  async whenIdle() {
919
1026
  await this.engine?.whenIdle();
920
1027
  }
921
- stop() {
922
- if (!this.engine)
923
- return;
924
- // Dispose engine (cleans up timers, listeners, etc.)
925
- this.engine.dispose();
926
- this.engine = undefined;
927
- // Emit status but keep runtime alive
928
- if (this.runMode) {
929
- this.runMode = undefined;
930
- this.emit("status", { running: false, runMode: undefined });
931
- }
932
- }
933
1028
  setRunMode(runMode) {
934
- if (!this.engine) {
935
- throw new Error("Cannot set run mode: engine not running");
1029
+ if (this.engine) {
1030
+ this.engine.setRunMode(runMode);
1031
+ this.emit("status", { running: true, runMode });
936
1032
  }
937
- // Update engine run mode (this will update pause/resume state)
938
- this.engine.setRunMode(runMode);
939
- // Update local state and emit status event
940
- this.runMode = runMode;
941
- this.emit("status", { running: true, runMode: this.runMode });
942
1033
  }
943
1034
  getInputDefaults(def) {
944
1035
  const out = {};
@@ -968,17 +1059,10 @@ class AbstractGraphRunner {
968
1059
  this.engine = undefined;
969
1060
  this.runtime?.dispose();
970
1061
  this.runtime = undefined;
971
- if (this.runMode) {
972
- this.runMode = undefined;
973
- this.emit("status", { running: false, runMode: undefined });
974
- }
975
1062
  }
976
1063
  isRunning() {
977
1064
  return !!this.engine;
978
1065
  }
979
- getRunMode() {
980
- return this.runMode;
981
- }
982
1066
  // Optional undo/redo support
983
1067
  async undo() {
984
1068
  return false;
@@ -997,12 +1081,12 @@ let localRunnerCounter = 0;
997
1081
  class LocalGraphRunner extends AbstractGraphRunner {
998
1082
  constructor(registry) {
999
1083
  super(registry, { kind: "local" });
1084
+ this.extData = {};
1000
1085
  this.setEnvironment = (env, opts) => {
1001
1086
  if (!this.runtime)
1002
1087
  return;
1003
- const wasPaused = this.runtime.isPaused();
1004
- if (opts?.dry && !wasPaused)
1005
- this.runtime.pause();
1088
+ // Use requestPause for dry mode to temporarily pause without affecting base run mode
1089
+ const releasePause = opts?.dry ? this.runtime.requestPause() : null;
1006
1090
  try {
1007
1091
  if (opts?.merge) {
1008
1092
  const current = this.runtime.getEnvironment();
@@ -1014,8 +1098,7 @@ class LocalGraphRunner extends AbstractGraphRunner {
1014
1098
  }
1015
1099
  }
1016
1100
  finally {
1017
- if (opts?.dry && !wasPaused)
1018
- this.runtime.resume();
1101
+ releasePause?.();
1019
1102
  }
1020
1103
  };
1021
1104
  this.getEnvironment = () => {
@@ -1036,24 +1119,21 @@ class LocalGraphRunner extends AbstractGraphRunner {
1036
1119
  update(def, options) {
1037
1120
  if (!this.runtime)
1038
1121
  return;
1039
- const wasPaused = this.runtime.isPaused();
1040
- // Pause runtime if dry option is set (to prevent execution) or if not paused already
1041
- if (options?.dry && !wasPaused) {
1042
- this.runtime.pause();
1043
- }
1122
+ // Use requestPause for dry mode to temporarily pause without affecting base run mode
1123
+ const releasePause = options?.dry ? this.runtime.requestPause() : null;
1044
1124
  try {
1045
1125
  this.runtime.update(def, this.registry);
1046
1126
  this.emit("invalidate", { reason: "graph-updated" });
1047
1127
  }
1048
1128
  finally {
1049
- // Resume only if we paused it due to dry option
1050
- if (options?.dry && !wasPaused) {
1051
- this.runtime.resume();
1052
- }
1129
+ releasePause?.();
1053
1130
  }
1054
1131
  }
1055
1132
  launch(def, opts) {
1056
- super.launch(def, opts);
1133
+ if (this.engine) {
1134
+ this.engine.dispose();
1135
+ this.engine = undefined;
1136
+ }
1057
1137
  this.build(def);
1058
1138
  if (!this.runtime)
1059
1139
  throw new Error("Runtime not built");
@@ -1072,7 +1152,7 @@ class LocalGraphRunner extends AbstractGraphRunner {
1072
1152
  if (!this.runtime)
1073
1153
  throw new Error("Runtime not built");
1074
1154
  // Use shared engine factory
1075
- this.engine = new UnifiedEngine(this.runtime, opts?.runMode);
1155
+ this.engine = new LocalEngine(this.runtime, opts?.runMode);
1076
1156
  if (!this.engine)
1077
1157
  throw new Error("Failed to create engine");
1078
1158
  this.engine.on("value", (e) => this.emit("value", e));
@@ -1080,8 +1160,8 @@ class LocalGraphRunner extends AbstractGraphRunner {
1080
1160
  this.engine.on("invalidate", (e) => this.emit("invalidate", e));
1081
1161
  this.engine.on("stats", (e) => this.emit("stats", e));
1082
1162
  this.engine.launch(opts?.invalidate);
1083
- this.runMode = opts?.runMode ?? "manual";
1084
- this.emit("status", { running: true, runMode: this.runMode });
1163
+ const runMode = opts?.runMode ?? "manual";
1164
+ this.emit("status", { running: true, runMode });
1085
1165
  for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
1086
1166
  this.engine.setInputs(nodeId, map);
1087
1167
  }
@@ -1175,6 +1255,24 @@ class LocalGraphRunner extends AbstractGraphRunner {
1175
1255
  this.engine.copyOutputs(fromNodeId, toNodeId, options);
1176
1256
  }
1177
1257
  }
1258
+ async setExtData(data) {
1259
+ if (!data || typeof data !== "object") {
1260
+ this.extData = {};
1261
+ return;
1262
+ }
1263
+ this.extData = { ...this.extData, ...data };
1264
+ }
1265
+ async updateExtData(updates) {
1266
+ if (!this.extData ||
1267
+ typeof this.extData !== "object" ||
1268
+ Array.isArray(this.extData)) {
1269
+ this.extData = {};
1270
+ }
1271
+ for (const { path, value } of updates) {
1272
+ const pathSegments = parseJsonPath(path);
1273
+ setValueAtPathWithCreation(this.extData, pathSegments, value);
1274
+ }
1275
+ }
1178
1276
  async snapshotFull() {
1179
1277
  const def = undefined; // UI will supply def/positions on download for local
1180
1278
  const inputs = this.getInputs(this.runtime
@@ -1196,7 +1294,8 @@ class LocalGraphRunner extends AbstractGraphRunner {
1196
1294
  }
1197
1295
  : { nodes: [], edges: [] });
1198
1296
  const environment = this.getEnvironment() || {};
1199
- return { def, environment, inputs, outputs };
1297
+ const extData = this.extData;
1298
+ return { def, environment, inputs, outputs, extData };
1200
1299
  }
1201
1300
  async applySnapshotFull(payload, options) {
1202
1301
  if (payload.def && !options?.skipBuild) {
@@ -1204,6 +1303,9 @@ class LocalGraphRunner extends AbstractGraphRunner {
1204
1303
  }
1205
1304
  this.setEnvironment?.(payload.environment || {}, { merge: false });
1206
1305
  this.hydrate(payload, { dry: options?.dry });
1306
+ if (payload.extData) {
1307
+ await this.setExtData(payload.extData);
1308
+ }
1207
1309
  }
1208
1310
  hydrate(snapshot, opts) {
1209
1311
  // Hydrate via runtime for exact restore (this emits events on runtime emitter)
@@ -1573,7 +1675,10 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1573
1675
  }
1574
1676
  }
1575
1677
  launch(def, opts) {
1576
- super.launch(def, opts);
1678
+ if (this.engine) {
1679
+ this.engine.dispose();
1680
+ this.engine = undefined;
1681
+ }
1577
1682
  // Remote: build remotely then launch
1578
1683
  this.ensureClient().then(async (client) => {
1579
1684
  await client.api.build(def);
@@ -1629,8 +1734,8 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1629
1734
  this.listenersBound = true;
1630
1735
  }
1631
1736
  this.engine = eng;
1632
- this.runMode = opts?.runMode ?? "manual";
1633
- this.emit("status", { running: true, runMode: this.runMode });
1737
+ const runMode = opts?.runMode ?? "manual";
1738
+ this.emit("status", { running: true, runMode });
1634
1739
  // Re-apply staged inputs using client.setInputs for consistency
1635
1740
  for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
1636
1741
  await eng.setInputs(nodeId, map, undefined).catch(() => {
@@ -1646,7 +1751,10 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1646
1751
  * the runtime state that was just restored.
1647
1752
  */
1648
1753
  launchExisting(def, opts) {
1649
- super.launch(def, opts);
1754
+ if (this.engine) {
1755
+ this.engine.dispose();
1756
+ this.engine = undefined;
1757
+ }
1650
1758
  // Remote: attach to existing runtime and launch (do NOT rebuild)
1651
1759
  this.ensureClient().then(async (client) => {
1652
1760
  // NOTE: We do NOT call client.build(def) here because the backend runtime
@@ -1657,14 +1765,10 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1657
1765
  });
1658
1766
  }
1659
1767
  setRunMode(runMode) {
1660
- if (!this.engine) {
1661
- throw new Error("Cannot set run mode: engine not running");
1768
+ if (this.engine) {
1769
+ this.engine.setRunMode(runMode);
1770
+ this.emit("status", { running: true, runMode });
1662
1771
  }
1663
- // Update engine run mode (sends SetRunMode command to backend)
1664
- this.engine.setRunMode(runMode);
1665
- // Update local state and emit status event
1666
- this.runMode = runMode;
1667
- this.emit("status", { running: true, runMode: this.runMode });
1668
1772
  }
1669
1773
  async computeNode(nodeId, options) {
1670
1774
  const client = await this.ensureClient();
@@ -1727,6 +1831,10 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1727
1831
  const client = await this.ensureClient();
1728
1832
  await client.api.setExtData(data);
1729
1833
  }
1834
+ async updateExtData(updates) {
1835
+ const client = await this.ensureClient();
1836
+ await client.api.updateExtData(updates);
1837
+ }
1730
1838
  async commit(reason) {
1731
1839
  const client = await this.ensureClient();
1732
1840
  try {
@@ -2598,35 +2706,40 @@ function toReactFlow(def, positions, sizes, registry, opts) {
2598
2706
  }));
2599
2707
  const handleLayout = geom.handleLayout;
2600
2708
  const handles = geom.handles;
2709
+ const baseData = {
2710
+ typeId: n.typeId,
2711
+ params: n.params,
2712
+ inputHandles,
2713
+ outputHandles,
2714
+ inputConnected: Object.fromEntries(inputHandles.map((h) => [
2715
+ h.id,
2716
+ !!connectedInputs[n.nodeId]?.has(h.id),
2717
+ ])),
2718
+ handleLayout,
2719
+ showValues: opts.showValues,
2720
+ renderWidth,
2721
+ renderHeight,
2722
+ initialWidth: initialGeom.width,
2723
+ initialHeight: initialGeom.height,
2724
+ inputValues: opts.inputs?.[n.nodeId],
2725
+ inputDefaults: opts.inputDefaults?.[n.nodeId],
2726
+ outputValues: opts.outputs?.[n.nodeId],
2727
+ status: opts.nodeStatus?.[n.nodeId],
2728
+ validation: {
2729
+ inputs: opts.nodeValidation?.inputs?.[n.nodeId] ?? [],
2730
+ outputs: opts.nodeValidation?.outputs?.[n.nodeId] ?? [],
2731
+ issues: opts.nodeValidation?.issues?.[n.nodeId] ?? [],
2732
+ },
2733
+ toString: opts.toString,
2734
+ toElement: opts.toElement,
2735
+ };
2736
+ const customNodeData = opts.customData?.nodes;
2737
+ const mergedData = customNodeData?.[n.nodeId]
2738
+ ? { ...baseData, custom: customNodeData[n.nodeId] }
2739
+ : baseData;
2601
2740
  return {
2602
2741
  id: n.nodeId,
2603
- data: {
2604
- typeId: n.typeId,
2605
- params: n.params,
2606
- inputHandles,
2607
- outputHandles,
2608
- inputConnected: Object.fromEntries(inputHandles.map((h) => [
2609
- h.id,
2610
- !!connectedInputs[n.nodeId]?.has(h.id),
2611
- ])),
2612
- handleLayout,
2613
- showValues: opts.showValues,
2614
- renderWidth,
2615
- renderHeight,
2616
- initialWidth: initialGeom.width,
2617
- initialHeight: initialGeom.height,
2618
- inputValues: opts.inputs?.[n.nodeId],
2619
- inputDefaults: opts.inputDefaults?.[n.nodeId],
2620
- outputValues: opts.outputs?.[n.nodeId],
2621
- status: opts.nodeStatus?.[n.nodeId],
2622
- validation: {
2623
- inputs: opts.nodeValidation?.inputs?.[n.nodeId] ?? [],
2624
- outputs: opts.nodeValidation?.outputs?.[n.nodeId] ?? [],
2625
- issues: opts.nodeValidation?.issues?.[n.nodeId] ?? [],
2626
- },
2627
- toString: opts.toString,
2628
- toElement: opts.toElement,
2629
- },
2742
+ data: mergedData,
2630
2743
  position: positions[n.nodeId] ?? { x: 0, y: 0 },
2631
2744
  type: opts.resolveNodeType?.(n.typeId) ?? "spark-default",
2632
2745
  selected: opts.selectedNodeIds
@@ -2669,6 +2782,25 @@ function toReactFlow(def, positions, sizes, registry, opts) {
2669
2782
  const targetHandleTypeId = targetHandles?.inputs[e.target.handle]
2670
2783
  ? getInputTypeId(targetHandles.inputs, e.target.handle) ?? "unknown"
2671
2784
  : "unknown";
2785
+ const baseEdgeData = {
2786
+ sourceNodeId: e.source.nodeId,
2787
+ sourceNodeTypeId: sourceNode?.typeId || "unknown",
2788
+ sourceHandle: e.source.handle,
2789
+ sourceHandleTypeId,
2790
+ targetNodeId: e.target.nodeId,
2791
+ targetNodeTypeId: targetNode?.typeId || "unknown",
2792
+ targetHandle: e.target.handle,
2793
+ targetHandleTypeId,
2794
+ edgeTypeId,
2795
+ isRunning,
2796
+ hasError,
2797
+ isInvalid: isInvalidEdge,
2798
+ isMissing,
2799
+ };
2800
+ const customEdgeData = opts.customData?.edges;
2801
+ const mergedEdgeData = customEdgeData?.[e.id]
2802
+ ? { ...baseEdgeData, custom: customEdgeData[e.id] }
2803
+ : baseEdgeData;
2672
2804
  return {
2673
2805
  id: e.id,
2674
2806
  source: e.source.nodeId,
@@ -2682,21 +2814,7 @@ function toReactFlow(def, positions, sizes, registry, opts) {
2682
2814
  style,
2683
2815
  label: edgeTypeId,
2684
2816
  type: "default",
2685
- data: {
2686
- sourceNodeId: e.source.nodeId,
2687
- sourceNodeTypeId: sourceNode?.typeId || "unknown",
2688
- sourceHandle: e.source.handle,
2689
- sourceHandleTypeId,
2690
- targetNodeId: e.target.nodeId,
2691
- targetNodeTypeId: targetNode?.typeId || "unknown",
2692
- targetHandle: e.target.handle,
2693
- targetHandleTypeId,
2694
- edgeTypeId,
2695
- isRunning,
2696
- hasError,
2697
- isInvalid: isInvalidEdge,
2698
- isMissing,
2699
- },
2817
+ data: mergedEdgeData,
2700
2818
  };
2701
2819
  });
2702
2820
  return { nodes, edges };
@@ -2852,6 +2970,9 @@ async function upload(parsed, wb, runner) {
2852
2970
  if (extData.runtime && typeof extData.runtime === "object") {
2853
2971
  wb.setRuntimeState(extData.runtime);
2854
2972
  }
2973
+ if (extData.custom && typeof extData.custom === "object") {
2974
+ wb.setCustomData(extData.custom);
2975
+ }
2855
2976
  if (runner.isRunning()) {
2856
2977
  await runner.applySnapshotFull({
2857
2978
  def: wb.def,
@@ -2868,6 +2989,9 @@ async function upload(parsed, wb, runner) {
2868
2989
  runner.setInputs(nodeId, map, { dry: true });
2869
2990
  }
2870
2991
  }
2992
+ if (extData) {
2993
+ await runner.setExtData(extData);
2994
+ }
2871
2995
  }
2872
2996
  }
2873
2997
 
@@ -3664,15 +3788,7 @@ function WorkbenchProvider({ wb, runner, overrides, uiVersion, children, }) {
3664
3788
  const graphUiTick = useWorkbenchGraphUiTick(wb);
3665
3789
  const versionTick = useWorkbenchVersionTick(runner);
3666
3790
  const valuesTick = versionTick + graphTick + graphUiTick;
3667
- // Keep local runMode state loosely in sync with runner status.
3668
- // - Seed from runner.getRunMode() on mount if available.
3669
- // - On status events, update only when a non-undefined runMode is reported,
3670
- // so the UI preserves the last selected mode after stop().
3671
3791
  useEffect(() => {
3672
- const initialMode = runner.getRunMode();
3673
- if (initialMode) {
3674
- setRunModeState(initialMode);
3675
- }
3676
3792
  const offRunnerStatus = runner.on("status", (status) => {
3677
3793
  if (status.runMode) {
3678
3794
  setRunModeState(status.runMode);
@@ -3832,12 +3948,13 @@ function WorkbenchProvider({ wb, runner, overrides, uiVersion, children, }) {
3832
3948
  workbench.setRuntimeState(metadata);
3833
3949
  const fullUiState = workbench.getUIState();
3834
3950
  const uiWithoutViewport = excludeViewportFromUIState(fullUiState);
3835
- await graphRunner.setExtData?.({
3836
- ...(Object.keys(uiWithoutViewport || {}).length > 0
3837
- ? { ui: uiWithoutViewport }
3838
- : {}),
3839
- runtime: metadata,
3840
- });
3951
+ // Use updateExtData for efficient batched partial updates
3952
+ const updates = [];
3953
+ if (Object.keys(uiWithoutViewport || {}).length > 0) {
3954
+ updates.push({ path: "ui", value: uiWithoutViewport });
3955
+ }
3956
+ updates.push({ path: "runtime", value: metadata });
3957
+ await graphRunner.updateExtData(updates);
3841
3958
  }
3842
3959
  catch (err) {
3843
3960
  console.warn("[WorkbenchContext] Failed to save runtime metadata:", err);
@@ -4270,6 +4387,18 @@ function WorkbenchProvider({ wb, runner, overrides, uiVersion, children, }) {
4270
4387
  else if (changeType === "selection") {
4271
4388
  reason = "selection";
4272
4389
  }
4390
+ else if (changeType === "customNodeData") {
4391
+ reason = "custom-node-data";
4392
+ }
4393
+ else if (changeType === "customEdgeData") {
4394
+ reason = "custom-edge-data";
4395
+ }
4396
+ else if (changeType === "customMetaData") {
4397
+ reason = "custom-meta";
4398
+ }
4399
+ else if (changeType === "customData") {
4400
+ reason = "custom";
4401
+ }
4273
4402
  }
4274
4403
  await saveUiRuntimeMetadata(wb, runner);
4275
4404
  const history = await runner
@@ -4375,27 +4504,11 @@ function WorkbenchProvider({ wb, runner, overrides, uiVersion, children, }) {
4375
4504
  };
4376
4505
  }, [runner, wb]);
4377
4506
  const isRunning = useCallback(() => runner.isRunning(), [runner]);
4378
- const getRunMode = useCallback(() => runner.getRunMode(), [runner]);
4379
- const stop = useCallback(() => runner.stop(), [runner]);
4380
- // Run mode actions
4381
4507
  const setRunMode = useCallback((mode) => {
4382
4508
  if (mode === runMode)
4383
4509
  return;
4384
- const wasRunning = runner.isRunning();
4385
- if (wasRunning) {
4386
- // Use setRunMode to change run mode without rebuilding
4387
- try {
4388
- runner.setRunMode(mode);
4389
- setRunModeState(mode);
4390
- }
4391
- catch (err) {
4392
- console.error("Failed to set run mode:", err);
4393
- }
4394
- }
4395
- else {
4396
- // Just update state if not running (will be applied on next launch)
4397
- setRunModeState(mode);
4398
- }
4510
+ runner.setRunMode(mode);
4511
+ setRunModeState(mode);
4399
4512
  }, [runMode, runner]);
4400
4513
  const runNodeAction = useCallback(async (nodeId) => {
4401
4514
  await runner.computeNode(nodeId);
@@ -4497,8 +4610,6 @@ function WorkbenchProvider({ wb, runner, overrides, uiVersion, children, }) {
4497
4610
  removeRegistryError,
4498
4611
  removeInputValidationError,
4499
4612
  isRunning,
4500
- getRunMode,
4501
- stop,
4502
4613
  runMode,
4503
4614
  setRunMode,
4504
4615
  runNode: runNodeAction,
@@ -4539,8 +4650,6 @@ function WorkbenchProvider({ wb, runner, overrides, uiVersion, children, }) {
4539
4650
  events,
4540
4651
  clearEvents,
4541
4652
  isRunning,
4542
- getRunMode,
4543
- stop,
4544
4653
  runMode,
4545
4654
  setRunMode,
4546
4655
  runNodeAction,
@@ -5574,6 +5683,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
5574
5683
  status: n.data.status,
5575
5684
  validation: n.data.validation,
5576
5685
  inputConnected: n.data.inputConnected,
5686
+ custom: n.data.custom,
5577
5687
  },
5578
5688
  });
5579
5689
  return lod.isEqual(pick(a), pick(b));
@@ -5589,6 +5699,14 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
5589
5699
  style: e.style,
5590
5700
  label: e.label,
5591
5701
  type: e.type,
5702
+ data: e.data && {
5703
+ edgeTypeId: e.data.edgeTypeId,
5704
+ isRunning: e.data.isRunning,
5705
+ hasError: e.data.hasError,
5706
+ isInvalid: e.data.isInvalid,
5707
+ isMissing: e.data.isMissing,
5708
+ custom: e.data.custom,
5709
+ },
5592
5710
  });
5593
5711
  return lod.isEqual(pick(a), pick(b));
5594
5712
  };
@@ -5676,6 +5794,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
5676
5794
  selectedEdgeIds: new Set(sel.edges),
5677
5795
  getDefaultNodeSize,
5678
5796
  ui,
5797
+ customData: wb.getCustomData(),
5679
5798
  });
5680
5799
  // Retain references for unchanged items
5681
5800
  const stableNodes = retainStabilityById(prevNodesRef.current, out.nodes, isSameNode);
@@ -6223,14 +6342,14 @@ function WorkbenchStudioCanvas({ autoScroll, onAutoScrollChange, example, onExam
6223
6342
  const isConnecting = transportStatus.state === "connecting" ||
6224
6343
  transportStatus.state === "retrying";
6225
6344
  // Only allow Start/Stop when transport is connected or local
6345
+ // For local backend, always allow control (transport state is "local")
6346
+ // For remote backends, require connection
6226
6347
  const canControl = transportStatus.state === "connected" ||
6227
- transportStatus.state === "local";
6348
+ transportStatus.state === "local" ||
6349
+ backendKind === "local"; // Always allow control for local backend
6228
6350
  if (isConnecting) {
6229
6351
  return (jsxs("button", { className: "border rounded px-2 py-1.5 text-gray-500 border-gray-400 flex items-center gap-1 disabled:opacity-50", disabled: true, title: "Connecting to backend...", children: [jsx(ClockClockwiseIcon, { size: 16, className: "animate-spin" }), jsx("span", { className: "font-medium ml-1", children: "Connecting..." })] }));
6230
6352
  }
6231
- if (isGraphRunning) {
6232
- return (jsxs("button", { className: "border rounded px-2 py-1.5 text-red-700 border-red-600 flex items-center gap-1 disabled:opacity-50 disabled:text-gray-400 disabled:border-gray-300", onClick: () => runner.stop(), disabled: !canControl, title: canControl ? "Stop engine" : "Waiting for connection", children: [jsx(StopIcon, { size: 16, weight: "fill" }), jsx("span", { className: "font-medium ml-1", children: "Stop" })] }));
6233
- }
6234
6353
  return (jsxs("button", { className: "border rounded px-2 py-1.5 text-green-700 border-green-600 flex items-center gap-1 disabled:text-gray-400 disabled:border-gray-300 disabled:opacity-50", onClick: (evt) => {
6235
6354
  if (evt.shiftKey && !confirm("Invalidate and re-run graph?"))
6236
6355
  return;
@@ -6247,7 +6366,7 @@ function WorkbenchStudioCanvas({ autoScroll, onAutoScrollChange, example, onExam
6247
6366
  }, disabled: !canControl, title: !canControl
6248
6367
  ? "Waiting for connection"
6249
6368
  : `Start ${runMode === "manual" ? "manual" : "auto"} mode`, children: [jsx(PlayIcon, { size: 16, weight: "fill" }), jsx("span", { className: "font-medium ml-1", children: "Start" })] }));
6250
- }, [transportStatus, isGraphRunning, runner, runMode, wb]);
6369
+ }, [transportStatus, isGraphRunning, runner, runMode, wb, backendKind]);
6251
6370
  const defaultExamples = useMemo(() => [
6252
6371
  {
6253
6372
  id: "simple",
@@ -6617,9 +6736,7 @@ function WorkbenchStudioCanvas({ autoScroll, onAutoScrollChange, example, onExam
6617
6736
  if (mode !== runMode) {
6618
6737
  await setRunMode(mode);
6619
6738
  }
6620
- }, disabled: isGraphRunning, title: isGraphRunning
6621
- ? "Stop before switching run mode"
6622
- : "Select run mode", children: [jsx("option", { value: "manual", children: "Manual" }), jsx("option", { value: "auto", children: "Auto" })] }), renderStartStopButton(), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: runAutoLayout, children: jsx(TreeStructureIcon, { size: 24 }) }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: () => canvasRef.current?.fitView?.(), title: "Fit View", children: jsx(CornersOutIcon, { size: 24 }) }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: download$1, children: jsx(DownloadIcon, { size: 24 }) }), jsx("input", { ref: uploadInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: onUploadPicked }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: triggerUpload, children: jsx(UploadIcon, { size: 24 }) }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: async () => {
6739
+ }, title: "Select run mode", children: [jsx("option", { value: "manual", children: "Manual" }), jsx("option", { value: "auto", children: "Auto" })] }), renderStartStopButton(), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: runAutoLayout, children: jsx(TreeStructureIcon, { size: 24 }) }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: () => canvasRef.current?.fitView?.(), title: "Fit View", children: jsx(CornersOutIcon, { size: 24 }) }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: download$1, children: jsx(DownloadIcon, { size: 24 }) }), jsx("input", { ref: uploadInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: onUploadPicked }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: triggerUpload, children: jsx(UploadIcon, { size: 24 }) }), jsx("button", { className: "border border-gray-300 rounded p-1", onClick: async () => {
6623
6740
  await downloadCanvasThumbnail(canvasContainerRef.current);
6624
6741
  }, title: "Download Flow Thumbnail (SVG)", children: jsx(ImageIcon, { size: 24 }) }), jsxs("label", { className: "flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsx(BugBeetleIcon, { size: 24, weight: debug ? "fill" : undefined })] }), jsxs("label", { className: "flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsx(ListBulletsIcon, { size: 24, weight: showValues ? "fill" : undefined })] })] }), jsxs("div", { className: "flex flex-1 min-h-0", children: [jsx("div", { className: "flex-1 min-w-0", ref: canvasContainerRef, children: jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement, getDefaultNodeSize: overrides?.getDefaultNodeSize }) }), jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, contextPanel: overrides?.contextPanel })] })] }));
6625
6742
  }