@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 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, NEVER overwrite the hydrated remote registry
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
- 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]);
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
- // 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]);
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
- if (backendKind !== "local" && !hydrated)
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, hydrated]);
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;