1use crate::errors::{PolyfillError, Result};
7use crate::types::*;
8use crate::ws_hot_path::{WsBookApplyStats, WsBookUpdateProcessor};
9use chrono::Utc;
10use futures::{ready, SinkExt, Stream, StreamExt};
11use parking_lot::Mutex;
12use serde_json::Value;
13use std::collections::VecDeque;
14use std::pin::Pin;
15use std::task::{Context, Poll};
16use tokio::sync::mpsc;
17use tracing::{debug, error, info, warn};
18
19pub trait MarketStream: Stream<Item = Result<StreamMessage>> + Send + Sync {
21 fn subscribe(&mut self, subscription: Subscription) -> Result<()>;
23
24 fn unsubscribe(&mut self, token_ids: &[String]) -> Result<()>;
26
27 fn is_connected(&self) -> bool;
29
30 fn get_stats(&self) -> StreamStats;
32}
33
34#[derive(Debug)]
36#[allow(dead_code)]
37pub struct WebSocketStream {
38 connection: Option<
40 tokio_tungstenite::WebSocketStream<
41 tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
42 >,
43 >,
44 url: String,
46 auth: Option<WssAuth>,
48 subscriptions: Vec<WssSubscription>,
50 pending: VecDeque<StreamMessage>,
55 pending_capacity: usize,
56 stats: StreamStats,
58 reconnect_config: ReconnectConfig,
60}
61
62#[derive(Debug, Clone)]
64pub struct StreamStats {
65 pub messages_received: u64,
66 pub messages_sent: u64,
67 pub errors: u64,
68 pub dropped_messages: u64,
69 pub last_message_time: Option<chrono::DateTime<Utc>>,
70 pub connection_uptime: std::time::Duration,
71 pub reconnect_count: u32,
72}
73
74#[derive(Debug, Clone)]
76pub struct ReconnectConfig {
77 pub max_retries: u32,
78 pub base_delay: std::time::Duration,
79 pub max_delay: std::time::Duration,
80 pub backoff_multiplier: f64,
81}
82
83impl Default for ReconnectConfig {
84 fn default() -> Self {
85 Self {
86 max_retries: 5,
87 base_delay: std::time::Duration::from_secs(1),
88 max_delay: std::time::Duration::from_secs(60),
89 backoff_multiplier: 2.0,
90 }
91 }
92}
93
94impl WebSocketStream {
95 pub fn new(url: &str) -> Self {
97 let pending_capacity = 1024;
98
99 Self {
100 connection: None,
101 url: url.to_string(),
102 auth: None,
103 subscriptions: Vec::new(),
104 pending: VecDeque::with_capacity(pending_capacity),
105 pending_capacity,
106 stats: StreamStats {
107 messages_received: 0,
108 messages_sent: 0,
109 errors: 0,
110 dropped_messages: 0,
111 last_message_time: None,
112 connection_uptime: std::time::Duration::ZERO,
113 reconnect_count: 0,
114 },
115 reconnect_config: ReconnectConfig::default(),
116 }
117 }
118
119 fn enqueue(&mut self, message: StreamMessage) {
120 if self.pending.len() >= self.pending_capacity {
121 let _ = self.pending.pop_front();
122 self.stats.dropped_messages += 1;
123 }
124 self.pending.push_back(message);
125 }
126
127 pub fn with_auth(mut self, auth: WssAuth) -> Self {
129 self.auth = Some(auth);
130 self
131 }
132
133 async fn connect(&mut self) -> Result<()> {
135 let (ws_stream, _) = tokio_tungstenite::connect_async(&self.url)
136 .await
137 .map_err(|e| {
138 PolyfillError::stream(
139 format!("WebSocket connection failed: {}", e),
140 crate::errors::StreamErrorKind::ConnectionFailed,
141 )
142 })?;
143
144 self.connection = Some(ws_stream);
145 info!("Connected to WebSocket stream at {}", self.url);
146 Ok(())
147 }
148
149 async fn send_message(&mut self, message: Value) -> Result<()> {
151 if let Some(connection) = &mut self.connection {
152 let text = serde_json::to_string(&message).map_err(|e| {
153 PolyfillError::parse(format!("Failed to serialize message: {}", e), None)
154 })?;
155
156 let ws_message = tokio_tungstenite::tungstenite::Message::Text(text);
157 connection.send(ws_message).await.map_err(|e| {
158 PolyfillError::stream(
159 format!("Failed to send message: {}", e),
160 crate::errors::StreamErrorKind::MessageCorrupted,
161 )
162 })?;
163
164 self.stats.messages_sent += 1;
165 }
166
167 Ok(())
168 }
169
170 pub async fn subscribe_async(&mut self, subscription: WssSubscription) -> Result<()> {
172 if self.connection.is_none() {
174 self.connect().await?;
175 }
176
177 let message = serde_json::to_value(&subscription).map_err(|e| {
180 PolyfillError::parse(format!("Failed to serialize subscription: {}", e), None)
181 })?;
182
183 self.send_message(message).await?;
184 self.subscriptions.push(subscription.clone());
185
186 info!("Subscribed to {} channel", subscription.channel_type);
187 Ok(())
188 }
189
190 pub async fn subscribe_user_channel(&mut self, markets: Vec<String>) -> Result<()> {
192 let auth = self
193 .auth
194 .as_ref()
195 .ok_or_else(|| PolyfillError::auth("No authentication provided for WebSocket"))?
196 .clone();
197
198 let subscription = WssSubscription {
199 channel_type: "user".to_string(),
200 operation: Some("subscribe".to_string()),
201 markets,
202 asset_ids: Vec::new(),
203 initial_dump: Some(true),
204 custom_feature_enabled: None,
205 auth: Some(auth),
206 };
207
208 self.subscribe_async(subscription).await
209 }
210
211 pub async fn subscribe_market_channel(&mut self, asset_ids: Vec<String>) -> Result<()> {
214 let subscription = WssSubscription {
215 channel_type: "market".to_string(),
216 operation: Some("subscribe".to_string()),
217 markets: Vec::new(),
218 asset_ids,
219 initial_dump: Some(true),
220 custom_feature_enabled: None,
221 auth: None,
222 };
223
224 self.subscribe_async(subscription).await
225 }
226
227 pub async fn subscribe_market_channel_with_features(
230 &mut self,
231 asset_ids: Vec<String>,
232 ) -> Result<()> {
233 let subscription = WssSubscription {
234 channel_type: "market".to_string(),
235 operation: Some("subscribe".to_string()),
236 markets: Vec::new(),
237 asset_ids,
238 initial_dump: Some(true),
239 custom_feature_enabled: Some(true),
240 auth: None,
241 };
242
243 self.subscribe_async(subscription).await
244 }
245
246 pub async fn unsubscribe_market_channel(&mut self, asset_ids: Vec<String>) -> Result<()> {
248 let subscription = WssSubscription {
249 channel_type: "market".to_string(),
250 operation: Some("unsubscribe".to_string()),
251 markets: Vec::new(),
252 asset_ids,
253 initial_dump: None,
254 custom_feature_enabled: None,
255 auth: None,
256 };
257
258 self.subscribe_async(subscription).await
259 }
260
261 pub async fn unsubscribe_user_channel(&mut self, markets: Vec<String>) -> Result<()> {
263 let auth = self
264 .auth
265 .as_ref()
266 .ok_or_else(|| PolyfillError::auth("No authentication provided for WebSocket"))?
267 .clone();
268
269 let subscription = WssSubscription {
270 channel_type: "user".to_string(),
271 operation: Some("unsubscribe".to_string()),
272 markets,
273 asset_ids: Vec::new(),
274 initial_dump: None,
275 custom_feature_enabled: None,
276 auth: Some(auth),
277 };
278
279 self.subscribe_async(subscription).await
280 }
281
282 #[allow(dead_code)]
284 async fn handle_message(
285 &mut self,
286 message: tokio_tungstenite::tungstenite::Message,
287 ) -> Result<()> {
288 match message {
289 tokio_tungstenite::tungstenite::Message::Text(text) => {
290 debug!("Received WebSocket message: {}", text);
291
292 let stream_messages = crate::decode::parse_stream_messages(&text)?;
294 for stream_message in stream_messages {
295 self.enqueue(stream_message);
296 }
297
298 self.stats.messages_received += 1;
299 self.stats.last_message_time = Some(Utc::now());
300 },
301 tokio_tungstenite::tungstenite::Message::Close(_) => {
302 info!("WebSocket connection closed by server");
303 self.connection = None;
304 },
305 tokio_tungstenite::tungstenite::Message::Ping(data) => {
306 if let Some(connection) = &mut self.connection {
308 let pong = tokio_tungstenite::tungstenite::Message::Pong(data);
309 if let Err(e) = connection.send(pong).await {
310 error!("Failed to send pong: {}", e);
311 }
312 }
313 },
314 tokio_tungstenite::tungstenite::Message::Pong(_) => {
315 debug!("Received pong");
317 },
318 tokio_tungstenite::tungstenite::Message::Binary(_) => {
319 warn!("Received binary message (not supported)");
320 },
321 tokio_tungstenite::tungstenite::Message::Frame(_) => {
322 warn!("Received raw frame (not supported)");
323 },
324 }
325
326 Ok(())
327 }
328
329 #[allow(dead_code)]
331 fn parse_polymarket_messages(&self, text: &str) -> Result<Vec<StreamMessage>> {
332 crate::decode::parse_stream_messages(text)
333 }
334
335 #[allow(dead_code)]
337 async fn reconnect(&mut self) -> Result<()> {
338 let mut delay = self.reconnect_config.base_delay;
339 let mut retries = 0;
340
341 while retries < self.reconnect_config.max_retries {
342 warn!("Attempting to reconnect (attempt {})", retries + 1);
343
344 match self.connect().await {
345 Ok(()) => {
346 info!("Successfully reconnected");
347 self.stats.reconnect_count += 1;
348
349 let subscriptions = self.subscriptions.clone();
351 for subscription in subscriptions {
352 self.send_message(serde_json::to_value(subscription)?)
353 .await?;
354 }
355
356 return Ok(());
357 },
358 Err(e) => {
359 error!("Reconnection attempt {} failed: {}", retries + 1, e);
360 retries += 1;
361
362 if retries < self.reconnect_config.max_retries {
363 tokio::time::sleep(delay).await;
364 delay = std::cmp::min(
365 delay.mul_f64(self.reconnect_config.backoff_multiplier),
366 self.reconnect_config.max_delay,
367 );
368 }
369 },
370 }
371 }
372
373 Err(PolyfillError::stream(
374 format!(
375 "Failed to reconnect after {} attempts",
376 self.reconnect_config.max_retries
377 ),
378 crate::errors::StreamErrorKind::ConnectionFailed,
379 ))
380 }
381}
382
383fn poll_send_pong(
384 connection: &mut tokio_tungstenite::WebSocketStream<
385 tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
386 >,
387 cx: &mut Context<'_>,
388 data: Vec<u8>,
389) -> Poll<Result<()>> {
390 ready!(connection.poll_ready_unpin(cx)).map_err(|e| {
391 PolyfillError::stream(
392 format!("Failed to prepare pong: {}", e),
393 crate::errors::StreamErrorKind::MessageCorrupted,
394 )
395 })?;
396 connection
397 .start_send_unpin(tokio_tungstenite::tungstenite::Message::Pong(data))
398 .map_err(|e| {
399 PolyfillError::stream(
400 format!("Failed to send pong: {}", e),
401 crate::errors::StreamErrorKind::MessageCorrupted,
402 )
403 })?;
404 ready!(connection.poll_flush_unpin(cx)).map_err(|e| {
405 PolyfillError::stream(
406 format!("Failed to flush pong: {}", e),
407 crate::errors::StreamErrorKind::MessageCorrupted,
408 )
409 })?;
410 Poll::Ready(Ok(()))
411}
412
413pub struct WebSocketBookApplier<'a> {
420 stream: WebSocketStream,
421 books: &'a crate::book::OrderBookManager,
422 processor: WsBookUpdateProcessor,
423}
424
425impl WebSocketStream {
426 pub fn into_book_applier<'a>(
431 mut self,
432 books: &'a crate::book::OrderBookManager,
433 processor: WsBookUpdateProcessor,
434 ) -> WebSocketBookApplier<'a> {
435 self.pending.clear();
437 WebSocketBookApplier {
438 stream: self,
439 books,
440 processor,
441 }
442 }
443}
444
445impl<'a> WebSocketBookApplier<'a> {
446 pub fn stream_mut(&mut self) -> &mut WebSocketStream {
448 &mut self.stream
449 }
450
451 pub fn stream_stats(&self) -> StreamStats {
453 self.stream.stats.clone()
454 }
455
456 pub fn processor_mut(&mut self) -> &mut WsBookUpdateProcessor {
458 &mut self.processor
459 }
460
461 pub fn apply_text_message(&mut self, text: String) -> Result<WsBookApplyStats> {
467 let stats = self.processor.process_text(text, self.books)?;
468 self.stream.stats.messages_received += 1;
469 self.stream.stats.last_message_time = Some(Utc::now());
470 Ok(stats)
471 }
472
473 pub fn apply_bytes_message(&mut self, bytes: &mut [u8]) -> Result<WsBookApplyStats> {
479 let stats = self.processor.process_bytes(bytes, self.books)?;
480 self.stream.stats.messages_received += 1;
481 self.stream.stats.last_message_time = Some(Utc::now());
482 Ok(stats)
483 }
484}
485
486impl<'a> Stream for WebSocketBookApplier<'a> {
487 type Item = Result<WsBookApplyStats>;
488
489 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
490 loop {
491 let Some(connection) = &mut self.stream.connection else {
492 return Poll::Ready(None);
493 };
494
495 match connection.poll_next_unpin(cx) {
496 Poll::Pending => return Poll::Pending,
497 Poll::Ready(Some(Ok(msg))) => match msg {
498 tokio_tungstenite::tungstenite::Message::Text(text) => {
499 match self.apply_text_message(text) {
500 Ok(stats) => {
501 if stats.book_messages == 0 {
502 continue;
503 }
504 return Poll::Ready(Some(Ok(stats)));
505 },
506 Err(e) => {
507 self.stream.stats.errors += 1;
508 return Poll::Ready(Some(Err(e)));
509 },
510 }
511 },
512 tokio_tungstenite::tungstenite::Message::Close(_) => {
513 info!("WebSocket connection closed by server");
514 self.stream.connection = None;
515 return Poll::Ready(None);
516 },
517 tokio_tungstenite::tungstenite::Message::Ping(data) => {
518 match poll_send_pong(connection, cx, data) {
519 Poll::Ready(Ok(())) => continue,
520 Poll::Ready(Err(e)) => {
521 self.stream.stats.errors += 1;
522 return Poll::Ready(Some(Err(e)));
523 },
524 Poll::Pending => return Poll::Pending,
525 }
526 },
527 tokio_tungstenite::tungstenite::Message::Pong(_) => continue,
528 tokio_tungstenite::tungstenite::Message::Binary(_) => continue,
529 tokio_tungstenite::tungstenite::Message::Frame(_) => continue,
530 },
531 Poll::Ready(Some(Err(e))) => {
532 error!("WebSocket error: {}", e);
533 self.stream.stats.errors += 1;
534 return Poll::Ready(Some(Err(e.into())));
535 },
536 Poll::Ready(None) => {
537 info!("WebSocket stream ended");
538 return Poll::Ready(None);
539 },
540 }
541 }
542 }
543}
544
545impl Stream for WebSocketStream {
546 type Item = Result<StreamMessage>;
547
548 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
549 loop {
550 if let Some(message) = self.pending.pop_front() {
551 return Poll::Ready(Some(Ok(message)));
552 }
553
554 let Some(connection) = &mut self.connection else {
555 return Poll::Ready(None);
556 };
557
558 match connection.poll_next_unpin(cx) {
559 Poll::Pending => return Poll::Pending,
560 Poll::Ready(Some(Ok(ws_message))) => match ws_message {
561 tokio_tungstenite::tungstenite::Message::Text(text) => {
562 match crate::decode::parse_stream_messages(&text) {
563 Ok(messages) => {
564 let mut iter = messages.into_iter();
565 let Some(first) = iter.next() else {
566 continue;
567 };
568
569 for msg in iter {
570 self.enqueue(msg);
571 }
572 self.stats.messages_received += 1;
573 self.stats.last_message_time = Some(Utc::now());
574 return Poll::Ready(Some(Ok(first)));
575 },
576 Err(e) => {
577 self.stats.errors += 1;
578 return Poll::Ready(Some(Err(e)));
579 },
580 }
581 },
582 tokio_tungstenite::tungstenite::Message::Close(_) => {
583 info!("WebSocket connection closed by server");
584 self.connection = None;
585 return Poll::Ready(None);
586 },
587 tokio_tungstenite::tungstenite::Message::Ping(data) => {
588 match poll_send_pong(connection, cx, data) {
589 Poll::Ready(Ok(())) => continue,
590 Poll::Ready(Err(e)) => {
591 self.stats.errors += 1;
592 return Poll::Ready(Some(Err(e)));
593 },
594 Poll::Pending => return Poll::Pending,
595 }
596 },
597 tokio_tungstenite::tungstenite::Message::Pong(_) => continue,
598 tokio_tungstenite::tungstenite::Message::Binary(_) => continue,
599 tokio_tungstenite::tungstenite::Message::Frame(_) => continue,
600 },
601 Poll::Ready(Some(Err(e))) => {
602 error!("WebSocket error: {}", e);
603 self.stats.errors += 1;
604 return Poll::Ready(Some(Err(e.into())));
605 },
606 Poll::Ready(None) => {
607 info!("WebSocket stream ended");
608 return Poll::Ready(None);
609 },
610 }
611 }
612 }
613}
614
615impl MarketStream for WebSocketStream {
616 fn subscribe(&mut self, _subscription: Subscription) -> Result<()> {
617 Ok(())
619 }
620
621 fn unsubscribe(&mut self, _token_ids: &[String]) -> Result<()> {
622 Ok(())
624 }
625
626 fn is_connected(&self) -> bool {
627 self.connection.is_some()
628 }
629
630 fn get_stats(&self) -> StreamStats {
631 self.stats.clone()
632 }
633}
634
635#[derive(Debug)]
637pub struct MockStream {
638 messages: Vec<Result<StreamMessage>>,
639 index: usize,
640 connected: bool,
641}
642
643impl Default for MockStream {
644 fn default() -> Self {
645 Self::new()
646 }
647}
648
649impl MockStream {
650 pub fn new() -> Self {
651 Self {
652 messages: Vec::new(),
653 index: 0,
654 connected: true,
655 }
656 }
657
658 pub fn add_message(&mut self, message: StreamMessage) {
659 self.messages.push(Ok(message));
660 }
661
662 pub fn add_error(&mut self, error: PolyfillError) {
663 self.messages.push(Err(error));
664 }
665
666 pub fn set_connected(&mut self, connected: bool) {
667 self.connected = connected;
668 }
669}
670
671impl Stream for MockStream {
672 type Item = Result<StreamMessage>;
673
674 fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
675 if self.index >= self.messages.len() {
676 Poll::Ready(None)
677 } else {
678 let message = self.messages[self.index].clone();
679 self.index += 1;
680 Poll::Ready(Some(message))
681 }
682 }
683}
684
685impl MarketStream for MockStream {
686 fn subscribe(&mut self, _subscription: Subscription) -> Result<()> {
687 Ok(())
688 }
689
690 fn unsubscribe(&mut self, _token_ids: &[String]) -> Result<()> {
691 Ok(())
692 }
693
694 fn is_connected(&self) -> bool {
695 self.connected
696 }
697
698 fn get_stats(&self) -> StreamStats {
699 StreamStats {
700 messages_received: self.messages.len() as u64,
701 messages_sent: 0,
702 errors: self.messages.iter().filter(|m| m.is_err()).count() as u64,
703 dropped_messages: 0,
704 last_message_time: None,
705 connection_uptime: std::time::Duration::ZERO,
706 reconnect_count: 0,
707 }
708 }
709}
710
711#[allow(dead_code)]
713pub struct StreamManager {
714 streams: Vec<Box<dyn MarketStream>>,
715 message_subscribers: Mutex<Vec<mpsc::UnboundedSender<StreamMessage>>>,
716}
717
718impl Default for StreamManager {
719 fn default() -> Self {
720 Self::new()
721 }
722}
723
724impl StreamManager {
725 pub fn new() -> Self {
726 Self {
727 streams: Vec::new(),
728 message_subscribers: Mutex::new(Vec::new()),
729 }
730 }
731
732 pub fn add_stream(&mut self, stream: Box<dyn MarketStream>) {
733 self.streams.push(stream);
734 }
735
736 pub fn get_message_receiver(&mut self) -> mpsc::UnboundedReceiver<StreamMessage> {
737 let (tx, rx) = mpsc::unbounded_channel();
738 self.message_subscribers.lock().push(tx);
739 rx
740 }
741
742 pub fn broadcast_message(&self, message: StreamMessage) -> Result<()> {
743 let mut subscribers = self.message_subscribers.lock();
744 subscribers.retain(|tx| tx.send(message.clone()).is_ok());
745 Ok(())
746 }
747}
748
749#[cfg(test)]
750mod tests {
751 use super::*;
752 use rust_decimal::Decimal;
753 use std::str::FromStr;
754
755 #[test]
756 fn test_mock_stream() {
757 let mut stream = MockStream::new();
758
759 stream.add_message(StreamMessage::Book(BookUpdate {
761 asset_id: "1".to_string(),
762 market: "0xabc".to_string(),
763 timestamp: 1_234_567_890,
764 bids: vec![],
765 asks: vec![],
766 hash: None,
767 }));
768 stream.add_message(StreamMessage::PriceChange(PriceChange {
769 market: "0xabc".to_string(),
770 timestamp: 1_234_567_891,
771 price_changes: vec![],
772 }));
773
774 assert!(stream.is_connected());
775 assert_eq!(stream.get_stats().messages_received, 2);
776 }
777
778 #[test]
779 fn test_stream_manager() {
780 let mut manager = StreamManager::new();
781 let mock_stream = Box::new(MockStream::new());
782 manager.add_stream(mock_stream);
783 let mut first_rx = manager.get_message_receiver();
784 let mut second_rx = manager.get_message_receiver();
785
786 let message = StreamMessage::Book(BookUpdate {
788 asset_id: "1".to_string(),
789 market: "0xabc".to_string(),
790 timestamp: 1_234_567_890,
791 bids: vec![],
792 asks: vec![],
793 hash: None,
794 });
795 assert!(manager.broadcast_message(message).is_ok());
796
797 assert!(matches!(
798 first_rx.try_recv().unwrap(),
799 StreamMessage::Book(BookUpdate { asset_id, .. }) if asset_id == "1"
800 ));
801 assert!(matches!(
802 second_rx.try_recv().unwrap(),
803 StreamMessage::Book(BookUpdate { asset_id, .. }) if asset_id == "1"
804 ));
805
806 drop(first_rx);
807 let message = StreamMessage::PriceChange(PriceChange {
808 market: "0xabc".to_string(),
809 timestamp: 1_234_567_891,
810 price_changes: vec![],
811 });
812 assert!(manager.broadcast_message(message).is_ok());
813
814 assert!(matches!(
815 second_rx.try_recv().unwrap(),
816 StreamMessage::PriceChange(PriceChange { market, .. }) if market == "0xabc"
817 ));
818 }
819
820 #[test]
821 fn test_websocket_book_applier_apply_text_message_updates_book() {
822 let books = crate::book::OrderBookManager::new(64);
823 let _ = books.get_or_create_book("12345").unwrap();
824
825 let processor = WsBookUpdateProcessor::new(1024);
826 let stream = WebSocketStream::new("wss://example.com/ws");
827 let mut applier = stream.into_book_applier(&books, processor);
828
829 let msg = r#"{"event_type":"book","asset_id":"12345","timestamp":1,"bids":[{"price":"0.75","size":"10"}],"asks":[{"price":"0.76","size":"5"}]}"#.to_string();
830 let stats = applier.apply_text_message(msg).unwrap();
831 assert_eq!(stats.book_messages, 1);
832 assert_eq!(stats.book_levels_applied, 2);
833
834 let snapshot = books.get_book("12345").unwrap();
835 assert_eq!(snapshot.bids.len(), 1);
836 assert_eq!(snapshot.asks.len(), 1);
837 assert_eq!(snapshot.bids[0].price, Decimal::from_str("0.75").unwrap());
838 assert_eq!(snapshot.bids[0].size, Decimal::from_str("10").unwrap());
839 assert_eq!(snapshot.asks[0].price, Decimal::from_str("0.76").unwrap());
840 assert_eq!(snapshot.asks[0].size, Decimal::from_str("5").unwrap());
841 }
842
843 #[test]
844 fn test_websocket_book_applier_replaces_snapshot() {
845 let books = crate::book::OrderBookManager::new(64);
846 let _ = books.get_or_create_book("12345").unwrap();
847
848 let processor = WsBookUpdateProcessor::new(1024);
849 let stream = WebSocketStream::new("wss://example.com/ws");
850 let mut applier = stream.into_book_applier(&books, processor);
851
852 let first = r#"{"event_type":"book","asset_id":"12345","timestamp":"1757908892351","bids":[{"price":"0.74","size":"8"},{"price":"0.75","size":"10"}],"asks":[{"price":"0.76","size":"5"},{"price":"0.77","size":"4"}]}"#.to_string();
853 applier.apply_text_message(first).unwrap();
854
855 let second = r#"{"event_type":"book","asset_id":"12345","timestamp":"1757908892352","bids":[{"price":"0.75","size":"11"}],"asks":[{"price":"0.76","size":"6"}]}"#.to_string();
856 let stats = applier.apply_text_message(second).unwrap();
857 assert_eq!(stats.book_messages, 1);
858 assert_eq!(stats.book_levels_applied, 2);
859
860 let snapshot = books.get_book("12345").unwrap();
861 assert_eq!(snapshot.timestamp.timestamp_millis(), 1_757_908_892_352);
862 assert_eq!(snapshot.bids.len(), 1);
863 assert_eq!(snapshot.asks.len(), 1);
864 assert_eq!(snapshot.bids[0].price, Decimal::from_str("0.75").unwrap());
865 assert_eq!(snapshot.bids[0].size, Decimal::from_str("11").unwrap());
866 assert_eq!(snapshot.asks[0].price, Decimal::from_str("0.76").unwrap());
867 assert_eq!(snapshot.asks[0].size, Decimal::from_str("6").unwrap());
868 }
869}