@bian-womp/spark-workbench 0.2.25 → 0.2.27

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
@@ -572,6 +572,10 @@ class RemoteGraphRunner extends AbstractGraphRunner {
572
572
  super(registry, backend);
573
573
  this.valueCache = new Map();
574
574
  this.listenersBound = false;
575
+ this.registryFetched = false;
576
+ this.registryFetching = false;
577
+ this.MAX_REGISTRY_FETCH_ATTEMPTS = 3;
578
+ this.INITIAL_RETRY_DELAY_MS = 1000; // 1 second
575
579
  // Auto-handle registry-changed invalidations from remote
576
580
  // We listen on invalidate and if reason matches, we rehydrate registry and emit a registry event
577
581
  this.ensureRemoteRunner().then(async (runner) => {
@@ -806,11 +810,125 @@ class RemoteGraphRunner extends AbstractGraphRunner {
806
810
  super.dispose();
807
811
  this.runner = undefined;
808
812
  this.transport = undefined;
813
+ this.registryFetched = false; // Reset so registry is fetched again on reconnect
814
+ this.registryFetching = false; // Reset fetching state
809
815
  this.emit("transport", {
810
816
  state: "disconnected",
811
817
  kind: this.backend.kind,
812
818
  });
813
819
  }
820
+ /**
821
+ * Fetch full registry description from remote and register it locally.
822
+ * Called automatically on first connection with retry mechanism.
823
+ */
824
+ async fetchRegistry(runner, attempt = 1) {
825
+ if (this.registryFetching) {
826
+ // Already fetching, don't start another fetch
827
+ return;
828
+ }
829
+ this.registryFetching = true;
830
+ try {
831
+ const desc = await runner.describeRegistry();
832
+ // Register types
833
+ for (const t of desc.types) {
834
+ if (t.options) {
835
+ this.registry.registerEnum({
836
+ id: t.id,
837
+ options: t.options,
838
+ bakeTarget: t.bakeTarget,
839
+ });
840
+ }
841
+ else {
842
+ if (!this.registry.types.has(t.id)) {
843
+ this.registry.registerType({
844
+ id: t.id,
845
+ displayName: t.displayName,
846
+ validate: (_v) => true,
847
+ bakeTarget: t.bakeTarget,
848
+ });
849
+ }
850
+ }
851
+ }
852
+ // Register categories
853
+ for (const c of desc.categories || []) {
854
+ if (!this.registry.categories.has(c.id)) {
855
+ // Create placeholder category descriptor
856
+ const category = {
857
+ id: c.id,
858
+ displayName: c.displayName,
859
+ createRuntime: () => ({
860
+ async onInputsChanged() { },
861
+ }),
862
+ policy: { asyncConcurrency: "switch" },
863
+ };
864
+ this.registry.categories.register(category);
865
+ }
866
+ }
867
+ // Register coercions
868
+ for (const c of desc.coercions) {
869
+ if (c.async) {
870
+ this.registry.registerAsyncCoercion(c.from, c.to, async (v) => v, {
871
+ nonTransitive: c.nonTransitive,
872
+ });
873
+ }
874
+ else {
875
+ this.registry.registerCoercion(c.from, c.to, (v) => v, {
876
+ nonTransitive: c.nonTransitive,
877
+ });
878
+ }
879
+ }
880
+ // Register nodes
881
+ for (const n of desc.nodes) {
882
+ if (!this.registry.nodes.has(n.id)) {
883
+ this.registry.registerNode({
884
+ id: n.id,
885
+ categoryId: n.categoryId,
886
+ displayName: n.displayName,
887
+ inputs: n.inputs || {},
888
+ outputs: n.outputs || {},
889
+ impl: () => { },
890
+ });
891
+ }
892
+ }
893
+ this.registryFetched = true;
894
+ this.registryFetching = false;
895
+ this.emit("registry", this.registry);
896
+ }
897
+ catch (err) {
898
+ this.registryFetching = false;
899
+ const error = err instanceof Error ? err : new Error(String(err));
900
+ // Retry with exponential backoff if attempts remaining
901
+ if (attempt < this.MAX_REGISTRY_FETCH_ATTEMPTS) {
902
+ const delayMs = this.INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
903
+ console.warn(`Failed to fetch registry (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying in ${delayMs}ms...`, error);
904
+ // Emit error event for UI feedback
905
+ this.emit("error", {
906
+ kind: "registry",
907
+ message: `Registry fetch failed (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying...`,
908
+ err: error,
909
+ attempt,
910
+ maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
911
+ });
912
+ // Retry after delay
913
+ setTimeout(() => {
914
+ this.fetchRegistry(runner, attempt + 1).catch(() => {
915
+ // Final failure handled below
916
+ });
917
+ }, delayMs);
918
+ }
919
+ else {
920
+ // Max attempts reached, emit final error
921
+ console.error(`Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts:`, error);
922
+ this.emit("error", {
923
+ kind: "registry",
924
+ message: `Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts. Please check your connection and try refreshing.`,
925
+ err: error,
926
+ attempt: this.MAX_REGISTRY_FETCH_ATTEMPTS,
927
+ maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
928
+ });
929
+ }
930
+ }
931
+ }
814
932
  // Ensure remote transport/runner
815
933
  async ensureRemoteRunner() {
816
934
  if (this.runner)
@@ -856,6 +974,14 @@ class RemoteGraphRunner extends AbstractGraphRunner {
856
974
  this.valueCache.clear();
857
975
  this.listenersBound = false;
858
976
  this.emit("transport", { state: "connected", kind });
977
+ // Auto-fetch registry on first connection (only once)
978
+ if (!this.registryFetched && !this.registryFetching) {
979
+ // Log loading state (UI can listen to transport status for loading indication)
980
+ console.info("Loading registry from remote...");
981
+ this.fetchRegistry(runner).catch(() => {
982
+ // Error handling is done inside fetchRegistry
983
+ });
984
+ }
859
985
  return runner;
860
986
  }
861
987
  }
@@ -1521,6 +1647,16 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, child
1521
1647
  const [edgeStatus, setEdgeStatus] = React.useState({});
1522
1648
  const [events, setEvents] = React.useState([]);
1523
1649
  const clearEvents = React.useCallback(() => setEvents([]), []);
1650
+ const [systemErrors, setSystemErrors] = React.useState([]);
1651
+ const [registryErrors, setRegistryErrors] = React.useState([]);
1652
+ const clearSystemErrors = React.useCallback(() => setSystemErrors([]), []);
1653
+ const clearRegistryErrors = React.useCallback(() => setRegistryErrors([]), []);
1654
+ const removeSystemError = React.useCallback((index) => {
1655
+ setSystemErrors((prev) => prev.filter((_, idx) => idx !== index));
1656
+ }, []);
1657
+ const removeRegistryError = React.useCallback((index) => {
1658
+ setRegistryErrors((prev) => prev.filter((_, idx) => idx !== index));
1659
+ }, []);
1524
1660
  // Fallback progress animation: drive progress to 100% over ~2 minutes
1525
1661
  const FALLBACK_TOTAL_MS = 2 * 60 * 1000;
1526
1662
  const [fallbackStarts, setFallbackStarts] = React.useState({});
@@ -1703,7 +1839,10 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, child
1703
1839
  if (remoteDef && Array.isArray(remoteDef.nodes)) {
1704
1840
  // Mutate current def in-place to avoid emitting graphChanged and causing update loop
1705
1841
  const cur = wb.export();
1706
- const byId = new Map((remoteDef.nodes || []).map((n) => [n.nodeId, n]));
1842
+ const byId = new Map((remoteDef.nodes || []).map((n) => [
1843
+ n.nodeId,
1844
+ n,
1845
+ ]));
1707
1846
  let changed = false;
1708
1847
  for (const n of cur.nodes) {
1709
1848
  const rn = byId.get(n.nodeId);
@@ -1735,6 +1874,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, child
1735
1874
  const off2 = runner.on("error", (e) => {
1736
1875
  const edgeError = e;
1737
1876
  const nodeError = e;
1877
+ const registryError = e;
1878
+ const systemError = e;
1738
1879
  if (edgeError.kind === "edge-convert") {
1739
1880
  const edgeId = edgeError.edgeId;
1740
1881
  setEdgeStatus((s) => ({
@@ -1742,7 +1883,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, child
1742
1883
  [edgeId]: { ...s[edgeId], lastError: edgeError.err },
1743
1884
  }));
1744
1885
  }
1745
- else if (nodeError.nodeId) {
1886
+ else if (nodeError.kind === "node-run" && nodeError.nodeId) {
1746
1887
  const nodeId = nodeError.nodeId;
1747
1888
  const runId = nodeError.runId;
1748
1889
  setNodeStatus((s) => ({
@@ -1760,6 +1901,27 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, child
1760
1901
  };
1761
1902
  }
1762
1903
  }
1904
+ else if (registryError.kind === "registry") {
1905
+ // Track registry errors for UI display
1906
+ setRegistryErrors((prev) => {
1907
+ // Avoid duplicates by checking message
1908
+ if (prev.some((err) => err.message === registryError.message)) {
1909
+ return prev;
1910
+ }
1911
+ return [...prev, registryError];
1912
+ });
1913
+ }
1914
+ else if (systemError.kind === "system") {
1915
+ // Track custom errors for UI display
1916
+ setSystemErrors((prev) => {
1917
+ // Avoid duplicates by checking message and code
1918
+ if (prev.some((err) => err.message === systemError.message &&
1919
+ err.code === systemError.code)) {
1920
+ return prev;
1921
+ }
1922
+ return [...prev, systemError];
1923
+ });
1924
+ }
1763
1925
  return add("runner", "error")(e);
1764
1926
  });
1765
1927
  const off3 = runner.on("invalidate", (e) => {
@@ -2007,6 +2169,12 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, child
2007
2169
  validationGlobal,
2008
2170
  events,
2009
2171
  clearEvents,
2172
+ systemErrors,
2173
+ registryErrors,
2174
+ clearSystemErrors,
2175
+ clearRegistryErrors,
2176
+ removeSystemError,
2177
+ removeRegistryError,
2010
2178
  isRunning,
2011
2179
  engineKind,
2012
2180
  start,
@@ -2028,6 +2196,12 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, child
2028
2196
  nodeStatus,
2029
2197
  edgeStatus,
2030
2198
  valuesTick,
2199
+ systemErrors,
2200
+ registryErrors,
2201
+ clearSystemErrors,
2202
+ clearRegistryErrors,
2203
+ removeSystemError,
2204
+ removeRegistryError,
2031
2205
  inputsMap,
2032
2206
  outputsMap,
2033
2207
  validationByNode,
@@ -2094,7 +2268,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2094
2268
  return String(value ?? "");
2095
2269
  }
2096
2270
  };
2097
- const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, outputsMap, outputTypesMap, nodeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, updateEdgeType, } = useWorkbenchContext();
2271
+ const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, outputsMap, outputTypesMap, nodeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, updateEdgeType, systemErrors, registryErrors, clearSystemErrors, clearRegistryErrors, removeSystemError, removeRegistryError, } = useWorkbenchContext();
2098
2272
  const nodeValidationIssues = validationByNode.issues;
2099
2273
  const edgeValidationIssues = validationByEdge.issues;
2100
2274
  const nodeValidationHandles = validationByNode;
@@ -2180,7 +2354,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2180
2354
  }
2181
2355
  catch { }
2182
2356
  };
2183
- return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, children: [contextPanel && jsxRuntime.jsx("div", { className: "mb-2", children: contextPanel }), jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsxRuntime.jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
2357
+ return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, children: [contextPanel && jsxRuntime.jsx("div", { className: "mb-2", children: contextPanel }), systemErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: "\u00D7" })] }, i))), systemErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Registry Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxRuntime.jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsxRuntime.jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: "\u00D7" })] }, i))), registryErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsxRuntime.jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
2184
2358
  e.stopPropagation();
2185
2359
  deleteEdgeById(m.data?.edgeId);
2186
2360
  }, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) : selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxRuntime.jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), jsxRuntime.jsx("div", { className: "mt-1", children: jsxRuntime.jsx("button", { className: "text-xs px-2 py-1 border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
@@ -2972,7 +3146,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
2972
3146
  }, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, deleteKeyCode: ["Backspace", "Delete"], proOptions: { hideAttribution: true }, noDragClassName: "wb-nodrag", noWheelClassName: "wb-nowheel", noPanClassName: "wb-nopan", fitView: true, children: [jsxRuntime.jsx(react.Background, { id: "workbench-canvas-background", variant: react.BackgroundVariant.Dots, gap: 12, size: 1 }), jsxRuntime.jsx(react.MiniMap, {}), jsxRuntime.jsx(react.Controls, {}), jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: onCloseMenu }), !!nodeAtMenu && (jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, onClose: onCloseNodeMenu }))] }) }) }));
2973
3147
  });
2974
3148
 
2975
- function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
3149
+ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, backendOptions, overrides, onInit, onChange, }) {
2976
3150
  const { wb, runner, registry, def, selectedNodeId, runAutoLayout } = useWorkbenchContext();
2977
3151
  const [transportStatus, setTransportStatus] = React.useState({
2978
3152
  state: "local",
@@ -3021,17 +3195,17 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3021
3195
  return overrides.getExamples(defaultExamples);
3022
3196
  return defaultExamples;
3023
3197
  }, [overrides, defaultExamples]);
3024
- const [hydrated, setHydrated] = React.useState(false);
3025
3198
  const lastAutoLaunched = React.useRef(undefined);
3026
3199
  const autoLayoutRan = React.useRef(false);
3027
3200
  const canvasRef = React.useRef(null);
3028
3201
  const uploadInputRef = React.useRef(null);
3202
+ const [registryReady, setRegistryReady] = React.useState(() => {
3203
+ // For local backends, registry is always ready
3204
+ return backendKind === "local";
3205
+ });
3029
3206
  // Expose init callback with setInitialGraph helper
3030
- const initCalled = React.useRef(false);
3207
+ // Note: This runs whenever runner changes (e.g., when Flow is enabled and backendOptions changes)
3031
3208
  React.useEffect(() => {
3032
- if (initCalled.current)
3033
- return;
3034
- initCalled.current = true;
3035
3209
  if (!onInit)
3036
3210
  return;
3037
3211
  const setInitialGraph = async (d, inputs) => {
@@ -3084,7 +3258,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3084
3258
  const { registry: r, def } = await ex.load();
3085
3259
  // Keep registry consistent with backend:
3086
3260
  // - For local backend, allow example to provide its own registry
3087
- // - For remote backend, NEVER overwrite the hydrated remote registry
3261
+ // - For remote backend, registry is automatically managed by RemoteGraphRunner
3088
3262
  if (backendKind === "local") {
3089
3263
  if (r) {
3090
3264
  setRegistry(r);
@@ -3197,79 +3371,8 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3197
3371
  const triggerUpload = React.useCallback(() => {
3198
3372
  uploadInputRef.current?.click();
3199
3373
  }, []);
3200
- const hydrateFromBackend = React.useCallback(async (kind, base) => {
3201
- try {
3202
- const transport = kind === "remote-http"
3203
- ? new sparkRemote.HttpPollingTransport(base)
3204
- : new sparkRemote.WebSocketTransport(base);
3205
- await transport.connect();
3206
- const rr = new sparkRemote.RemoteRunner(transport);
3207
- const desc = await rr.describeRegistry();
3208
- const r = new sparkGraph.Registry();
3209
- // Types
3210
- for (const t of desc.types) {
3211
- if (t.options) {
3212
- r.registerEnum({
3213
- id: t.id,
3214
- options: t.options,
3215
- bakeTarget: t.bakeTarget,
3216
- });
3217
- }
3218
- else {
3219
- r.registerType({
3220
- id: t.id,
3221
- displayName: t.displayName,
3222
- validate: (_v) => true,
3223
- bakeTarget: t.bakeTarget,
3224
- });
3225
- }
3226
- }
3227
- // Categories: create placeholders for display name
3228
- for (const c of desc.categories || []) {
3229
- // If you later expose real category descriptors, register them here
3230
- // For now, rely on ComputeCategory for behavior
3231
- const category = {
3232
- id: c.id,
3233
- displayName: c.displayName,
3234
- createRuntime: () => ({
3235
- async onInputsChanged() { },
3236
- }),
3237
- policy: { asyncConcurrency: "switch" },
3238
- };
3239
- r.categories.register(category);
3240
- }
3241
- // Coercions (client-side no-op to satisfy validation) if provided
3242
- for (const c of desc.coercions) {
3243
- if (c.async) {
3244
- r.registerAsyncCoercion(c.from, c.to, async (v) => v, {
3245
- nonTransitive: c.nonTransitive,
3246
- });
3247
- }
3248
- else {
3249
- r.registerCoercion(c.from, c.to, (v) => v, {
3250
- nonTransitive: c.nonTransitive,
3251
- });
3252
- }
3253
- }
3254
- // Nodes (use no-op impl for compute)
3255
- for (const n of desc.nodes) {
3256
- r.registerNode({
3257
- id: n.id,
3258
- categoryId: n.categoryId,
3259
- displayName: n.displayName,
3260
- inputs: n.inputs || {},
3261
- outputs: n.outputs || {},
3262
- impl: () => { },
3263
- });
3264
- }
3265
- setRegistry(r);
3266
- wb.setRegistry(r);
3267
- await transport.close();
3268
- }
3269
- catch (err) {
3270
- console.error("Failed to hydrate registry from backend:", err);
3271
- }
3272
- }, [setRegistry, wb]);
3374
+ // Registry is now automatically fetched by RemoteGraphRunner on first connection
3375
+ // No need for manual hydration
3273
3376
  // Ensure initial example is loaded (and sync when example prop changes)
3274
3377
  React.useEffect(() => {
3275
3378
  if (!example)
@@ -3280,6 +3383,21 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3280
3383
  const off = runner.on("transport", (s) => setTransportStatus(s));
3281
3384
  return () => off();
3282
3385
  }, [runner]);
3386
+ // Track registry readiness for remote backends
3387
+ React.useEffect(() => {
3388
+ // For local backends, registry is always ready
3389
+ if (backendKind === "local") {
3390
+ setRegistryReady(true);
3391
+ return;
3392
+ }
3393
+ // Reset readiness when switching to remote backend
3394
+ setRegistryReady(false);
3395
+ // For remote backends, wait for registry event
3396
+ const off = runner.on("registry", () => {
3397
+ setRegistryReady(true);
3398
+ });
3399
+ return () => off();
3400
+ }, [runner, backendKind]);
3283
3401
  React.useEffect(() => {
3284
3402
  if (!engine)
3285
3403
  return;
@@ -3301,25 +3419,13 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3301
3419
  // ignore
3302
3420
  }
3303
3421
  }, [engine, runner, wb, backendKind]);
3304
- // When switching to remote backend, auto-hydrate registry from backend
3305
- React.useEffect(() => {
3306
- let hydrate;
3307
- if (backendKind === "remote-http" && httpBaseUrl) {
3308
- hydrate = hydrateFromBackend("remote-http", httpBaseUrl);
3309
- }
3310
- else if (backendKind === "remote-ws" && wsUrl) {
3311
- hydrate = hydrateFromBackend("remote-ws", wsUrl);
3312
- }
3313
- if (hydrate) {
3314
- hydrate.then(() => {
3315
- setHydrated(true);
3316
- });
3317
- }
3318
- }, [backendKind, httpBaseUrl, wsUrl, hydrateFromBackend, setHydrated]);
3422
+ // Registry is automatically fetched by RemoteGraphRunner when it connects
3423
+ // Run auto layout after registry is hydrated (for remote backends)
3319
3424
  React.useEffect(() => {
3320
3425
  if (autoLayoutRan.current)
3321
3426
  return;
3322
- if (backendKind !== "local" && !hydrated)
3427
+ // Wait for registry to be ready for remote backends
3428
+ if (backendKind !== "local" && !registryReady)
3323
3429
  return;
3324
3430
  const cur = wb.export();
3325
3431
  const positions = wb.getPositions();
@@ -3328,7 +3434,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3328
3434
  autoLayoutRan.current = true;
3329
3435
  runAutoLayout();
3330
3436
  }
3331
- }, [wb, runAutoLayout, backendKind, hydrated]);
3437
+ }, [wb, runAutoLayout, backendKind, registryReady, registry]);
3332
3438
  const baseSetInput = React.useCallback((handle, raw) => {
3333
3439
  if (!selectedNodeId)
3334
3440
  return;
@@ -3545,7 +3651,19 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3545
3651
  function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, backendOptions, overrides, onInit, onChange, }) {
3546
3652
  const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());
3547
3653
  const [wb] = React.useState(() => new InMemoryWorkbench({ ui: new DefaultUIExtensionRegistry() }));
3654
+ // Store previous runner for cleanup
3655
+ const prevRunnerRef = React.useRef(null);
3548
3656
  const runner = React.useMemo(() => {
3657
+ // Dispose previous runner if it exists
3658
+ if (prevRunnerRef.current) {
3659
+ try {
3660
+ prevRunnerRef.current.dispose();
3661
+ }
3662
+ catch (err) {
3663
+ console.warn("Error disposing previous runner:", err);
3664
+ }
3665
+ }
3666
+ let newRunner;
3549
3667
  if (backendKind === "remote-http") {
3550
3668
  const backend = {
3551
3669
  kind: "remote-http",
@@ -3557,9 +3675,9 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
3557
3675
  onCustomEvent: backendOptions.onCustomEvent,
3558
3676
  }),
3559
3677
  };
3560
- return new RemoteGraphRunner(registry, backend);
3678
+ newRunner = new RemoteGraphRunner(registry, backend);
3561
3679
  }
3562
- if (backendKind === "remote-ws") {
3680
+ else if (backendKind === "remote-ws") {
3563
3681
  const backend = {
3564
3682
  kind: "remote-ws",
3565
3683
  url: wsUrl,
@@ -3570,10 +3688,27 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
3570
3688
  onCustomEvent: backendOptions.onCustomEvent,
3571
3689
  }),
3572
3690
  };
3573
- return new RemoteGraphRunner(registry, backend);
3691
+ newRunner = new RemoteGraphRunner(registry, backend);
3574
3692
  }
3575
- return new LocalGraphRunner(registry);
3693
+ else {
3694
+ newRunner = new LocalGraphRunner(registry);
3695
+ }
3696
+ prevRunnerRef.current = newRunner;
3697
+ return newRunner;
3576
3698
  }, [registry, backendKind, httpBaseUrl, wsUrl, backendOptions]);
3699
+ // Cleanup runner on unmount
3700
+ React.useEffect(() => {
3701
+ return () => {
3702
+ if (prevRunnerRef.current) {
3703
+ try {
3704
+ prevRunnerRef.current.dispose();
3705
+ }
3706
+ catch (err) {
3707
+ console.warn("Error disposing runner on unmount:", err);
3708
+ }
3709
+ }
3710
+ };
3711
+ }, []);
3577
3712
  // Allow external UI registration (e.g., node renderers) with access to wb
3578
3713
  React.useEffect(() => {
3579
3714
  const baseRegisterUI = (_wb) => { };
@@ -3584,7 +3719,7 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
3584
3719
  if (runner.isRunning())
3585
3720
  runner.dispose();
3586
3721
  onBackendKindChange(v);
3587
- }, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange, overrides: overrides, onInit: onInit, onChange: onChange }) }));
3722
+ }, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange, backendOptions: backendOptions, overrides: overrides, onInit: onInit, onChange: onChange }) }));
3588
3723
  }
3589
3724
 
3590
3725
  exports.AbstractWorkbench = AbstractWorkbench;