1use std::{
23 collections::VecDeque,
24 fmt::Debug,
25 sync::{
26 Arc,
27 atomic::{AtomicBool, Ordering},
28 },
29};
30
31use ahash::AHashMap;
32use nautilus_network::{
33 RECONNECTED,
34 retry::{RetryManager, create_websocket_retry_manager},
35 websocket::{SubscriptionState, WebSocketClient},
36};
37use tokio_tungstenite::tungstenite::Message;
38use ustr::Ustr;
39
40use super::{
41 DydxWsError, DydxWsResult,
42 client::DYDX_RATE_LIMIT_KEY_SUBSCRIPTION,
43 enums::{DydxWsChannel, DydxWsMessage, DydxWsOutputMessage},
44 error::DydxWebSocketError,
45 messages::{
46 DydxCandle, DydxMarketsContents, DydxOrderbookContents, DydxOrderbookSnapshotContents,
47 DydxSubscription, DydxTradeContents, DydxWsBlockHeightMessage, DydxWsCandlesMessage,
48 DydxWsChannelBatchDataMsg, DydxWsChannelDataMsg, DydxWsConnectedMsg, DydxWsFeedMessage,
49 DydxWsGenericMsg, DydxWsMarketsMessage, DydxWsOrderbookMessage,
50 DydxWsParentSubaccountsMessage, DydxWsSubaccountsChannelContents,
51 DydxWsSubaccountsChannelData, DydxWsSubaccountsMessage, DydxWsSubaccountsSubscribed,
52 DydxWsSubscriptionMsg, DydxWsTradesMessage,
53 },
54};
55
56#[derive(Debug, Clone)]
58pub enum HandlerCommand {
59 RegisterSubscription {
61 topic: String,
62 subscription: DydxSubscription,
63 },
64 UnregisterSubscription { topic: String },
66 SendText(String),
68 Disconnect,
70}
71
72pub struct FeedHandler {
77 cmd_rx: tokio::sync::mpsc::UnboundedReceiver<HandlerCommand>,
78 out_tx: tokio::sync::mpsc::UnboundedSender<DydxWsOutputMessage>,
79 raw_rx: tokio::sync::mpsc::UnboundedReceiver<Message>,
80 client: WebSocketClient,
81 signal: Arc<AtomicBool>,
82 retry_manager: RetryManager<DydxWsError>,
83 subscriptions: SubscriptionState,
84 subscription_messages: AHashMap<String, DydxSubscription>,
85 message_buffer: VecDeque<DydxWsOutputMessage>,
86 book_sequence: AHashMap<String, u64>,
87}
88
89impl Debug for FeedHandler {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 f.debug_struct(stringify!(FeedHandler))
92 .field("subscriptions", &self.subscriptions.len())
93 .finish_non_exhaustive()
94 }
95}
96
97impl FeedHandler {
98 #[must_use]
100 pub fn new(
101 cmd_rx: tokio::sync::mpsc::UnboundedReceiver<HandlerCommand>,
102 out_tx: tokio::sync::mpsc::UnboundedSender<DydxWsOutputMessage>,
103 raw_rx: tokio::sync::mpsc::UnboundedReceiver<Message>,
104 client: WebSocketClient,
105 signal: Arc<AtomicBool>,
106 subscriptions: SubscriptionState,
107 ) -> Self {
108 Self {
109 cmd_rx,
110 out_tx,
111 raw_rx,
112 client,
113 signal,
114 retry_manager: create_websocket_retry_manager(),
115 subscriptions,
116 subscription_messages: AHashMap::new(),
117 message_buffer: VecDeque::new(),
118 book_sequence: AHashMap::new(),
119 }
120 }
121
122 async fn send_with_retry(
123 &self,
124 payload: String,
125 rate_limit_keys: Option<&[Ustr]>,
126 ) -> Result<(), DydxWsError> {
127 let keys_owned: Option<Vec<Ustr>> = rate_limit_keys.map(|k| k.to_vec());
128 self.retry_manager
129 .execute_with_retry(
130 "websocket_send",
131 || {
132 let payload = payload.clone();
133 let keys = keys_owned.clone();
134 async move {
135 self.client
136 .send_text(payload, keys.as_deref())
137 .await
138 .map_err(|e| DydxWsError::ClientError(format!("Send failed: {e}")))
139 }
140 },
141 should_retry_dydx_error,
142 create_dydx_timeout_error,
143 )
144 .await
145 }
146
147 pub async fn run(&mut self) {
154 log::debug!("WebSocket handler started");
155
156 loop {
157 if !self.message_buffer.is_empty() {
159 let msg = self.message_buffer.pop_front().unwrap();
160 if self.out_tx.send(msg).is_err() {
161 log::debug!("Receiver dropped, stopping handler");
162 break;
163 }
164 continue;
165 }
166
167 tokio::select! {
168 Some(cmd) = self.cmd_rx.recv() => {
169 if self.handle_command(cmd).await {
170 break;
171 }
172 }
173
174 Some(msg) = self.raw_rx.recv() => {
175 log::trace!("Handler received raw message");
176 let msgs = self.process_raw_message(msg).await;
177 if !msgs.is_empty() {
178 let mut iter = msgs.into_iter();
179 let first = iter.next().expect("non-empty vec has first element");
181 self.message_buffer.extend(iter);
182 log::trace!("Handler sending message: {:?}", std::mem::discriminant(&first));
183 if self.out_tx.send(first).is_err() {
184 log::debug!("Receiver dropped, stopping handler");
185 break;
186 }
187 }
188 }
189
190 else => {
191 log::debug!("Handler shutting down: channels closed");
192 break;
193 }
194 }
195
196 if self.signal.load(Ordering::Acquire) {
197 log::debug!("Handler received stop signal");
198 break;
199 }
200 }
201 }
202
203 async fn process_raw_message(&mut self, msg: Message) -> Vec<DydxWsOutputMessage> {
204 match msg {
205 Message::Text(txt) => {
206 if txt == RECONNECTED {
207 self.clear_state();
208
209 if let Err(e) = self.replay_subscriptions().await {
210 log::error!("Failed to replay subscriptions after reconnect: {e}");
211 }
212 let topics = self.subscriptions.all_topics();
213 return vec![DydxWsOutputMessage::Reconnected { topics }];
214 }
215
216 match serde_json::from_str::<DydxWsFeedMessage>(&txt) {
218 Ok(feed_msg) => {
219 return self.handle_feed_message(feed_msg);
220 }
221 Err(e) => {
222 if txt.contains("v4_subaccounts") {
223 log::warn!(
224 "[WS_DESER] Failed to parse v4_subaccounts as DydxWsFeedMessage: {e}\nRaw: {txt}"
225 );
226 }
227 }
228 }
229
230 match serde_json::from_str::<serde_json::Value>(&txt) {
232 Ok(val) => match serde_json::from_value::<DydxWsGenericMsg>(val.clone()) {
233 Ok(meta) => {
234 let result = if meta.is_connected() {
235 serde_json::from_value::<DydxWsConnectedMsg>(val)
236 .map(DydxWsMessage::Connected)
237 } else if meta.is_subscribed() {
238 log::debug!("Processing subscribed message via fallback path");
239
240 if let Ok(sub_msg) =
241 serde_json::from_value::<DydxWsSubscriptionMsg>(val.clone())
242 {
243 if sub_msg.channel == DydxWsChannel::Subaccounts {
244 log::debug!("Parsing subaccounts subscription (fallback)");
245 serde_json::from_value::<DydxWsSubaccountsSubscribed>(val)
246 .map(DydxWsMessage::SubaccountsSubscribed)
247 .or_else(|e| {
248 log::warn!(
249 "Failed to parse subaccounts subscription: {e}"
250 );
251 Ok(DydxWsMessage::Subscribed(sub_msg))
252 })
253 } else {
254 Ok(DydxWsMessage::Subscribed(sub_msg))
255 }
256 } else {
257 serde_json::from_value::<DydxWsSubscriptionMsg>(val)
258 .map(DydxWsMessage::Subscribed)
259 }
260 } else if meta.is_unsubscribed() {
261 serde_json::from_value::<DydxWsSubscriptionMsg>(val)
262 .map(DydxWsMessage::Unsubscribed)
263 } else if meta.is_error() {
264 serde_json::from_value::<DydxWebSocketError>(val)
265 .map(DydxWsMessage::Error)
266 } else if meta.is_unknown() {
267 log::warn!("Received unknown WebSocket message type: {txt}",);
268 Ok(DydxWsMessage::Raw(val))
269 } else {
270 Ok(DydxWsMessage::Raw(val))
271 };
272
273 match result {
274 Ok(dydx_msg) => self.handle_dydx_message(dydx_msg).await,
275 Err(e) => {
276 log::error!(
277 "Failed to parse WebSocket message: {e}. Message type: {:?}, Channel: {:?}. Raw: {txt}",
278 meta.msg_type,
279 meta.channel,
280 );
281 vec![]
282 }
283 }
284 }
285 Err(e) => {
286 log::error!(
287 "Failed to parse WebSocket message envelope (DydxWsGenericMsg): {e}\nRaw JSON:\n{txt}"
288 );
289 vec![]
290 }
291 },
292 Err(e) => {
293 let err = DydxWebSocketError::from_message(e.to_string());
294 vec![DydxWsOutputMessage::Error(err)]
295 }
296 }
297 }
298 Message::Pong(_data) => vec![],
299 Message::Ping(_data) => vec![],
300 Message::Binary(_bin) => vec![],
301 Message::Close(_frame) => {
302 log::debug!("WebSocket close frame received");
303 vec![]
304 }
305 Message::Frame(_) => vec![],
306 }
307 }
308
309 async fn handle_dydx_message(&mut self, msg: DydxWsMessage) -> Vec<DydxWsOutputMessage> {
310 match self.handle_message(msg).await {
311 Ok(msgs) => msgs,
312 Err(e) => {
313 log::error!("Error handling message: {e}");
314 vec![]
315 }
316 }
317 }
318
319 fn handle_feed_message(&mut self, feed_msg: DydxWsFeedMessage) -> Vec<DydxWsOutputMessage> {
320 log::trace!(
321 "Handling feed message: {:?}",
322 std::mem::discriminant(&feed_msg)
323 );
324
325 match feed_msg {
326 DydxWsFeedMessage::Subaccounts(msg) => self.handle_subaccounts(msg),
327 DydxWsFeedMessage::Orderbook(msg) => self.handle_orderbook(msg),
328 DydxWsFeedMessage::Trades(msg) => self.handle_trades(msg),
329 DydxWsFeedMessage::Markets(msg) => self.handle_markets_feed(msg),
330 DydxWsFeedMessage::Candles(msg) => self.handle_candles_feed(msg),
331 DydxWsFeedMessage::ParentSubaccounts(msg) => self.handle_parent_subaccounts(msg),
332 DydxWsFeedMessage::BlockHeight(msg) => self.handle_block_height_feed(msg),
333 }
334 }
335
336 fn handle_subaccounts(&self, msg: DydxWsSubaccountsMessage) -> Vec<DydxWsOutputMessage> {
337 match msg {
338 DydxWsSubaccountsMessage::Subscribed(data) => {
339 let topic =
340 self.topic_from_msg(&DydxWsChannel::Subaccounts, &Some(data.id.clone()));
341 self.subscriptions.confirm_subscribe(&topic);
342 log::debug!("Forwarding subaccount subscription to execution client");
343 vec![DydxWsOutputMessage::SubaccountSubscribed(Box::new(data))]
344 }
345 DydxWsSubaccountsMessage::ChannelData(data) => {
346 let has_orders = data.contents.orders.as_ref().is_some_and(|o| !o.is_empty());
347 let has_fills = data.contents.fills.as_ref().is_some_and(|f| !f.is_empty());
348
349 if has_orders || has_fills {
350 log::debug!(
351 "Received {} order(s), {} fill(s) - forwarding to execution client",
352 data.contents.orders.as_ref().map_or(0, |o| o.len()),
353 data.contents.fills.as_ref().map_or(0, |f| f.len())
354 );
355 vec![DydxWsOutputMessage::SubaccountsChannelData(Box::new(data))]
356 } else {
357 vec![]
358 }
359 }
360 DydxWsSubaccountsMessage::Unsubscribed(data) => {
361 let topic = self.topic_from_msg(&DydxWsChannel::Subaccounts, &data.id);
362 self.subscriptions.confirm_unsubscribe(&topic);
363 vec![]
364 }
365 }
366 }
367
368 fn handle_orderbook(&mut self, msg: DydxWsOrderbookMessage) -> Vec<DydxWsOutputMessage> {
369 match msg {
370 DydxWsOrderbookMessage::Subscribed(data) => {
371 let topic = self.topic_from_msg(&DydxWsChannel::Orderbook, &data.id);
372 self.subscriptions.confirm_subscribe(&topic);
373
374 if let Some(id) = &data.id {
375 self.book_sequence.insert(id.clone(), data.message_id);
376 }
377
378 self.deserialize_orderbook_snapshot(&data)
379 }
380 DydxWsOrderbookMessage::ChannelData(data) => {
381 if let Some(id) = &data.id {
382 if let Some(last_id) = self.book_sequence.get(id)
383 && data.message_id <= *last_id
384 {
385 log::warn!(
386 "Orderbook sequence regression for {id}: last {last_id}, received {}",
387 data.message_id
388 );
389 }
390 self.book_sequence.insert(id.clone(), data.message_id);
391 }
392 self.deserialize_orderbook_update(&data)
393 }
394 DydxWsOrderbookMessage::ChannelBatchData(data) => {
395 if let Some(id) = &data.id {
396 if let Some(last_id) = self.book_sequence.get(id)
397 && data.message_id <= *last_id
398 {
399 log::warn!(
400 "Orderbook batch sequence regression for {id}: last {last_id}, received {}",
401 data.message_id
402 );
403 }
404 self.book_sequence.insert(id.clone(), data.message_id);
405 }
406 self.deserialize_orderbook_batch(&data)
407 }
408 DydxWsOrderbookMessage::Unsubscribed(data) => {
409 let topic = self.topic_from_msg(&DydxWsChannel::Orderbook, &data.id);
410 self.subscriptions.confirm_unsubscribe(&topic);
411
412 if let Some(id) = &data.id {
413 self.book_sequence.remove(id);
414 }
415 vec![]
416 }
417 }
418 }
419
420 fn handle_trades(&self, msg: DydxWsTradesMessage) -> Vec<DydxWsOutputMessage> {
421 match msg {
422 DydxWsTradesMessage::Subscribed(data) => {
423 let topic = self.topic_from_msg(&DydxWsChannel::Trades, &data.id);
424 self.subscriptions.confirm_subscribe(&topic);
425 self.deserialize_trades(&data)
426 }
427 DydxWsTradesMessage::ChannelData(data) => self.deserialize_trades(&data),
428 DydxWsTradesMessage::Unsubscribed(data) => {
429 let topic = self.topic_from_msg(&DydxWsChannel::Trades, &data.id);
430 self.subscriptions.confirm_unsubscribe(&topic);
431 vec![]
432 }
433 }
434 }
435
436 fn handle_markets_feed(&self, msg: DydxWsMarketsMessage) -> Vec<DydxWsOutputMessage> {
437 match msg {
438 DydxWsMarketsMessage::Subscribed(data) => {
439 let topic = self.topic_from_msg(&DydxWsChannel::Markets, &data.id);
440 self.subscriptions.confirm_subscribe(&topic);
441 self.deserialize_markets(&data)
442 }
443 DydxWsMarketsMessage::ChannelData(data) => self.deserialize_markets(&data),
444 DydxWsMarketsMessage::Unsubscribed(data) => {
445 let topic = self.topic_from_msg(&DydxWsChannel::Markets, &data.id);
446 self.subscriptions.confirm_unsubscribe(&topic);
447 vec![]
448 }
449 }
450 }
451
452 fn handle_candles_feed(&self, msg: DydxWsCandlesMessage) -> Vec<DydxWsOutputMessage> {
453 match msg {
454 DydxWsCandlesMessage::Subscribed(data) => {
455 let topic = self.topic_from_msg(&DydxWsChannel::Candles, &data.id);
456 self.subscriptions.confirm_subscribe(&topic);
457 vec![]
458 }
459 DydxWsCandlesMessage::ChannelData(data) => self.deserialize_candles(&data),
460 DydxWsCandlesMessage::Unsubscribed(data) => {
461 let topic = self.topic_from_msg(&DydxWsChannel::Candles, &data.id);
462 self.subscriptions.confirm_unsubscribe(&topic);
463 vec![]
464 }
465 }
466 }
467
468 fn handle_parent_subaccounts(
469 &self,
470 msg: DydxWsParentSubaccountsMessage,
471 ) -> Vec<DydxWsOutputMessage> {
472 match msg {
473 DydxWsParentSubaccountsMessage::Subscribed(data) => {
474 let topic = self.topic_from_msg(&DydxWsChannel::ParentSubaccounts, &data.id);
475 self.subscriptions.confirm_subscribe(&topic);
476 self.deserialize_parent_subaccounts(&data)
477 }
478 DydxWsParentSubaccountsMessage::ChannelData(data) => {
479 self.deserialize_parent_subaccounts(&data)
480 }
481 DydxWsParentSubaccountsMessage::Unsubscribed(data) => {
482 let topic = self.topic_from_msg(&DydxWsChannel::ParentSubaccounts, &data.id);
483 self.subscriptions.confirm_unsubscribe(&topic);
484 vec![]
485 }
486 }
487 }
488
489 fn handle_block_height_feed(&self, msg: DydxWsBlockHeightMessage) -> Vec<DydxWsOutputMessage> {
490 match msg {
491 DydxWsBlockHeightMessage::Subscribed(data) => {
492 let topic =
493 self.topic_from_msg(&DydxWsChannel::BlockHeight, &Some(data.id.clone()));
494 self.subscriptions.confirm_subscribe(&topic);
495
496 match data.contents.height.parse::<u64>() {
497 Ok(height) => vec![DydxWsOutputMessage::BlockHeight {
498 height,
499 time: data.contents.time,
500 }],
501 Err(e) => {
502 log::warn!("Failed to parse block height from subscription: {e}");
503 vec![]
504 }
505 }
506 }
507 DydxWsBlockHeightMessage::ChannelData(data) => {
508 match data.contents.block_height.parse::<u64>() {
509 Ok(height) => vec![DydxWsOutputMessage::BlockHeight {
510 height,
511 time: data.contents.time,
512 }],
513 Err(e) => {
514 log::warn!("Failed to parse block height from channel data: {e}");
515 vec![]
516 }
517 }
518 }
519 DydxWsBlockHeightMessage::Unsubscribed(data) => {
520 let topic = self.topic_from_msg(&DydxWsChannel::BlockHeight, &data.id);
521 self.subscriptions.confirm_unsubscribe(&topic);
522 vec![]
523 }
524 }
525 }
526
527 fn deserialize_trades(&self, data: &DydxWsChannelDataMsg) -> Vec<DydxWsOutputMessage> {
528 let Some(id) = data.id.clone() else {
529 log::error!("Missing id for trades channel");
530 return vec![];
531 };
532
533 match serde_json::from_value::<DydxTradeContents>(data.contents.clone()) {
534 Ok(contents) => vec![DydxWsOutputMessage::Trades { id, contents }],
535 Err(e) => {
536 log::error!("Failed to deserialize trade contents: {e}");
537 vec![]
538 }
539 }
540 }
541
542 fn deserialize_orderbook_snapshot(
543 &self,
544 data: &DydxWsChannelDataMsg,
545 ) -> Vec<DydxWsOutputMessage> {
546 let Some(id) = data.id.clone() else {
547 log::error!("Missing id for orderbook snapshot");
548 return vec![];
549 };
550
551 match serde_json::from_value::<DydxOrderbookSnapshotContents>(data.contents.clone()) {
552 Ok(contents) => vec![DydxWsOutputMessage::OrderbookSnapshot { id, contents }],
553 Err(e) => {
554 log::error!("Failed to deserialize orderbook snapshot: {e}");
555 vec![]
556 }
557 }
558 }
559
560 fn deserialize_orderbook_update(
561 &self,
562 data: &DydxWsChannelDataMsg,
563 ) -> Vec<DydxWsOutputMessage> {
564 let Some(id) = data.id.clone() else {
565 log::error!("Missing id for orderbook update");
566 return vec![];
567 };
568
569 match serde_json::from_value::<DydxOrderbookContents>(data.contents.clone()) {
570 Ok(contents) => vec![DydxWsOutputMessage::OrderbookUpdate { id, contents }],
571 Err(e) => {
572 log::error!("Failed to deserialize orderbook contents: {e}");
573 vec![]
574 }
575 }
576 }
577
578 fn deserialize_orderbook_batch(
579 &self,
580 data: &DydxWsChannelBatchDataMsg,
581 ) -> Vec<DydxWsOutputMessage> {
582 let Some(id) = data.id.clone() else {
583 log::error!("Missing id for orderbook batch");
584 return vec![];
585 };
586
587 match serde_json::from_value::<Vec<DydxOrderbookContents>>(data.contents.clone()) {
588 Ok(updates) => vec![DydxWsOutputMessage::OrderbookBatch { id, updates }],
589 Err(e) => {
590 log::error!("Failed to deserialize orderbook batch: {e}");
591 vec![]
592 }
593 }
594 }
595
596 fn deserialize_candles(&self, data: &DydxWsChannelDataMsg) -> Vec<DydxWsOutputMessage> {
597 let Some(id) = data.id.clone() else {
598 log::error!("Missing id for candles channel");
599 return vec![];
600 };
601
602 match serde_json::from_value::<DydxCandle>(data.contents.clone()) {
603 Ok(contents) => vec![DydxWsOutputMessage::Candles { id, contents }],
604 Err(e) => {
605 log::error!("Failed to deserialize candle contents: {e}");
606 vec![]
607 }
608 }
609 }
610
611 fn deserialize_markets(&self, data: &DydxWsChannelDataMsg) -> Vec<DydxWsOutputMessage> {
612 match serde_json::from_value::<DydxMarketsContents>(data.contents.clone()) {
613 Ok(contents) => vec![DydxWsOutputMessage::Markets(contents)],
614 Err(e) => {
615 log::error!("Failed to deserialize markets contents: {e}");
616 vec![]
617 }
618 }
619 }
620
621 fn deserialize_parent_subaccounts(
622 &self,
623 data: &DydxWsChannelDataMsg,
624 ) -> Vec<DydxWsOutputMessage> {
625 match serde_json::from_value::<DydxWsSubaccountsChannelContents>(data.contents.clone()) {
626 Ok(contents) => {
627 let has_orders = contents.orders.as_ref().is_some_and(|o| !o.is_empty());
628 let has_fills = contents.fills.as_ref().is_some_and(|f| !f.is_empty());
629
630 if has_orders || has_fills {
631 let channel_data = DydxWsSubaccountsChannelData {
632 connection_id: data.connection_id.clone(),
633 message_id: data.message_id,
634 id: data.id.clone().unwrap_or_default(),
635 version: data.version.clone().unwrap_or_default(),
636 contents,
637 };
638 vec![DydxWsOutputMessage::SubaccountsChannelData(Box::new(
639 channel_data,
640 ))]
641 } else {
642 vec![]
643 }
644 }
645 Err(e) => {
646 log::error!("Failed to deserialize parent subaccounts contents: {e}");
647 vec![]
648 }
649 }
650 }
651
652 async fn handle_command(&mut self, command: HandlerCommand) -> bool {
653 match command {
654 HandlerCommand::RegisterSubscription {
655 topic,
656 subscription,
657 } => {
658 self.subscription_messages.insert(topic, subscription);
659 }
660 HandlerCommand::UnregisterSubscription { topic } => {
661 self.subscription_messages.remove(&topic);
662 }
663 HandlerCommand::SendText(text) => {
664 if let Err(e) = self
665 .send_with_retry(text, Some(DYDX_RATE_LIMIT_KEY_SUBSCRIPTION.as_slice()))
666 .await
667 {
668 log::error!("Failed to send WebSocket text after retries: {e}");
669 }
670 }
671 HandlerCommand::Disconnect => {
672 log::debug!("Disconnect command received");
673 self.client.disconnect().await;
674 return true;
675 }
676 }
677 false
678 }
679
680 fn topic_from_msg(&self, channel: &DydxWsChannel, id: &Option<String>) -> String {
681 match id {
682 Some(id) => format!(
683 "{}{}{}",
684 channel.as_ref(),
685 self.subscriptions.delimiter(),
686 id
687 ),
688 None => channel.as_ref().to_string(),
689 }
690 }
691
692 fn clear_state(&mut self) {
693 let buffer_count = self.message_buffer.len();
694 let seq_count = self.book_sequence.len();
695 self.message_buffer.clear();
696 self.book_sequence.clear();
697 log::debug!(
698 "Cleared reconnect state: message_buffer={buffer_count}, book_sequence={seq_count}"
699 );
700 }
701
702 async fn replay_subscriptions(&self) -> DydxWsResult<()> {
703 let topics = self.subscriptions.all_topics();
704 for topic in topics {
705 let Some(subscription) = self.subscription_messages.get(&topic).cloned() else {
706 log::warn!("No preserved subscription message for topic: {topic}");
707 continue;
708 };
709
710 let payload = serde_json::to_string(&subscription)?;
711 self.subscriptions.mark_subscribe(&topic);
712
713 if let Err(e) = self
714 .send_with_retry(payload, Some(DYDX_RATE_LIMIT_KEY_SUBSCRIPTION.as_slice()))
715 .await
716 {
717 self.subscriptions.mark_failure(&topic);
718 return Err(e);
719 }
720 }
721
722 Ok(())
723 }
724
725 pub async fn handle_message(
733 &mut self,
734 msg: DydxWsMessage,
735 ) -> DydxWsResult<Vec<DydxWsOutputMessage>> {
736 match msg {
737 DydxWsMessage::Connected(_) => {
738 log::debug!("dYdX WebSocket connected");
739 Ok(vec![])
740 }
741 DydxWsMessage::Subscribed(sub) => {
742 log::debug!("Subscribed to {} (id: {:?})", sub.channel, sub.id);
743 let topic = self.topic_from_msg(&sub.channel, &sub.id);
744 self.subscriptions.confirm_subscribe(&topic);
745 Ok(vec![])
746 }
747 DydxWsMessage::SubaccountsSubscribed(msg) => {
748 log::debug!("Subaccounts subscribed with initial state (fallback path)");
749 let topic = self.topic_from_msg(&DydxWsChannel::Subaccounts, &Some(msg.id.clone()));
750 self.subscriptions.confirm_subscribe(&topic);
751 Ok(vec![DydxWsOutputMessage::SubaccountSubscribed(Box::new(
752 msg,
753 ))])
754 }
755 DydxWsMessage::Unsubscribed(unsub) => {
756 log::debug!("Unsubscribed from {} (id: {:?})", unsub.channel, unsub.id);
757 let topic = self.topic_from_msg(&unsub.channel, &unsub.id);
758 self.subscriptions.confirm_unsubscribe(&topic);
759 Ok(vec![])
760 }
761 DydxWsMessage::Error(err) => Ok(vec![DydxWsOutputMessage::Error(err)]),
762 DydxWsMessage::Reconnected => {
763 self.clear_state();
764
765 if let Err(e) = self.replay_subscriptions().await {
766 log::error!("Failed to replay subscriptions after reconnect message: {e}");
767 }
768 let topics = self.subscriptions.all_topics();
769 Ok(vec![DydxWsOutputMessage::Reconnected { topics }])
770 }
771 DydxWsMessage::Pong => Ok(vec![]),
772 DydxWsMessage::Raw(_) => Ok(vec![]),
773 }
774 }
775}
776
777fn should_retry_dydx_error(error: &DydxWsError) -> bool {
779 match error {
780 DydxWsError::Transport(_) => true,
781 DydxWsError::Send(_) => true,
782 DydxWsError::ClientError(msg) => {
783 let msg_lower = msg.to_lowercase();
784 msg_lower.contains("timeout")
785 || msg_lower.contains("timed out")
786 || msg_lower.contains("connection")
787 || msg_lower.contains("network")
788 }
789 DydxWsError::NotConnected
790 | DydxWsError::Json(_)
791 | DydxWsError::Parse(_)
792 | DydxWsError::Authentication(_)
793 | DydxWsError::Subscription(_)
794 | DydxWsError::Venue(_) => false,
795 }
796}
797
798fn create_dydx_timeout_error(msg: String) -> DydxWsError {
800 DydxWsError::ClientError(msg)
801}