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