@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/src/ipc.rs DELETED
@@ -1,499 +0,0 @@
1
- //! Unix Domain Socket IPC Protocol
2
- //!
3
- //! High-performance inter-process communication between TypeScript wrapper and Rust binary.
4
- //! Protocol: Length-prefixed MessagePack messages (default) with JSON fallback.
5
- //!
6
- //! Frame format: [4-byte big-endian length][payload]
7
- //! - MessagePack: First byte is 0x80-0xBF (map fixmap) or 0xDE-0xDF (map16/32)
8
- //! - JSON: First byte is '{' (0x7B)
9
-
10
- use crate::error::{ZapError, ZapResult};
11
- use serde::{Deserialize, Serialize};
12
- use std::collections::HashMap;
13
- use tokio::io::{AsyncReadExt, AsyncWriteExt};
14
- use tokio::net::UnixStream;
15
-
16
- /// IPC encoding format
17
- #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
18
- pub enum IpcEncoding {
19
- /// MessagePack (default, ~40% faster)
20
- #[default]
21
- MessagePack,
22
- /// JSON (for debugging)
23
- Json,
24
- }
25
-
26
- /// Messages sent over the IPC channel
27
- #[derive(Debug, Clone, Serialize, Deserialize)]
28
- #[serde(tag = "type", rename_all = "snake_case")]
29
- #[allow(clippy::large_enum_variant)] // IpcRequest is used directly, boxing adds overhead
30
- pub enum IpcMessage {
31
- /// TypeScript asks Rust to invoke a handler
32
- InvokeHandler {
33
- handler_id: String,
34
- request: IpcRequest,
35
- },
36
-
37
- /// TypeScript responds with handler result
38
- HandlerResponse {
39
- handler_id: String,
40
- status: u16,
41
- headers: HashMap<String, String>,
42
- body: String,
43
- },
44
-
45
- /// Health check ping from TypeScript
46
- HealthCheck,
47
-
48
- /// Health check response from Rust
49
- HealthCheckResponse,
50
-
51
- /// Structured error response with full context
52
- Error {
53
- /// Machine-readable error code (e.g., "HANDLER_ERROR")
54
- code: String,
55
- /// Human-readable error message
56
- message: String,
57
- /// HTTP status code
58
- #[serde(default = "default_error_status")]
59
- status: u16,
60
- /// Unique error ID for log correlation
61
- #[serde(default)]
62
- digest: String,
63
- /// Additional error details
64
- #[serde(skip_serializing_if = "Option::is_none")]
65
- details: Option<serde_json::Value>,
66
- },
67
-
68
- // Phase 8: Streaming support
69
- /// Start a streaming response
70
- StreamStart {
71
- stream_id: String,
72
- status: u16,
73
- headers: HashMap<String, String>,
74
- },
75
-
76
- /// A chunk of streaming data
77
- StreamChunk {
78
- stream_id: String,
79
- /// Base64-encoded binary data
80
- data: String,
81
- },
82
-
83
- /// End of streaming response
84
- StreamEnd {
85
- stream_id: String,
86
- },
87
-
88
- // Phase 8: WebSocket support
89
- /// WebSocket connection opened
90
- WsConnect {
91
- connection_id: String,
92
- handler_id: String,
93
- path: String,
94
- headers: HashMap<String, String>,
95
- },
96
-
97
- /// WebSocket message from client
98
- WsMessage {
99
- connection_id: String,
100
- handler_id: String,
101
- /// Message data (text or base64-encoded binary)
102
- data: String,
103
- /// true if binary data
104
- binary: bool,
105
- },
106
-
107
- /// WebSocket connection closed
108
- WsClose {
109
- connection_id: String,
110
- handler_id: String,
111
- code: Option<u16>,
112
- reason: Option<String>,
113
- },
114
-
115
- /// WebSocket message to send to client (TypeScript -> Rust)
116
- WsSend {
117
- connection_id: String,
118
- data: String,
119
- binary: bool,
120
- },
121
- }
122
-
123
- fn default_error_status() -> u16 {
124
- 500
125
- }
126
-
127
- /// Serialize an IPC message to bytes
128
- pub fn serialize_message(msg: &IpcMessage, encoding: IpcEncoding) -> ZapResult<Vec<u8>> {
129
- match encoding {
130
- IpcEncoding::MessagePack => {
131
- // IMPORTANT: Use to_vec_named to preserve string field names
132
- // This is required for #[serde(tag = "type")] to work correctly
133
- // with @msgpack/msgpack on the TypeScript side
134
- rmp_serde::to_vec_named(msg).map_err(|e| ZapError::ipc(format!("MessagePack serialize error: {}", e)))
135
- }
136
- IpcEncoding::Json => {
137
- serde_json::to_vec(msg).map_err(|e| ZapError::ipc(format!("JSON serialize error: {}", e)))
138
- }
139
- }
140
- }
141
-
142
- /// Deserialize an IPC message from bytes, auto-detecting encoding
143
- pub fn deserialize_message(data: &[u8]) -> ZapResult<IpcMessage> {
144
- if data.is_empty() {
145
- return Err(ZapError::ipc("Empty message".to_string()));
146
- }
147
-
148
- // Auto-detect encoding from first byte
149
- let first_byte = data[0];
150
- if first_byte == b'{' {
151
- // JSON
152
- serde_json::from_slice(data).map_err(|e| ZapError::ipc(format!("JSON deserialize error: {}", e)))
153
- } else {
154
- // MessagePack (maps start with 0x80-0xBF, 0xDE, or 0xDF)
155
- rmp_serde::from_slice(data).map_err(|e| ZapError::ipc(format!("MessagePack deserialize error: {}", e)))
156
- }
157
- }
158
-
159
- /// Request data sent to TypeScript handler
160
- #[derive(Debug, Clone, Serialize, Deserialize)]
161
- pub struct IpcRequest {
162
- /// Unique request ID for correlation across Rust/TypeScript boundary
163
- pub request_id: String,
164
-
165
- /// HTTP method (GET, POST, etc.)
166
- pub method: String,
167
-
168
- /// Full path with query string
169
- pub path: String,
170
-
171
- /// Path without query string
172
- pub path_only: String,
173
-
174
- /// Query parameters
175
- pub query: HashMap<String, String>,
176
-
177
- /// Route parameters (from :id in path)
178
- pub params: HashMap<String, String>,
179
-
180
- /// HTTP headers
181
- pub headers: HashMap<String, String>,
182
-
183
- /// Request body as UTF-8 string
184
- pub body: String,
185
-
186
- /// Cookies parsed from headers
187
- pub cookies: HashMap<String, String>,
188
- }
189
-
190
- /// IPC Server - receives requests from Rust, forwards to TypeScript
191
- pub struct IpcServer {
192
- socket_path: String,
193
- }
194
-
195
- impl IpcServer {
196
- /// Create a new IPC server
197
- pub fn new(socket_path: String) -> Self {
198
- Self { socket_path }
199
- }
200
-
201
- /// Start listening on the Unix socket
202
- pub async fn listen(&self) -> ZapResult<()> {
203
- // Remove existing socket file if it exists
204
- #[cfg(unix)]
205
- {
206
- let _ = std::fs::remove_file(&self.socket_path);
207
- }
208
-
209
- // Create Unix socket listener
210
- let listener = tokio::net::UnixListener::bind(&self.socket_path)
211
- .map_err(|e| ZapError::ipc(format!("Failed to bind socket: {}", e)))?;
212
-
213
- tracing::info!("🔌 IPC server listening on {}", self.socket_path);
214
-
215
- // Accept connections in background
216
- tokio::spawn(async move {
217
- loop {
218
- match listener.accept().await {
219
- Ok((stream, _)) => {
220
- tokio::spawn(async move {
221
- if let Err(e) = handle_ipc_connection(stream).await {
222
- tracing::error!("IPC connection error: {}", e);
223
- }
224
- });
225
- }
226
- Err(e) => {
227
- tracing::error!("IPC accept error: {}", e);
228
- }
229
- }
230
- }
231
- });
232
-
233
- Ok(())
234
- }
235
- }
236
-
237
- /// IPC Client - connects to TypeScript's IPC server
238
- pub struct IpcClient {
239
- stream: UnixStream,
240
- encoding: IpcEncoding,
241
- }
242
-
243
- impl IpcClient {
244
- /// Connect to a remote IPC server with default MessagePack encoding
245
- pub async fn connect(socket_path: &str) -> ZapResult<Self> {
246
- Self::connect_with_encoding(socket_path, IpcEncoding::default()).await
247
- }
248
-
249
- /// Connect to a remote IPC server with specified encoding
250
- pub async fn connect_with_encoding(socket_path: &str, encoding: IpcEncoding) -> ZapResult<Self> {
251
- let stream = UnixStream::connect(socket_path).await.map_err(|e| {
252
- ZapError::ipc(format!("Failed to connect to IPC socket: {}", e))
253
- })?;
254
-
255
- Ok(Self { stream, encoding })
256
- }
257
-
258
- /// Send a message over the IPC channel using length-prefixed framing
259
- pub async fn send_message(&mut self, msg: IpcMessage) -> ZapResult<()> {
260
- let payload = serialize_message(&msg, self.encoding)?;
261
- let len = payload.len() as u32;
262
-
263
- // ATOMIC: Combine length prefix and payload into single buffer to prevent frame corruption
264
- let mut frame = Vec::with_capacity(4 + payload.len());
265
- frame.extend_from_slice(&len.to_be_bytes());
266
- frame.extend_from_slice(&payload);
267
-
268
- // Single atomic write
269
- self.stream
270
- .write_all(&frame)
271
- .await
272
- .map_err(|e| ZapError::ipc(format!("Write frame error: {}", e)))?;
273
-
274
- self.stream.flush().await.map_err(|e| {
275
- ZapError::ipc(format!("Flush error: {}", e))
276
- })?;
277
-
278
- Ok(())
279
- }
280
-
281
- /// Receive a message from the IPC channel using length-prefixed framing
282
- pub async fn recv_message(&mut self) -> ZapResult<Option<IpcMessage>> {
283
- // Read 4-byte length prefix
284
- let mut len_buf = [0u8; 4];
285
- match self.stream.read_exact(&mut len_buf).await {
286
- Ok(_) => {}
287
- Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
288
- Err(e) => return Err(ZapError::ipc(format!("Read length error: {}", e))),
289
- }
290
-
291
- let len = u32::from_be_bytes(len_buf) as usize;
292
- if len > 100 * 1024 * 1024 {
293
- // 100MB limit
294
- return Err(ZapError::ipc(format!("Message too large: {} bytes", len)));
295
- }
296
-
297
- // Read payload
298
- let mut buffer = vec![0u8; len];
299
- self.stream
300
- .read_exact(&mut buffer)
301
- .await
302
- .map_err(|e| ZapError::ipc(format!("Read payload error: {}", e)))?;
303
-
304
- // Auto-detect encoding and deserialize
305
- let msg = deserialize_message(&buffer)?;
306
-
307
- Ok(Some(msg))
308
- }
309
-
310
- /// Send a message and receive a response (request-response pattern)
311
- pub async fn send_recv(&mut self, msg: IpcMessage) -> ZapResult<IpcMessage> {
312
- self.send_message(msg).await?;
313
- match self.recv_message().await? {
314
- Some(response) => Ok(response),
315
- None => Err(ZapError::ipc("Connection closed".to_string())),
316
- }
317
- }
318
-
319
- /// Get the encoding being used
320
- pub fn encoding(&self) -> IpcEncoding {
321
- self.encoding
322
- }
323
- }
324
-
325
- /// Handle an IPC client connection (for future use)
326
- async fn handle_ipc_connection(mut _stream: UnixStream) -> ZapResult<()> {
327
- // Currently, the Rust server only initiates connections to TypeScript
328
- // This handler is here for future bidirectional communication
329
- Ok(())
330
- }
331
-
332
- #[cfg(test)]
333
- mod tests {
334
- use super::*;
335
-
336
- #[test]
337
- fn test_ipc_message_json_serialization() {
338
- let msg = IpcMessage::HealthCheck;
339
- let json = serde_json::to_string(&msg).unwrap();
340
- assert!(json.contains("health_check"));
341
-
342
- let decoded: IpcMessage = serde_json::from_str(&json).unwrap();
343
- matches!(decoded, IpcMessage::HealthCheck);
344
- }
345
-
346
- #[test]
347
- fn test_ipc_message_msgpack_serialization() {
348
- let msg = IpcMessage::HealthCheck;
349
- let msgpack = serialize_message(&msg, IpcEncoding::MessagePack).unwrap();
350
- let decoded = deserialize_message(&msgpack).unwrap();
351
- matches!(decoded, IpcMessage::HealthCheck);
352
- }
353
-
354
- #[test]
355
- fn test_ipc_request_json_serialization() {
356
- let req = IpcRequest {
357
- request_id: "test-request-123".to_string(),
358
- method: "GET".to_string(),
359
- path: "/api/users/123?sort=asc".to_string(),
360
- path_only: "/api/users/123".to_string(),
361
- query: {
362
- let mut m = HashMap::new();
363
- m.insert("sort".to_string(), "asc".to_string());
364
- m
365
- },
366
- params: {
367
- let mut m = HashMap::new();
368
- m.insert("id".to_string(), "123".to_string());
369
- m
370
- },
371
- headers: HashMap::new(),
372
- body: String::new(),
373
- cookies: HashMap::new(),
374
- };
375
-
376
- let json = serde_json::to_string(&req).unwrap();
377
- let decoded: IpcRequest = serde_json::from_str(&json).unwrap();
378
-
379
- assert_eq!(decoded.method, "GET");
380
- assert_eq!(decoded.path, "/api/users/123?sort=asc");
381
- assert_eq!(decoded.params.get("id").unwrap(), "123");
382
- }
383
-
384
- #[test]
385
- fn test_ipc_request_msgpack_serialization() {
386
- let req = IpcRequest {
387
- request_id: "test-request-123".to_string(),
388
- method: "GET".to_string(),
389
- path: "/api/users/123".to_string(),
390
- path_only: "/api/users/123".to_string(),
391
- query: HashMap::new(),
392
- params: {
393
- let mut m = HashMap::new();
394
- m.insert("id".to_string(), "123".to_string());
395
- m
396
- },
397
- headers: HashMap::new(),
398
- body: String::new(),
399
- cookies: HashMap::new(),
400
- };
401
-
402
- let msg = IpcMessage::InvokeHandler {
403
- handler_id: "handler_0".to_string(),
404
- request: req,
405
- };
406
-
407
- let msgpack = serialize_message(&msg, IpcEncoding::MessagePack).unwrap();
408
- let json = serialize_message(&msg, IpcEncoding::Json).unwrap();
409
-
410
- // MessagePack should be smaller
411
- assert!(msgpack.len() < json.len(), "MessagePack ({}) should be smaller than JSON ({})", msgpack.len(), json.len());
412
-
413
- // Both should deserialize correctly
414
- let decoded_msgpack = deserialize_message(&msgpack).unwrap();
415
- let decoded_json = deserialize_message(&json).unwrap();
416
-
417
- if let (
418
- IpcMessage::InvokeHandler { request: req1, .. },
419
- IpcMessage::InvokeHandler { request: req2, .. },
420
- ) = (decoded_msgpack, decoded_json)
421
- {
422
- assert_eq!(req1.method, req2.method);
423
- assert_eq!(req1.path, req2.path);
424
- } else {
425
- panic!("Unexpected message types");
426
- }
427
- }
428
-
429
- #[test]
430
- fn test_auto_detect_encoding() {
431
- let msg = IpcMessage::HealthCheck;
432
-
433
- // JSON starts with '{'
434
- let json = serialize_message(&msg, IpcEncoding::Json).unwrap();
435
- assert_eq!(json[0], b'{');
436
- let decoded_json = deserialize_message(&json).unwrap();
437
- matches!(decoded_json, IpcMessage::HealthCheck);
438
-
439
- // MessagePack starts with 0x80-0xBF for fixmap
440
- let msgpack = serialize_message(&msg, IpcEncoding::MessagePack).unwrap();
441
- assert!(msgpack[0] >= 0x80 || msgpack[0] == 0xDE || msgpack[0] == 0xDF);
442
- let decoded_msgpack = deserialize_message(&msgpack).unwrap();
443
- matches!(decoded_msgpack, IpcMessage::HealthCheck);
444
- }
445
-
446
- #[test]
447
- fn test_stream_messages() {
448
- let start = IpcMessage::StreamStart {
449
- stream_id: "stream-123".to_string(),
450
- status: 200,
451
- headers: HashMap::new(),
452
- };
453
-
454
- let chunk = IpcMessage::StreamChunk {
455
- stream_id: "stream-123".to_string(),
456
- data: "SGVsbG8gV29ybGQ=".to_string(), // "Hello World" base64
457
- };
458
-
459
- let end = IpcMessage::StreamEnd {
460
- stream_id: "stream-123".to_string(),
461
- };
462
-
463
- // Test serialization round-trip
464
- for msg in [start, chunk, end] {
465
- let msgpack = serialize_message(&msg, IpcEncoding::MessagePack).unwrap();
466
- let _decoded = deserialize_message(&msgpack).unwrap();
467
- }
468
- }
469
-
470
- #[test]
471
- fn test_websocket_messages() {
472
- let connect = IpcMessage::WsConnect {
473
- connection_id: "ws-123".to_string(),
474
- handler_id: "ws_handler_0".to_string(),
475
- path: "/ws/chat".to_string(),
476
- headers: HashMap::new(),
477
- };
478
-
479
- let message = IpcMessage::WsMessage {
480
- connection_id: "ws-123".to_string(),
481
- handler_id: "ws_handler_0".to_string(),
482
- data: "Hello".to_string(),
483
- binary: false,
484
- };
485
-
486
- let close = IpcMessage::WsClose {
487
- connection_id: "ws-123".to_string(),
488
- handler_id: "ws_handler_0".to_string(),
489
- code: Some(1000),
490
- reason: Some("Normal closure".to_string()),
491
- };
492
-
493
- // Test serialization round-trip
494
- for msg in [connect, message, close] {
495
- let msgpack = serialize_message(&msg, IpcEncoding::MessagePack).unwrap();
496
- let _decoded = deserialize_message(&msgpack).unwrap();
497
- }
498
- }
499
- }