pjson_rs/infrastructure/websocket/
client.rs1#[cfg(feature = "websocket-client")]
4use super::{StreamOptions, WsMessage};
5use crate::{
6 Error as PjsError, Result as PjsResult,
7 infrastructure::bounded_channel::{ByteBoundedSender, Envelope, byte_bounded_channel},
8};
9use futures::StreamExt;
10use serde_json::Value;
11use std::{
12 collections::HashMap,
13 sync::Arc,
14 time::{Duration, Instant},
15};
16use tokio::sync::{RwLock, mpsc};
17use tokio_tungstenite::{connect_async, tungstenite::Message};
18use tracing::{debug, error, info, warn};
19use url::Url;
20
21const MESSAGE_QUEUE_CAPACITY: usize = 1000;
28
29const MAX_QUEUED_MESSAGE_BYTES: usize = 16 * 1024 * 1024;
35
36pub struct PjsWebSocketClient {
38 url: Url,
39 sessions: Arc<RwLock<HashMap<String, ClientStreamSession>>>,
40 message_tx: ByteBoundedSender<String>,
41 message_rx: Arc<RwLock<Option<mpsc::Receiver<Envelope<String>>>>>,
42 write_timeout: Duration,
43}
44
45#[derive(Debug)]
47struct ClientStreamSession {
48 id: String,
49 created_at: Instant,
50 received_frames: HashMap<u32, ReceivedFrame>,
51 reconstructed_data: Value,
52 is_complete: bool,
53}
54
55#[derive(Debug, Clone)]
57struct ReceivedFrame {
58 received_at: Instant,
59 processed_at: Option<Instant>,
60}
61
62impl PjsWebSocketClient {
63 pub fn new(url: impl AsRef<str>) -> PjsResult<Self> {
65 let url = Url::parse(url.as_ref()).map_err(|e| PjsError::InvalidUrl(e.to_string()))?;
66
67 let (message_tx, message_rx) =
68 byte_bounded_channel(MESSAGE_QUEUE_CAPACITY, MAX_QUEUED_MESSAGE_BYTES);
69
70 Ok(Self {
71 url,
72 sessions: Arc::new(RwLock::new(HashMap::new())),
73 message_tx,
74 message_rx: Arc::new(RwLock::new(Some(message_rx))),
75 write_timeout: super::WRITE_TIMEOUT,
76 })
77 }
78
79 #[must_use]
109 pub fn with_write_timeout(mut self, write_timeout: Duration) -> Self {
110 self.write_timeout = write_timeout;
111 self
112 }
113
114 pub async fn connect(&self) -> PjsResult<()> {
116 info!("Connecting to WebSocket server: {}", self.url);
117
118 let (ws_stream, _) = connect_async(self.url.as_str())
119 .await
120 .map_err(|e| PjsError::ConnectionFailed(e.to_string()))?;
121
122 info!("WebSocket connection established");
123
124 let (mut write, mut read) = ws_stream.split();
125
126 let mut message_rx = self
128 .message_rx
129 .write()
130 .await
131 .take()
132 .ok_or_else(|| PjsError::ClientError("Client already connected".to_string()))?;
133
134 let write_timeout = self.write_timeout;
142 let send_task = tokio::spawn(async move {
143 while let Some(envelope) = message_rx.recv().await {
144 let (json_str, _budget_permit) = envelope.split();
145 if let Err(e) = super::send_with_write_timeout(
146 &mut write,
147 Message::Text(json_str.into()),
148 write_timeout,
149 )
150 .await
151 {
152 error!("Failed to send message: {}", e);
153 break;
154 }
155 }
156 });
157
158 let sessions = self.sessions.clone();
160 let message_tx = self.message_tx.clone();
161 let receive_task = tokio::spawn(async move {
162 while let Some(msg) = read.next().await {
163 match msg {
164 Ok(Message::Text(text)) => match serde_json::from_str::<WsMessage>(&text) {
165 Ok(ws_message) => {
166 if let Err(e) = Self::handle_incoming_message(
167 sessions.clone(),
168 message_tx.clone(),
169 ws_message,
170 )
171 .await
172 {
173 error!("Failed to handle incoming message: {}", e);
174 }
175 }
176 Err(e) => {
177 warn!("Failed to parse incoming message: {}", e);
178 }
179 },
180 Ok(Message::Binary(data)) => {
181 debug!("Received binary data: {} bytes", data.len());
182 }
183 Ok(Message::Ping(_data)) => {
184 debug!("Received ping, sending pong");
185 }
187 Ok(Message::Pong(_)) => {
188 debug!("Received pong");
189 }
190 Ok(Message::Close(_)) => {
191 info!("Server closed connection");
192 break;
193 }
194 Ok(Message::Frame(_)) => {
195 debug!("Received raw frame");
197 }
198 Err(e) => {
199 error!("WebSocket error: {}", e);
200 break;
201 }
202 }
203 }
204 });
205
206 tokio::select! {
208 _ = send_task => {
209 debug!("Send task completed");
210 }
211 _ = receive_task => {
212 debug!("Receive task completed");
213 }
214 }
215
216 info!("WebSocket connection closed");
217 Ok(())
218 }
219
220 pub async fn request_stream(
222 &self,
223 data: Value,
224 options: Option<StreamOptions>,
225 ) -> PjsResult<String> {
226 let session_id = uuid::Uuid::new_v4().to_string();
227 let options = options.unwrap_or_default();
228
229 let message = WsMessage::StreamInit {
230 session_id: session_id.clone(),
231 data,
232 options,
233 };
234 let json_str = serde_json::to_string(&message).map_err(|e| {
235 PjsError::ClientError(format!("Failed to serialize stream request: {e}"))
236 })?;
237 let len = json_str.len();
238
239 self.message_tx.send(json_str, len).await.map_err(|_| {
240 PjsError::ClientError(
241 "Failed to send stream request: outgoing channel closed".to_string(),
242 )
243 })?;
244
245 let session = ClientStreamSession {
247 id: session_id.clone(),
248 created_at: Instant::now(),
249 received_frames: HashMap::new(),
250 reconstructed_data: serde_json::json!({}),
251 is_complete: false,
252 };
253
254 self.sessions
255 .write()
256 .await
257 .insert(session_id.clone(), session);
258
259 info!("Requested stream initialization: {}", session_id);
260 Ok(session_id)
261 }
262
263 pub async fn get_current_data(&self, session_id: &str) -> PjsResult<Option<Value>> {
265 let sessions = self.sessions.read().await;
266 Ok(sessions
267 .get(session_id)
268 .map(|session| session.reconstructed_data.clone()))
269 }
270
271 pub async fn is_stream_complete(&self, session_id: &str) -> bool {
273 let sessions = self.sessions.read().await;
274 sessions
275 .get(session_id)
276 .map(|session| session.is_complete)
277 .unwrap_or(false)
278 }
279
280 pub async fn get_stream_stats(&self, session_id: &str) -> Option<StreamStats> {
282 let sessions = self.sessions.read().await;
283 sessions.get(session_id).map(|session| {
284 let total_frames = session.received_frames.len();
285 let processed_frames = session
286 .received_frames
287 .values()
288 .filter(|frame| frame.processed_at.is_some())
289 .count();
290
291 let avg_processing_time = if processed_frames > 0 {
292 let total_time: Duration = session
293 .received_frames
294 .values()
295 .filter_map(|frame| {
296 frame
297 .processed_at
298 .map(|processed| processed.duration_since(frame.received_at))
299 })
300 .sum();
301 Some(total_time / processed_frames as u32)
302 } else {
303 None
304 };
305
306 StreamStats {
307 session_id: session.id.clone(),
308 total_frames,
309 processed_frames,
310 is_complete: session.is_complete,
311 duration: session.created_at.elapsed(),
312 average_processing_time: avg_processing_time,
313 }
314 })
315 }
316
317 fn try_send_control_message(
325 message_tx: &ByteBoundedSender<String>,
326 message: &WsMessage,
327 kind: &str,
328 ) {
329 match serde_json::to_string(message) {
330 Ok(json_str) => {
331 let len = json_str.len();
332 if let Err(e) = message_tx.try_send(json_str, len) {
333 warn!(
334 "Dropping {} (channel full, byte budget exceeded, or closed): {:?}",
335 kind, e
336 );
337 }
338 }
339 Err(e) => warn!("Failed to serialize {}: {}", kind, e),
340 }
341 }
342
343 async fn handle_incoming_message(
344 sessions: Arc<RwLock<HashMap<String, ClientStreamSession>>>,
345 message_tx: ByteBoundedSender<String>,
346 message: WsMessage,
347 ) -> PjsResult<()> {
348 match message {
349 WsMessage::StreamFrame {
350 session_id,
351 frame_id,
352 priority: _priority,
353 payload,
354 is_complete,
355 } => {
356 debug!("Received frame {} for session {}", frame_id, session_id);
357
358 let processing_start = Instant::now();
359
360 {
361 let mut sessions = sessions.write().await;
362 if let Some(session) = sessions.get_mut(&session_id) {
363 let frame = ReceivedFrame {
365 received_at: processing_start,
366 processed_at: None,
367 };
368 session.received_frames.insert(frame_id, frame);
369
370 Self::apply_frame_to_data(&mut session.reconstructed_data, &payload)?;
372
373 if is_complete {
374 session.is_complete = true;
375 info!("Stream completed for session {}", session_id);
376 }
377
378 if let Some(frame) = session.received_frames.get_mut(&frame_id) {
380 frame.processed_at = Some(Instant::now());
381 }
382 }
383 }
384
385 let processing_time = processing_start.elapsed();
386
387 let ack_message = WsMessage::FrameAck {
389 session_id,
390 frame_id,
391 processing_time_ms: processing_time.as_millis() as u64,
392 };
393
394 Self::try_send_control_message(&message_tx, &ack_message, "frame acknowledgment");
398 }
399 WsMessage::StreamComplete {
400 session_id,
401 checksum,
402 } => {
403 info!("Stream completed: {} (checksum: {})", session_id, checksum);
404
405 let mut sessions = sessions.write().await;
406 if let Some(session) = sessions.get_mut(&session_id) {
407 session.is_complete = true;
408 }
409 }
410 WsMessage::Error {
411 session_id,
412 error,
413 code,
414 } => {
415 error!(
416 "Received error from server: session={:?}, error={}, code={}",
417 session_id, error, code
418 );
419 }
420 WsMessage::Ping { timestamp } => {
421 debug!("Received ping with timestamp: {}", timestamp);
422 let pong = WsMessage::Pong { timestamp };
423 Self::try_send_control_message(&message_tx, &pong, "pong");
426 }
427 WsMessage::Pong { timestamp } => {
428 debug!("Received pong with timestamp: {}", timestamp);
429 }
430 _ => {
431 warn!("Unhandled message type: {:?}", message);
432 }
433 }
434 Ok(())
435 }
436
437 fn apply_frame_to_data(data: &mut Value, payload: &Value) -> PjsResult<()> {
438 match (data.as_object_mut(), payload.as_object()) {
440 (Some(data_map), Some(payload_map)) => {
441 for (key, value) in payload_map {
442 data_map.insert(key.clone(), value.clone());
443 }
444 }
445 _ => {
446 *data = payload.clone();
447 }
448 }
449 Ok(())
450 }
451}
452
453#[derive(Debug, Clone)]
455pub struct StreamStats {
456 pub session_id: String,
458 pub total_frames: usize,
460 pub processed_frames: usize,
462 pub is_complete: bool,
464 pub duration: Duration,
466 pub average_processing_time: Option<Duration>,
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473 use serde_json::json;
474
475 #[tokio::test]
476 async fn test_client_creation() {
477 let client = PjsWebSocketClient::new("ws://localhost:3001/ws").unwrap();
478 assert_eq!(client.url.as_str(), "ws://localhost:3001/ws");
479 assert_eq!(client.write_timeout, super::super::WRITE_TIMEOUT);
480 }
481
482 #[tokio::test]
483 async fn test_with_write_timeout_overrides_default() {
484 let client = PjsWebSocketClient::new("ws://localhost:3001/ws")
485 .unwrap()
486 .with_write_timeout(Duration::from_secs(3));
487 assert_eq!(client.write_timeout, Duration::from_secs(3));
488 }
489
490 #[tokio::test]
491 async fn test_stream_session() {
492 let client = PjsWebSocketClient::new("ws://localhost:3001/ws").unwrap();
493 let data = json!({"test": "data"});
494
495 let session_id = client.request_stream(data, None).await.unwrap();
496 assert!(!session_id.is_empty());
497
498 let sessions = client.sessions.read().await;
499 assert!(sessions.contains_key(&session_id));
500 }
501
502 #[tokio::test]
503 async fn test_message_channel_is_bounded() {
504 let client = PjsWebSocketClient::new("ws://localhost:3001/ws").unwrap();
509 let tx = client.message_tx.clone();
510 let pong = "pong".to_string();
511
512 for _ in 0..MESSAGE_QUEUE_CAPACITY {
513 tx.try_send(pong.clone(), pong.len())
514 .expect("channel should accept sends up to its capacity");
515 }
516
517 let result = tx.try_send(pong.clone(), pong.len());
518 assert!(
519 matches!(
520 result,
521 Err(
522 crate::infrastructure::bounded_channel::TrySendError::Channel(
523 mpsc::error::TrySendError::Full(_)
524 )
525 )
526 ),
527 "channel must reject sends past capacity instead of growing unbounded"
528 );
529 }
530
531 #[tokio::test]
532 async fn test_message_channel_rejects_when_byte_budget_exceeded() {
533 let client = PjsWebSocketClient::new("ws://localhost:3001/ws").unwrap();
538 let tx = client.message_tx.clone();
539 let oversized = "x".repeat(MAX_QUEUED_MESSAGE_BYTES + 1);
540 let len = oversized.len();
541
542 assert!(
543 matches!(
544 tx.try_send(oversized, len),
545 Err(crate::infrastructure::bounded_channel::TrySendError::BudgetExceeded(_))
546 ),
547 "a single over-budget message must be rejected"
548 );
549 }
550
551 #[test]
552 fn test_apply_frame_to_data() {
553 let mut data = json!({"existing": "value"});
554 let payload = json!({"new": "data", "existing": "updated"});
555
556 PjsWebSocketClient::apply_frame_to_data(&mut data, &payload).unwrap();
557
558 assert_eq!(data["existing"], "updated");
559 assert_eq!(data["new"], "data");
560 }
561}