Skip to main content

connector_client/
lib.rs

1//! Shared WebSocket client for connecting to tauri-plugin-connector.
2//!
3//! Both the MCP server and CLI use this crate to communicate with the
4//! running Tauri app's connector plugin over WebSocket.
5
6use std::collections::HashMap;
7use std::sync::Arc;
8use std::time::Duration;
9
10use futures_util::{SinkExt, StreamExt};
11use serde_json::Value;
12use tokio::net::TcpStream;
13use tokio::sync::{mpsc, oneshot, Mutex};
14use tokio_tungstenite::tungstenite::Message;
15use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
16
17pub mod batch;
18pub mod discovery;
19
20const DEFAULT_TIMEOUT_MS: u64 = 35_000;
21
22type _WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
23
24struct PendingRequest {
25    tx: oneshot::Sender<Result<Value, String>>,
26}
27
28/// WebSocket client that communicates with tauri-plugin-connector.
29pub struct ConnectorClient {
30    write_tx: Option<mpsc::UnboundedSender<String>>,
31    pending: Arc<Mutex<HashMap<String, PendingRequest>>>,
32    _reader_handle: Option<tokio::task::JoinHandle<()>>,
33}
34
35impl ConnectorClient {
36    pub fn new() -> Self {
37        Self {
38            write_tx: None,
39            pending: Arc::new(Mutex::new(HashMap::new())),
40            _reader_handle: None,
41        }
42    }
43
44    /// Connect to the plugin's WebSocket server.
45    pub async fn connect(&mut self, host: &str, port: u16) -> Result<(), String> {
46        self.disconnect().await;
47
48        let url = format!("ws://{host}:{port}");
49        let (ws, _) = tokio_tungstenite::connect_async(&url)
50            .await
51            .map_err(|e| format!("WebSocket connection failed: {e}"))?;
52
53        let (ws_write, ws_read) = ws.split();
54
55        // Writer task: forwards messages from channel to WebSocket
56        let (write_tx, mut write_rx) = mpsc::unbounded_channel::<String>();
57        let writer_handle = tokio::spawn(async move {
58            let mut ws_write = ws_write;
59            while let Some(msg) = write_rx.recv().await {
60                if ws_write.send(Message::Text(msg.into())).await.is_err() {
61                    break;
62                }
63            }
64        });
65
66        // Reader task: receives messages from WebSocket and resolves pending requests
67        let pending = self.pending.clone();
68        let reader_handle = tokio::spawn(async move {
69            let mut ws_read = ws_read;
70            while let Some(Ok(msg)) = ws_read.next().await {
71                if let Message::Text(text) = msg {
72                    let text: &str = text.as_ref();
73                    if let Ok(response) = serde_json::from_str::<Value>(text) {
74                        let id = response
75                            .get("id")
76                            .and_then(|v| v.as_str())
77                            .unwrap_or("")
78                            .to_string();
79
80                        let mut pending = pending.lock().await;
81                        if let Some(req) = pending.remove(&id) {
82                            let result = if let Some(error) = response.get("error") {
83                                Err(error.as_str().unwrap_or("Unknown error").to_string())
84                            } else {
85                                Ok(response.get("result").cloned().unwrap_or(Value::Null))
86                            };
87                            let _ = req.tx.send(result);
88                        }
89                    }
90                }
91            }
92            // Connection closed — reject all pending
93            let mut pending = pending.lock().await;
94            for (_, req) in pending.drain() {
95                let _ = req.tx.send(Err("Connection closed".to_string()));
96            }
97            drop(writer_handle);
98        });
99
100        self.write_tx = Some(write_tx);
101        self._reader_handle = Some(reader_handle);
102
103        Ok(())
104    }
105
106    /// Disconnect from the WebSocket server.
107    pub async fn disconnect(&mut self) {
108        self.write_tx = None;
109        if let Some(handle) = self._reader_handle.take() {
110            handle.abort();
111        }
112        let mut pending = self.pending.lock().await;
113        for (_, req) in pending.drain() {
114            let _ = req.tx.send(Err("Disconnected".to_string()));
115        }
116    }
117
118    /// Check if connected.
119    pub fn is_connected(&self) -> bool {
120        self.write_tx.is_some()
121    }
122
123    /// Send a command and wait for a response.
124    pub async fn send(&self, command: Value) -> Result<Value, String> {
125        self.send_with_timeout(command, DEFAULT_TIMEOUT_MS).await
126    }
127
128    /// Send a command with a custom timeout.
129    pub async fn send_with_timeout(
130        &self,
131        command: Value,
132        timeout_ms: u64,
133    ) -> Result<Value, String> {
134        let write_tx = self
135            .write_tx
136            .as_ref()
137            .ok_or_else(|| "Not connected".to_string())?;
138
139        let id = uuid::Uuid::new_v4().to_string();
140        let (tx, rx) = oneshot::channel();
141
142        {
143            let mut pending = self.pending.lock().await;
144            pending.insert(id.clone(), PendingRequest { tx });
145        }
146
147        // Build the message with the id
148        let mut msg = match command {
149            Value::Object(map) => map,
150            _ => return Err("Command must be a JSON object".to_string()),
151        };
152        msg.insert("id".to_string(), Value::String(id.clone()));
153
154        let json = serde_json::to_string(&msg).map_err(|e| e.to_string())?;
155        write_tx
156            .send(json)
157            .map_err(|_| "Send failed: connection closed".to_string())?;
158
159        // Wait for response with timeout
160        match tokio::time::timeout(Duration::from_millis(timeout_ms), rx).await {
161            Ok(Ok(result)) => result,
162            Ok(Err(_)) => {
163                self.pending.lock().await.remove(&id);
164                Err("Response channel closed".to_string())
165            }
166            Err(_) => {
167                self.pending.lock().await.remove(&id);
168                Err("Request timeout".to_string())
169            }
170        }
171    }
172}
173
174impl Default for ConnectorClient {
175    fn default() -> Self {
176        Self::new()
177    }
178}