Skip to content

BuckarooServerView should accept an injectable model (IModel) so hosts can keep the WebSocket out of the webview #759

@paddymul

Description

@paddymul

Problem

BuckarooServerView (added in #724) only accepts a wsUrl: string prop and constructs its own WebSocketModel internally:

// packages/buckaroo-js-core/src/server/BuckarooServerView.tsx
export interface BuckarooServerViewProps {
    wsUrl: string;
    renderConnecting?: () => React.ReactNode;
    renderError?: (err: Error) => React.ReactNode;
    onMetadata?: (metadata: BuckarooServerMetadata, prompt?: string) => void;
    style?: React.CSSProperties;
    className?: string;
}

This means a host app that mounts <BuckarooServerView> necessarily opens a WebSocket from its renderer / webview to the buckaroo sidecar. For Tauri / Electron / Wails desktop hosts that explicitly avoid renderer-side network sockets (CSP-tightening, sandbox compatibility, no-listening-port property), this is a non-starter — the whole reason buckaroo-tauri exists is to relay the buckaroo WS protocol through the Rust supervisor over Tauri IPC so the webview never opens a socket itself.

Today, integrating BuckarooServerView into a buckaroo-tauri-using host means either:

  1. Sacrifice the no-WS property — widen the host's CSP connect-src to ws://127.0.0.1:*, accept that the renderer talks to the local sidecar directly. (This is what my xorq-desktop spike currently does.)
  2. Don't use BuckarooServerView — assemble the inner widget tree manually with a TauriIPCModel. Doable, but the host now owns logic (row cache wiring, mode dispatch, pre-resolution) that BuckarooServerView was specifically built to encapsulate.

Both are bad trades for what could be a single optional prop.

Precedent: nteract

Modern nteract (nteract/desktop, Tauri-based — Cargo workspace) categorically does not open WebSockets from the renderer. Their architecture:

webview ──Tauri IPC── notebook crate (Rust) ──UnixStream── runtimed daemon ──ZMQ── kernel

WebSocket only appears in their codebase as (a) a comment about a future web-app target in packages/runtimed/src/transport.ts, and (b) a dev-only Vite relay (apps/notebook/vite-plugin-browser-relay.ts). All production kernel comms go renderer → IPC → Rust → daemon → ZMQ → kernel. The biggest Tauri-Jupyter shop deliberately took the IPC-relay route over a renderer-side WS.

buckaroo-tauri is the same shape, just for buckaroo's server protocol instead of Jupyter's: the Rust supervisor opens an internal 127.0.0.1:N WebSocket to the Python sidecar and relays messages to the webview via Tauri events. buckaroo-tauri-adapter already exports TauriIPCModel (implements IModel) for exactly this use case — but BuckarooServerView has no way to use it today.

Proposed change

Make the transport injectable. I see two clean shapes; happy with either, lean toward #2.

Option 1 — optional model prop, mutually exclusive with wsUrl

export type BuckarooServerViewProps =
  | { wsUrl: string;  model?: never; /* other props */ }
  | { wsUrl?: never;  model: IModel; mode: BuckarooServerMode; /* other props */ };

When model is provided, skip the WebSocketModel construction and use it as-is. Caller is then responsible for ensuring initial_state has been received (since there's no connecting phase). mode would need to be passed explicitly since it's no longer derived from a server message the component itself sees.

Drawback: prop shape gets pleonastic, the initial_state lifecycle for the injected case is awkward.

Option 2 — split into transport-agnostic inner + WS-specific wrapper (preferred)

// New, transport-agnostic. Takes an already-connected model + the initial_state it produced.
export interface BuckarooViewProps {
    model: IModel;
    initialState: Record<string, unknown>;
    mode: BuckarooServerMode;
    /* renderConnecting unnecessary — caller handles pre-connection */
    renderError?: (err: Error) => React.ReactNode;
    onMetadata?: (metadata: BuckarooServerMetadata, prompt?: string) => void;
    style?: React.CSSProperties;
    className?: string;
}
export function BuckarooView(props: BuckarooViewProps): JSX.Element { /* the existing widget-dispatch + row-cache + pre-resolution logic */ }

// Existing, unchanged public surface. Thin wrapper that constructs WebSocketModel,
// waits for initial_state, then renders <BuckarooView>.
export function BuckarooServerView(props: BuckarooServerViewProps): JSX.Element { /* … */ }

This:

  • Preserves the existing BuckarooServerView API surface byte-for-byte. No breaking changes.
  • Makes the model-injection case a first-class export: import { BuckarooView } from "buckaroo-js-core" and pass TauriIPCModel + a pre-collected initial_state (the adapter's waitForInitialState() already exists for this exact purpose).
  • Has a clear separation of concerns: connection handling lives in BuckarooServerView, rendering lives in BuckarooView.
  • Easier to test — BuckarooView can be exercised with a mock IModel without any WS plumbing.

Use case (concrete)

In a xorq-desktop spike I'm running today, my mount looks like:

<BuckarooServerView
  wsUrl={buckarooWsUrl(`http://127.0.0.1:${sidecarPort}`, sessionId)}
  onMetadata={(m) => console.log(m.path)}
/>

With option 2 it would become:

const initial = await waitForInitialState(); // adapter helper, already exists
const model = new TauriIPCModel(initial);
<BuckarooView
  model={model}
  initialState={initial}
  mode={initial.mode as BuckarooServerMode}
  onMetadata={(m) => console.log(m.path)}
/>

No webview WS, CSP can stay tight (connect-src ipc: http://ipc.localhost), and buckaroo-tauri's "renderer never opens a socket" property holds.

Out of scope

  • Whether buckaroo-tauri-adapter's TauriIPCModel needs API tweaks to match the upstream IModel precisely — that can be a follow-up. Today it's vendored.
  • Renaming / deprecation. BuckarooServerView keeps its current API and behavior.

Happy to draft the PR if direction is acceptable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions