@bian-womp/spark-workbench 0.2.25 → 0.2.26
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 +155 -94
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/misc/WorkbenchStudio.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 +156 -95
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/misc/WorkbenchStudio.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
|
}
|
|
@@ -2972,7 +3098,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
2972
3098
|
}, 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
3099
|
});
|
|
2974
3100
|
|
|
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, }) {
|
|
3101
|
+
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
3102
|
const { wb, runner, registry, def, selectedNodeId, runAutoLayout } = useWorkbenchContext();
|
|
2977
3103
|
const [transportStatus, setTransportStatus] = React.useState({
|
|
2978
3104
|
state: "local",
|
|
@@ -3021,11 +3147,14 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3021
3147
|
return overrides.getExamples(defaultExamples);
|
|
3022
3148
|
return defaultExamples;
|
|
3023
3149
|
}, [overrides, defaultExamples]);
|
|
3024
|
-
const [hydrated, setHydrated] = React.useState(false);
|
|
3025
3150
|
const lastAutoLaunched = React.useRef(undefined);
|
|
3026
3151
|
const autoLayoutRan = React.useRef(false);
|
|
3027
3152
|
const canvasRef = React.useRef(null);
|
|
3028
3153
|
const uploadInputRef = React.useRef(null);
|
|
3154
|
+
const [registryReady, setRegistryReady] = React.useState(() => {
|
|
3155
|
+
// For local backends, registry is always ready
|
|
3156
|
+
return backendKind === "local";
|
|
3157
|
+
});
|
|
3029
3158
|
// Expose init callback with setInitialGraph helper
|
|
3030
3159
|
const initCalled = React.useRef(false);
|
|
3031
3160
|
React.useEffect(() => {
|
|
@@ -3084,7 +3213,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3084
3213
|
const { registry: r, def } = await ex.load();
|
|
3085
3214
|
// Keep registry consistent with backend:
|
|
3086
3215
|
// - For local backend, allow example to provide its own registry
|
|
3087
|
-
// - For remote backend,
|
|
3216
|
+
// - For remote backend, registry is automatically managed by RemoteGraphRunner
|
|
3088
3217
|
if (backendKind === "local") {
|
|
3089
3218
|
if (r) {
|
|
3090
3219
|
setRegistry(r);
|
|
@@ -3197,79 +3326,8 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3197
3326
|
const triggerUpload = React.useCallback(() => {
|
|
3198
3327
|
uploadInputRef.current?.click();
|
|
3199
3328
|
}, []);
|
|
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]);
|
|
3329
|
+
// Registry is now automatically fetched by RemoteGraphRunner on first connection
|
|
3330
|
+
// No need for manual hydration
|
|
3273
3331
|
// Ensure initial example is loaded (and sync when example prop changes)
|
|
3274
3332
|
React.useEffect(() => {
|
|
3275
3333
|
if (!example)
|
|
@@ -3280,6 +3338,21 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3280
3338
|
const off = runner.on("transport", (s) => setTransportStatus(s));
|
|
3281
3339
|
return () => off();
|
|
3282
3340
|
}, [runner]);
|
|
3341
|
+
// Track registry readiness for remote backends
|
|
3342
|
+
React.useEffect(() => {
|
|
3343
|
+
// For local backends, registry is always ready
|
|
3344
|
+
if (backendKind === "local") {
|
|
3345
|
+
setRegistryReady(true);
|
|
3346
|
+
return;
|
|
3347
|
+
}
|
|
3348
|
+
// Reset readiness when switching to remote backend
|
|
3349
|
+
setRegistryReady(false);
|
|
3350
|
+
// For remote backends, wait for registry event
|
|
3351
|
+
const off = runner.on("registry", () => {
|
|
3352
|
+
setRegistryReady(true);
|
|
3353
|
+
});
|
|
3354
|
+
return () => off();
|
|
3355
|
+
}, [runner, backendKind]);
|
|
3283
3356
|
React.useEffect(() => {
|
|
3284
3357
|
if (!engine)
|
|
3285
3358
|
return;
|
|
@@ -3301,25 +3374,13 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3301
3374
|
// ignore
|
|
3302
3375
|
}
|
|
3303
3376
|
}, [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]);
|
|
3377
|
+
// Registry is automatically fetched by RemoteGraphRunner when it connects
|
|
3378
|
+
// Run auto layout after registry is hydrated (for remote backends)
|
|
3319
3379
|
React.useEffect(() => {
|
|
3320
3380
|
if (autoLayoutRan.current)
|
|
3321
3381
|
return;
|
|
3322
|
-
|
|
3382
|
+
// Wait for registry to be ready for remote backends
|
|
3383
|
+
if (backendKind !== "local" && !registryReady)
|
|
3323
3384
|
return;
|
|
3324
3385
|
const cur = wb.export();
|
|
3325
3386
|
const positions = wb.getPositions();
|
|
@@ -3328,7 +3389,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3328
3389
|
autoLayoutRan.current = true;
|
|
3329
3390
|
runAutoLayout();
|
|
3330
3391
|
}
|
|
3331
|
-
}, [wb, runAutoLayout, backendKind,
|
|
3392
|
+
}, [wb, runAutoLayout, backendKind, registryReady, registry]);
|
|
3332
3393
|
const baseSetInput = React.useCallback((handle, raw) => {
|
|
3333
3394
|
if (!selectedNodeId)
|
|
3334
3395
|
return;
|
|
@@ -3584,7 +3645,7 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
|
|
|
3584
3645
|
if (runner.isRunning())
|
|
3585
3646
|
runner.dispose();
|
|
3586
3647
|
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 }) }));
|
|
3648
|
+
}, 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
3649
|
}
|
|
3589
3650
|
|
|
3590
3651
|
exports.AbstractWorkbench = AbstractWorkbench;
|