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:
- 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.)
- 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.
Problem
BuckarooServerView(added in #724) only accepts awsUrl: stringprop and constructs its ownWebSocketModelinternally: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 reasonbuckaroo-tauriexists is to relay the buckaroo WS protocol through the Rust supervisor over Tauri IPC so the webview never opens a socket itself.Today, integrating
BuckarooServerViewinto abuckaroo-tauri-using host means either:connect-srctows://127.0.0.1:*, accept that the renderer talks to the local sidecar directly. (This is what my xorq-desktop spike currently does.)BuckarooServerView— assemble the inner widget tree manually with aTauriIPCModel. Doable, but the host now owns logic (row cache wiring, mode dispatch, pre-resolution) thatBuckarooServerViewwas 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:WebSocketonly appears in their codebase as (a) a comment about a future web-app target inpackages/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-tauriis the same shape, just for buckaroo's server protocol instead of Jupyter's: the Rust supervisor opens an internal127.0.0.1:NWebSocket to the Python sidecar and relays messages to the webview via Tauri events.buckaroo-tauri-adapteralready exportsTauriIPCModel(implementsIModel) for exactly this use case — butBuckarooServerViewhas 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
modelprop, mutually exclusive withwsUrlWhen
modelis provided, skip theWebSocketModelconstruction and use it as-is. Caller is then responsible for ensuringinitial_statehas been received (since there's noconnectingphase).modewould 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_statelifecycle for the injected case is awkward.Option 2 — split into transport-agnostic inner + WS-specific wrapper (preferred)
This:
BuckarooServerViewAPI surface byte-for-byte. No breaking changes.import { BuckarooView } from "buckaroo-js-core"and passTauriIPCModel+ a pre-collectedinitial_state(the adapter'swaitForInitialState()already exists for this exact purpose).BuckarooServerView, rendering lives inBuckarooView.BuckarooViewcan 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:
With option 2 it would become:
No webview WS, CSP can stay tight (
connect-src ipc: http://ipc.localhost), andbuckaroo-tauri's "renderer never opens a socket" property holds.Out of scope
buckaroo-tauri-adapter'sTauriIPCModelneeds API tweaks to match the upstreamIModelprecisely — that can be a follow-up. Today it's vendored.BuckarooServerViewkeeps its current API and behavior.Happy to draft the PR if direction is acceptable.