The Gateway Adapter is the single architectural abstraction that enables Diminuendo’s frontend code to run identically on web and desktop. It defines a uniform interface for gateway communication using Effect types, allowing React components, hooks, and Zustand stores to operate without knowledge of whether messages travel over a direct WebSocket or through Tauri IPC to a Rust backend.
Diminuendo has two frontend clients with fundamentally different transport mechanisms:
Web client: Connects directly to the gateway via WebSocket from the browser. The TypeScript SDK (DiminuendoClient) manages the connection, sending JSON messages and receiving JSON events.
Desktop client: Cannot (or should not) hold a WebSocket from the renderer process. Instead, the Tauri v2 Rust backend holds the WebSocket connection via the Rust SDK. The React frontend communicates with the Rust backend through Tauri’s IPC mechanism (invoke for commands, listen for events).
Without an abstraction layer, every component, hook, and store would need conditional logic:
Copy
// This is what we want to avoidif (platform === "web") { client.runTurn(sessionId, text)} else { invoke("run_turn", { sessionId, text })}
The GatewayAdapter interface defines all gateway operations using Effect types. Commands return Effect.Effect<T, GatewayError>. The event stream is a Stream.Stream<GatewayEvent, GatewayError>. The rest of the application consumes this interface without caring which implementation is active.
The web adapter wraps the TypeScript SDK’s DiminuendoClient. Promise-based SDK methods are lifted into Effect using Effect.tryPromise, and events are delivered as an Effect Stream.Stream via Stream.async.
The key characteristic of the web adapter is that it is a thin wrapper. The TypeScript SDK already handles connection management, authentication, reconnection, and request/response correlation. The adapter merely translates between the SDK’s Promise-based API and the Effect-based interface that the shared stores expect.
The Tauri adapter bridges from the React frontend to the Rust backend using Tauri’s IPC primitives:
Commands are sent via invoke() from @tauri-apps/api/core. Each invoke call maps to a #[tauri::command] function in the Rust backend that calls the corresponding method on the Rust SDK’s DiminuendoClient.
Events are received via listen() from @tauri-apps/api/event. The Rust backend’s event handler emits gateway-event Tauri events, which the adapter consumes and forwards to the Effect stream.
The Tauri adapter communicates with Rust #[tauri::command] handlers that hold the DiminuendoClient in Tauri’s managed state. The Rust backend spawns a tokio task that reads from the Rust SDK’s event channel and emits each event to the frontend via app_handle.emit("gateway-event", &event).
The connection store initializes the correct adapter based on platform detection. The useGatewayConnection hook subscribes to the adapter’s event stream and routes events to the appropriate Zustand stores:
Copy
// Platform detection and adapter initializationconst adapter: GatewayAdapter = window.__TAURI__ ? new TauriGatewayAdapter() : new WebGatewayAdapter()// Store initializationconst connectionStore = create<ConnectionState>((set) => ({ adapter, status: "disconnected", identity: null, // ...}))
Components never reference a specific adapter implementation. They interact exclusively through hooks that delegate to the adapter via the store:
The Gateway Adapter pattern is an application of the Strategy pattern with dependency injection. The adapter is the strategy, the store is the context, and platform detection is the factory. Swapping transports requires no changes to any UI component or business logic.
Both adapters must implement the same GatewayAdapter interface, which provides compile-time guarantees that they expose the same set of methods with the same signatures. When a new protocol method is added:
Add the method to the GatewayAdapter interface
TypeScript will produce compilation errors in both adapter implementations
Implement the method in WebGatewayAdapter (wrap SDK call in Effect)
Implement the method in TauriGatewayAdapter (wrap invoke call in Effect)
Add the corresponding #[tauri::command] in the Rust backend
The interface acts as a contract that enforces parity between the two transport layers.