@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 +241 -106
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/misc/Inspector.d.ts.map +1 -1
- package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +7 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +9 -0
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
- package/lib/esm/index.js +242 -107
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/misc/Inspector.d.ts.map +1 -1
- package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -1
- package/lib/esm/src/misc/context/WorkbenchContext.d.ts +7 -1
- package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
- package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +9 -0
- package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
- package/package.json +4 -4
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) => [
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
3201
|
-
|
|
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
|
-
//
|
|
3305
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
3691
|
+
newRunner = new RemoteGraphRunner(registry, backend);
|
|
3574
3692
|
}
|
|
3575
|
-
|
|
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;
|