@zap-js/server 0.0.2 → 0.0.4

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/src/websocket.rs DELETED
@@ -1,429 +0,0 @@
1
- //! WebSocket Handler for ZapJS
2
- //!
3
- //! Provides bidirectional real-time communication between clients and TypeScript handlers.
4
- //!
5
- //! Architecture:
6
- //! ```text
7
- //! Client <--WS--> Rust Server <--IPC--> TypeScript Handler
8
- //! ```
9
- //!
10
- //! Route Conventions:
11
- //! - `WEBSOCKET` export in api/ folder routes
12
- //! - Default export in ws/ folder routes
13
- //!
14
- //! IPC Message Flow:
15
- //! - WsConnect: Client connected (Rust -> TS)
16
- //! - WsMessage: Message received from client (Rust -> TS)
17
- //! - WsSend: Message to send to client (TS -> Rust)
18
- //! - WsClose: Connection closed (bidirectional)
19
-
20
- use crate::error::{ZapError, ZapResult};
21
- use crate::ipc::{IpcClient, IpcEncoding, IpcMessage};
22
- use futures::{SinkExt, StreamExt};
23
- use std::collections::HashMap;
24
- use std::sync::Arc;
25
- use tokio::sync::mpsc;
26
- use tokio_tungstenite::{
27
- accept_async,
28
- tungstenite::{Error as WsError, Message as WsMessage},
29
- WebSocketStream,
30
- };
31
- use tracing::{debug, error, info, warn};
32
- use uuid::Uuid;
33
-
34
- /// WebSocket handler configuration
35
- #[derive(Clone)]
36
- pub struct WsConfig {
37
- /// IPC socket path for communication with TypeScript
38
- pub ipc_socket_path: String,
39
- /// Handler ID for this WebSocket route
40
- pub handler_id: String,
41
- /// Maximum message size (default: 64KB)
42
- pub max_message_size: usize,
43
- /// Ping interval in seconds (default: 30)
44
- pub ping_interval_secs: u64,
45
- }
46
-
47
- impl Default for WsConfig {
48
- fn default() -> Self {
49
- Self {
50
- ipc_socket_path: String::new(),
51
- handler_id: String::new(),
52
- max_message_size: 64 * 1024, // 64KB
53
- ping_interval_secs: 30,
54
- }
55
- }
56
- }
57
-
58
- impl WsConfig {
59
- /// Create a new WebSocket config
60
- pub fn new(ipc_socket_path: String, handler_id: String) -> Self {
61
- Self {
62
- ipc_socket_path,
63
- handler_id,
64
- ..Default::default()
65
- }
66
- }
67
- }
68
-
69
- /// Handle a WebSocket connection
70
- ///
71
- /// This function upgrades the HTTP connection to WebSocket and manages
72
- /// bidirectional message flow between the client and TypeScript handler.
73
- pub async fn handle_websocket_connection<S>(
74
- stream: S,
75
- config: WsConfig,
76
- path: String,
77
- headers: HashMap<String, String>,
78
- ) -> ZapResult<()>
79
- where
80
- S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
81
- {
82
- // Accept the WebSocket connection
83
- let ws_stream = accept_async(stream).await.map_err(|e| {
84
- error!("WebSocket handshake failed: {}", e);
85
- ZapError::websocket(format!("Handshake failed: {}", e))
86
- })?;
87
-
88
- // Generate unique connection ID
89
- let connection_id = Uuid::new_v4().to_string();
90
- info!(
91
- "WebSocket connection established: {} on {}",
92
- connection_id, path
93
- );
94
-
95
- // Connect to TypeScript IPC server
96
- let mut ipc_client = IpcClient::connect_with_encoding(&config.ipc_socket_path, IpcEncoding::MessagePack)
97
- .await
98
- .map_err(|e| {
99
- error!("Failed to connect to IPC for WebSocket: {}", e);
100
- e
101
- })?;
102
-
103
- // Notify TypeScript of the new connection
104
- let connect_msg = IpcMessage::WsConnect {
105
- connection_id: connection_id.clone(),
106
- handler_id: config.handler_id.clone(),
107
- path: path.clone(),
108
- headers: headers.clone(),
109
- };
110
- ipc_client.send_message(connect_msg).await?;
111
-
112
- // Split the WebSocket stream
113
- let (ws_sink, ws_stream) = ws_stream.split();
114
-
115
- // Create channels for communication
116
- let (outbound_tx, outbound_rx) = mpsc::channel::<WsMessage>(32);
117
-
118
- // Spawn tasks for handling the connection
119
- let connection_id_clone = connection_id.clone();
120
- let config_clone = config.clone();
121
-
122
- // Task 1: Handle incoming WebSocket messages from client
123
- let inbound_handle = tokio::spawn(async move {
124
- handle_inbound_messages(ws_stream, ipc_client, connection_id_clone, config_clone).await
125
- });
126
-
127
- // Task 2: Handle outbound messages to client
128
- let outbound_handle = tokio::spawn(async move {
129
- handle_outbound_messages(ws_sink, outbound_rx).await
130
- });
131
-
132
- // Wait for either task to complete
133
- tokio::select! {
134
- result = inbound_handle => {
135
- if let Err(e) = result {
136
- error!("Inbound handler error: {}", e);
137
- }
138
- }
139
- result = outbound_handle => {
140
- if let Err(e) = result {
141
- error!("Outbound handler error: {}", e);
142
- }
143
- }
144
- }
145
-
146
- info!("WebSocket connection closed: {}", connection_id);
147
- Ok(())
148
- }
149
-
150
- /// Handle incoming WebSocket messages from the client
151
- async fn handle_inbound_messages<S>(
152
- mut ws_stream: futures::stream::SplitStream<WebSocketStream<S>>,
153
- mut ipc_client: IpcClient,
154
- connection_id: String,
155
- config: WsConfig,
156
- ) -> ZapResult<()>
157
- where
158
- S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
159
- {
160
- while let Some(msg_result) = ws_stream.next().await {
161
- match msg_result {
162
- Ok(msg) => {
163
- match msg {
164
- WsMessage::Text(text) => {
165
- debug!(
166
- "Received text message from {}: {} bytes",
167
- connection_id,
168
- text.len()
169
- );
170
-
171
- // Forward to TypeScript
172
- let ipc_msg = IpcMessage::WsMessage {
173
- connection_id: connection_id.clone(),
174
- handler_id: config.handler_id.clone(),
175
- data: text,
176
- binary: false,
177
- };
178
- if let Err(e) = ipc_client.send_message(ipc_msg).await {
179
- error!("Failed to forward message to TypeScript: {}", e);
180
- break;
181
- }
182
- }
183
- WsMessage::Binary(data) => {
184
- debug!(
185
- "Received binary message from {}: {} bytes",
186
- connection_id,
187
- data.len()
188
- );
189
-
190
- // Forward to TypeScript (base64 encoded)
191
- use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
192
- let encoded = BASE64.encode(&data);
193
-
194
- let ipc_msg = IpcMessage::WsMessage {
195
- connection_id: connection_id.clone(),
196
- handler_id: config.handler_id.clone(),
197
- data: encoded,
198
- binary: true,
199
- };
200
- if let Err(e) = ipc_client.send_message(ipc_msg).await {
201
- error!("Failed to forward binary message to TypeScript: {}", e);
202
- break;
203
- }
204
- }
205
- WsMessage::Ping(data) => {
206
- debug!("Received ping from {}", connection_id);
207
- // Pong is handled automatically by tungstenite
208
- }
209
- WsMessage::Pong(_) => {
210
- debug!("Received pong from {}", connection_id);
211
- }
212
- WsMessage::Close(frame) => {
213
- let (code, reason) = frame
214
- .map(|f| (Some(f.code.into()), Some(f.reason.to_string())))
215
- .unwrap_or((None, None));
216
-
217
- info!(
218
- "WebSocket {} closed by client: code={:?}, reason={:?}",
219
- connection_id, code, reason
220
- );
221
-
222
- // Notify TypeScript
223
- let close_msg = IpcMessage::WsClose {
224
- connection_id: connection_id.clone(),
225
- handler_id: config.handler_id.clone(),
226
- code,
227
- reason,
228
- };
229
- let _ = ipc_client.send_message(close_msg).await;
230
- break;
231
- }
232
- WsMessage::Frame(_) => {
233
- // Raw frames are not typically handled at this level
234
- }
235
- }
236
- }
237
- Err(e) => {
238
- match e {
239
- WsError::ConnectionClosed | WsError::AlreadyClosed => {
240
- info!("WebSocket {} connection closed", connection_id);
241
- }
242
- _ => {
243
- error!("WebSocket error for {}: {}", connection_id, e);
244
- }
245
- }
246
-
247
- // Notify TypeScript of closure
248
- let close_msg = IpcMessage::WsClose {
249
- connection_id: connection_id.clone(),
250
- handler_id: config.handler_id.clone(),
251
- code: None,
252
- reason: Some(format!("Error: {}", e)),
253
- };
254
- let _ = ipc_client.send_message(close_msg).await;
255
- break;
256
- }
257
- }
258
- }
259
-
260
- Ok(())
261
- }
262
-
263
- /// Handle outbound WebSocket messages to the client
264
- async fn handle_outbound_messages<S>(
265
- mut ws_sink: futures::stream::SplitSink<WebSocketStream<S>, WsMessage>,
266
- mut outbound_rx: mpsc::Receiver<WsMessage>,
267
- ) -> ZapResult<()>
268
- where
269
- S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
270
- {
271
- while let Some(msg) = outbound_rx.recv().await {
272
- if let Err(e) = ws_sink.send(msg).await {
273
- error!("Failed to send WebSocket message: {}", e);
274
- break;
275
- }
276
- }
277
-
278
- Ok(())
279
- }
280
-
281
- /// WebSocket handler that manages IPC communication for outbound messages
282
- pub struct WsHandler {
283
- config: WsConfig,
284
- /// Channel sender for outbound messages (connection_id -> sender)
285
- senders: Arc<tokio::sync::RwLock<HashMap<String, mpsc::Sender<WsMessage>>>>,
286
- }
287
-
288
- impl WsHandler {
289
- /// Create a new WebSocket handler
290
- pub fn new(config: WsConfig) -> Self {
291
- Self {
292
- config,
293
- senders: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
294
- }
295
- }
296
-
297
- /// Register a connection's outbound sender
298
- pub async fn register_connection(
299
- &self,
300
- connection_id: String,
301
- sender: mpsc::Sender<WsMessage>,
302
- ) {
303
- let mut senders = self.senders.write().await;
304
- senders.insert(connection_id, sender);
305
- }
306
-
307
- /// Unregister a connection
308
- pub async fn unregister_connection(&self, connection_id: &str) {
309
- let mut senders = self.senders.write().await;
310
- senders.remove(connection_id);
311
- }
312
-
313
- /// Send a message to a specific connection
314
- pub async fn send_to_connection(
315
- &self,
316
- connection_id: &str,
317
- message: WsMessage,
318
- ) -> ZapResult<()> {
319
- let senders = self.senders.read().await;
320
- if let Some(sender) = senders.get(connection_id) {
321
- sender.send(message).await.map_err(|e| {
322
- ZapError::websocket(format!("Failed to send to {}: {}", connection_id, e))
323
- })?;
324
- Ok(())
325
- } else {
326
- Err(ZapError::websocket(format!(
327
- "Connection {} not found",
328
- connection_id
329
- )))
330
- }
331
- }
332
-
333
- /// Handle an IPC message for WebSocket (from TypeScript)
334
- pub async fn handle_ipc_message(&self, msg: IpcMessage) -> ZapResult<()> {
335
- match msg {
336
- IpcMessage::WsSend {
337
- connection_id,
338
- data,
339
- binary,
340
- } => {
341
- let ws_msg = if binary {
342
- // Decode base64 for binary
343
- use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
344
- let decoded = BASE64.decode(&data).map_err(|e| {
345
- ZapError::websocket(format!("Invalid base64 data: {}", e))
346
- })?;
347
- WsMessage::Binary(decoded)
348
- } else {
349
- WsMessage::Text(data)
350
- };
351
-
352
- self.send_to_connection(&connection_id, ws_msg).await?;
353
- }
354
- IpcMessage::WsClose {
355
- connection_id,
356
- handler_id: _,
357
- code,
358
- reason,
359
- } => {
360
- // Close the connection
361
- let close_frame = code.map(|c| {
362
- tokio_tungstenite::tungstenite::protocol::CloseFrame {
363
- code: c.into(),
364
- reason: reason.unwrap_or_default().into(),
365
- }
366
- });
367
-
368
- let ws_msg = WsMessage::Close(close_frame);
369
- let _ = self.send_to_connection(&connection_id, ws_msg).await;
370
- self.unregister_connection(&connection_id).await;
371
- }
372
- _ => {
373
- warn!("Unexpected IPC message for WebSocket handler: {:?}", msg);
374
- }
375
- }
376
-
377
- Ok(())
378
- }
379
- }
380
-
381
- /// Check if an HTTP request is a WebSocket upgrade request
382
- pub fn is_websocket_upgrade(headers: &HashMap<String, String>) -> bool {
383
- headers
384
- .get("upgrade")
385
- .map(|v| v.to_lowercase() == "websocket")
386
- .unwrap_or(false)
387
- && headers
388
- .get("connection")
389
- .map(|v| v.to_lowercase().contains("upgrade"))
390
- .unwrap_or(false)
391
- }
392
-
393
- #[cfg(test)]
394
- mod tests {
395
- use super::*;
396
-
397
- #[test]
398
- fn test_is_websocket_upgrade() {
399
- let mut headers = HashMap::new();
400
- headers.insert("upgrade".to_string(), "websocket".to_string());
401
- headers.insert("connection".to_string(), "Upgrade".to_string());
402
- assert!(is_websocket_upgrade(&headers));
403
-
404
- // Case insensitive
405
- let mut headers2 = HashMap::new();
406
- headers2.insert("upgrade".to_string(), "WebSocket".to_string());
407
- headers2.insert("connection".to_string(), "keep-alive, Upgrade".to_string());
408
- assert!(is_websocket_upgrade(&headers2));
409
-
410
- // Not a WebSocket upgrade
411
- let mut headers3 = HashMap::new();
412
- headers3.insert("connection".to_string(), "keep-alive".to_string());
413
- assert!(!is_websocket_upgrade(&headers3));
414
- }
415
-
416
- #[test]
417
- fn test_ws_config_default() {
418
- let config = WsConfig::default();
419
- assert_eq!(config.max_message_size, 64 * 1024);
420
- assert_eq!(config.ping_interval_secs, 30);
421
- }
422
-
423
- #[test]
424
- fn test_ws_config_new() {
425
- let config = WsConfig::new("/tmp/test.sock".to_string(), "ws_handler_0".to_string());
426
- assert_eq!(config.ipc_socket_path, "/tmp/test.sock");
427
- assert_eq!(config.handler_id, "ws_handler_0");
428
- }
429
- }