@zap-js/server 0.0.2 → 0.0.5
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/index.js +18 -14
- package/package.json +4 -9
- package/src/bin/zap.rs +0 -154
- package/src/config.rs +0 -253
- package/src/connection_pool.rs +0 -404
- package/src/error.rs +0 -380
- package/src/handler.rs +0 -89
- package/src/ipc.js +0 -10
- package/src/ipc.rs +0 -499
- package/src/lib.rs +0 -433
- package/src/metrics.rs +0 -264
- package/src/proxy.rs +0 -436
- package/src/reliability.rs +0 -917
- package/src/request.rs +0 -60
- package/src/request_id.rs +0 -97
- package/src/response.rs +0 -182
- package/src/rpc.js +0 -14
- package/src/server.rs +0 -597
- package/src/static.rs +0 -572
- package/src/types.js +0 -21
- package/src/utils.rs +0 -18
- package/src/websocket.rs +0 -429
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
|
-
}
|