1use std::{
19 collections::VecDeque,
20 sync::{
21 Arc,
22 atomic::{AtomicBool, Ordering},
23 },
24};
25
26use ahash::AHashMap;
27use nautilus_live::book::snapshot::SnapshotGate;
28use nautilus_network::websocket::{SubscriptionState, WebSocketClient};
29use tokio_tungstenite::tungstenite::Message;
30use tokio_util::sync::CancellationToken;
31use ustr::Ustr;
32
33use super::{AxMdSubscriptionSpec, AxWsClientError};
34use crate::{
35 common::enums::{AxCandleWidth, AxMdRequestType},
36 websocket::{
37 messages::{
38 AxDataWsMessage, AxMdMessage, AxMdSubscribe, AxMdSubscribeCandles, AxMdUnsubscribe,
39 AxMdUnsubscribeCandles,
40 },
41 parse::parse_md_message,
42 },
43};
44
45#[derive(Debug)]
47pub enum HandlerCommand {
48 SetClient(WebSocketClient),
50 Disconnect,
52 ReplaySubscriptions,
54 Subscribe {
56 request_id: i64,
58 symbol: Ustr,
60 spec: AxMdSubscriptionSpec,
62 },
63 WriteBook {
70 request_id: i64,
72 unsubscribe_request_id: Option<i64>,
74 symbol: Ustr,
76 spec: AxMdSubscriptionSpec,
78 cancel: CancellationToken,
80 gate: SnapshotGate,
82 completion: tokio::sync::oneshot::Sender<Result<(), AxWsClientError>>,
84 },
85 Unsubscribe {
87 request_id: i64,
89 symbol: Ustr,
91 topic: String,
93 },
94 SubscribeCandles {
96 request_id: i64,
98 symbol: Ustr,
100 width: AxCandleWidth,
102 },
103 UnsubscribeCandles {
105 request_id: i64,
107 symbol: Ustr,
109 width: AxCandleWidth,
111 topic: String,
113 },
114}
115
116pub(crate) struct AxMdWsFeedHandler {
121 signal: Arc<AtomicBool>,
122 inner: Option<WebSocketClient>,
123 cmd_rx: tokio::sync::mpsc::UnboundedReceiver<HandlerCommand>,
124 raw_rx: tokio::sync::mpsc::UnboundedReceiver<Message>,
125 subscriptions: SubscriptionState,
126 message_queue: VecDeque<AxDataWsMessage>,
127 replay_request_id: i64,
128 needs_subscription_replay: bool,
129 pending_subscription_requests: AHashMap<i64, PendingSubscriptionRequest>,
130}
131
132impl AxMdWsFeedHandler {
133 #[must_use]
135 pub(crate) fn new(
136 signal: Arc<AtomicBool>,
137 cmd_rx: tokio::sync::mpsc::UnboundedReceiver<HandlerCommand>,
138 raw_rx: tokio::sync::mpsc::UnboundedReceiver<Message>,
139 subscriptions: SubscriptionState,
140 ) -> Self {
141 Self {
142 signal,
143 inner: None,
144 cmd_rx,
145 raw_rx,
146 subscriptions,
147 message_queue: VecDeque::new(),
148 replay_request_id: -1,
149 needs_subscription_replay: false,
150 pending_subscription_requests: AHashMap::new(),
151 }
152 }
153
154 fn next_replay_request_id(&mut self) -> i64 {
155 self.replay_request_id -= 1;
156 self.replay_request_id
157 }
158
159 async fn replay_subscriptions(&mut self) {
160 self.pending_subscription_requests.clear();
161 let topics = self.subscriptions.reset_after_reconnect();
162 if topics.is_empty() {
163 log::debug!("No subscriptions to replay after reconnect");
164 return;
165 }
166
167 log::debug!("Replaying {} subscriptions after reconnect", topics.len());
168
169 for topic in topics {
170 if let Some(rest) = topic.strip_prefix("candles:") {
172 if let Some((symbol, width_str)) = rest.rsplit_once(':') {
173 if let Some(width) = Self::parse_candle_width(width_str) {
174 let request_id = self.next_replay_request_id();
175 log::debug!(
176 "Replaying candle subscription: symbol={symbol}, width={width:?}"
177 );
178 self.pending_subscription_requests.insert(
179 request_id,
180 PendingSubscriptionRequest::Subscribe(topic.clone()),
181 );
182 self.send_subscribe_candles(request_id, Ustr::from(symbol), width)
183 .await;
184 } else {
185 log::warn!("Failed to parse candle width from topic: {topic}");
186 }
187 } else {
188 log::warn!("Invalid candle topic format: {topic}");
189 }
190 } else if let Some((symbol, spec)) = AxMdSubscriptionSpec::parse_topic(&topic) {
191 let request_id = self.next_replay_request_id();
192 log::debug!("Replaying market data subscription: symbol={symbol}, spec={spec:?}");
193 self.pending_subscription_requests
194 .insert(request_id, PendingSubscriptionRequest::Subscribe(topic));
195 self.send_subscribe(request_id, symbol, spec).await;
196 } else {
197 log::warn!("Failed to parse market data subscription topic: {topic}");
198 }
199 }
200
201 log::debug!("Subscription replay completed");
202 }
203
204 fn parse_candle_width(s: &str) -> Option<AxCandleWidth> {
205 match s {
206 "Seconds1" => Some(AxCandleWidth::Seconds1),
207 "Seconds5" => Some(AxCandleWidth::Seconds5),
208 "Minutes1" => Some(AxCandleWidth::Minutes1),
209 "Minutes5" => Some(AxCandleWidth::Minutes5),
210 "Minutes15" => Some(AxCandleWidth::Minutes15),
211 "Hours1" => Some(AxCandleWidth::Hours1),
212 "Days1" => Some(AxCandleWidth::Days1),
213 _ => None,
214 }
215 }
216
217 pub(crate) async fn next(&mut self) -> Option<AxDataWsMessage> {
221 loop {
222 if self.needs_subscription_replay && self.message_queue.is_empty() {
223 self.needs_subscription_replay = false;
224 self.replay_subscriptions().await;
225 }
226
227 if let Some(msg) = self.message_queue.pop_front() {
228 return Some(msg);
229 }
230
231 tokio::select! {
232 Some(cmd) = self.cmd_rx.recv() => {
233 self.handle_command(cmd).await;
234 }
235
236 () = tokio::time::sleep(std::time::Duration::from_millis(100)) => {
237 if self.signal.load(Ordering::Acquire) {
238 log::debug!("Stop signal received during idle period");
239 return None;
240 }
241 }
242
243 msg = self.raw_rx.recv() => {
244 let msg = match msg {
245 Some(msg) => msg,
246 None => {
247 log::debug!("WebSocket stream closed");
248 return None;
249 }
250 };
251
252 if let Message::Ping(data) = &msg {
253 log::trace!("Received ping frame with {} bytes", data.len());
254
255 if let Some(client) = &self.inner
256 && let Err(e) = client.send_pong(data.to_vec()).await
257 {
258 log::warn!("Failed to send pong frame: {e}");
259 }
260 continue;
261 }
262
263 if let Some(message) = self.parse_raw_message(msg) {
264 self.message_queue.push_back(message);
265 }
266
267 if self.signal.load(Ordering::Acquire) {
268 log::debug!("Stop signal received");
269 return None;
270 }
271 }
272 }
273 }
274 }
275
276 async fn handle_command(&mut self, cmd: HandlerCommand) {
277 match cmd {
278 HandlerCommand::SetClient(client) => {
279 log::debug!("WebSocketClient received by handler");
280 self.inner = Some(client);
281 }
282 HandlerCommand::Disconnect => {
283 log::debug!("Disconnect command received");
284
285 if let Some(inner) = self.inner.take() {
286 inner.disconnect().await;
287 }
288 }
289 HandlerCommand::ReplaySubscriptions => {
290 log::debug!("ReplaySubscriptions command received");
291 self.replay_subscriptions().await;
292 }
293 HandlerCommand::Subscribe {
294 request_id,
295 symbol,
296 spec,
297 } => {
298 log::debug!(
299 "Subscribe command received: request_id={request_id}, symbol={symbol}, spec={spec:?}"
300 );
301 let topic = spec.topic(symbol.as_str());
302 self.pending_subscription_requests
303 .insert(request_id, PendingSubscriptionRequest::Subscribe(topic));
304 self.send_subscribe(request_id, symbol, spec).await;
305 }
306 HandlerCommand::WriteBook {
307 request_id,
308 unsubscribe_request_id,
309 symbol,
310 spec,
311 cancel,
312 gate,
313 completion,
314 } => {
315 log::debug!(
316 "WriteBook command received: request_id={request_id}, symbol={symbol}, spec={spec:?}"
317 );
318 let result = self
319 .write_book(request_id, unsubscribe_request_id, symbol, spec)
320 .await;
321
322 if result.is_ok() && !cancel.is_cancelled() {
323 gate.open();
324 }
325
326 let _ = completion.send(result);
327 }
328 HandlerCommand::Unsubscribe {
329 request_id,
330 symbol,
331 topic,
332 } => {
333 log::debug!(
334 "Unsubscribe command received: request_id={request_id}, symbol={symbol}"
335 );
336 self.pending_subscription_requests
337 .insert(request_id, PendingSubscriptionRequest::Unsubscribe(topic));
338 self.send_unsubscribe(request_id, symbol).await;
339 }
340 HandlerCommand::SubscribeCandles {
341 request_id,
342 symbol,
343 width,
344 } => {
345 log::debug!(
346 "SubscribeCandles command received: request_id={request_id}, symbol={symbol}, width={width:?}"
347 );
348 let topic = format!("candles:{symbol}:{width:?}");
349 self.pending_subscription_requests
350 .insert(request_id, PendingSubscriptionRequest::Subscribe(topic));
351 self.send_subscribe_candles(request_id, symbol, width).await;
352 }
353 HandlerCommand::UnsubscribeCandles {
354 request_id,
355 symbol,
356 width,
357 topic,
358 } => {
359 log::debug!(
360 "UnsubscribeCandles command received: request_id={request_id}, symbol={symbol}, width={width:?}"
361 );
362 self.pending_subscription_requests
363 .insert(request_id, PendingSubscriptionRequest::Unsubscribe(topic));
364 self.message_queue
365 .push_back(AxDataWsMessage::CandleUnsubscribed { symbol, width });
366 self.send_unsubscribe_candles(request_id, symbol, width)
367 .await;
368 }
369 }
370 }
371
372 async fn send_subscribe(&mut self, request_id: i64, symbol: Ustr, spec: AxMdSubscriptionSpec) {
373 let msg = AxMdSubscribe {
374 rid: request_id,
375 msg_type: AxMdRequestType::Subscribe,
376 symbol,
377 level: spec.level,
378 trades: spec.trades,
379 ticker: spec.ticker,
380 };
381
382 if let Err(e) = self.send_json(&msg).await {
383 self.pending_subscription_requests.remove(&request_id);
384 log::error!("Failed to send subscribe message: {e}");
385 }
386 }
387
388 async fn write_book(
389 &mut self,
390 request_id: i64,
391 unsubscribe_request_id: Option<i64>,
392 symbol: Ustr,
393 spec: AxMdSubscriptionSpec,
394 ) -> Result<(), AxWsClientError> {
395 let Some(client) = self.inner.as_ref() else {
396 return Err(AxWsClientError::Transport(
397 "No WebSocket client available".to_string(),
398 ));
399 };
400
401 let epoch = client.connection_epoch();
402 let topic = spec.topic(symbol.as_str());
403
404 if let Some(rid) = unsubscribe_request_id {
405 self.subscriptions.mark_failure(&topic);
407
408 let msg = AxMdUnsubscribe {
409 rid,
410 msg_type: AxMdRequestType::Unsubscribe,
411 symbol,
412 };
413
414 send_json_on_connection(client, &msg, epoch).await?;
415 }
416
417 let msg = AxMdSubscribe {
418 rid: request_id,
419 msg_type: AxMdRequestType::Subscribe,
420 symbol,
421 level: spec.level,
422 trades: spec.trades,
423 ticker: spec.ticker,
424 };
425
426 self.pending_subscription_requests
427 .insert(request_id, PendingSubscriptionRequest::Subscribe(topic));
428
429 if let Err(e) = send_json_on_connection(client, &msg, epoch).await {
430 self.pending_subscription_requests.remove(&request_id);
431 return Err(e);
432 }
433
434 Ok(())
435 }
436
437 async fn send_unsubscribe(&mut self, request_id: i64, symbol: Ustr) {
438 let msg = AxMdUnsubscribe {
439 rid: request_id,
440 msg_type: AxMdRequestType::Unsubscribe,
441 symbol,
442 };
443
444 if let Err(e) = self.send_json(&msg).await {
445 self.pending_subscription_requests.remove(&request_id);
446 log::error!("Failed to send unsubscribe message: {e}");
447 }
448 }
449
450 async fn send_subscribe_candles(
451 &mut self,
452 request_id: i64,
453 symbol: Ustr,
454 width: AxCandleWidth,
455 ) {
456 let msg = AxMdSubscribeCandles {
457 rid: request_id,
458 msg_type: AxMdRequestType::SubscribeCandles,
459 symbol,
460 width,
461 };
462
463 if let Err(e) = self.send_json(&msg).await {
464 self.pending_subscription_requests.remove(&request_id);
465 log::error!("Failed to send subscribe_candles message: {e}");
466 }
467 }
468
469 async fn send_unsubscribe_candles(
470 &mut self,
471 request_id: i64,
472 symbol: Ustr,
473 width: AxCandleWidth,
474 ) {
475 let msg = AxMdUnsubscribeCandles {
476 rid: request_id,
477 msg_type: AxMdRequestType::UnsubscribeCandles,
478 symbol,
479 width,
480 };
481
482 if let Err(e) = self.send_json(&msg).await {
483 self.pending_subscription_requests.remove(&request_id);
484 log::error!("Failed to send unsubscribe_candles message: {e}");
485 }
486 }
487
488 async fn send_json<T: serde::Serialize>(&self, msg: &T) -> Result<(), String> {
489 let Some(inner) = &self.inner else {
490 return Err("No WebSocket client available".to_string());
491 };
492
493 let payload = serde_json::to_string(msg).map_err(|e| e.to_string())?;
494 log::trace!("Sending WebSocket payload ({} bytes)", payload.len());
495
496 inner
497 .send_text(payload, None)
498 .await
499 .map_err(|e| e.to_string())
500 }
501
502 fn parse_raw_message(&mut self, msg: Message) -> Option<AxDataWsMessage> {
503 match msg {
504 Message::Text(text) => {
505 if text == nautilus_network::RECONNECTED {
506 log::info!("Received WebSocket reconnected signal");
507 self.needs_subscription_replay = true;
508 return Some(AxDataWsMessage::Reconnected);
509 }
510
511 log::trace!("Raw websocket message: {text}");
512
513 match parse_md_message(&text) {
514 Ok(message) => self.handle_message(message),
515 Err(e) => {
516 log::error!("Failed to parse WebSocket message: {e}: {text}");
517 None
518 }
519 }
520 }
521 Message::Binary(data) => {
522 log::debug!("Received binary message with {} bytes", data.len());
523 None
524 }
525 Message::Close(_) => {
526 log::debug!("Received close message, waiting for reconnection");
527 None
528 }
529 _ => None,
530 }
531 }
532
533 fn handle_message(&mut self, message: AxMdMessage) -> Option<AxDataWsMessage> {
534 match &message {
535 AxMdMessage::Error(error) => {
536 let is_benign = error.message.contains("already subscribed")
537 || error.message.contains("not subscribed");
538
539 if let Some(rid) = error.request_id
540 && let Some(request) = self.pending_subscription_requests.remove(&rid)
541 {
542 match request {
543 PendingSubscriptionRequest::Subscribe(topic) => {
544 self.subscriptions.mark_failure(&topic);
545 }
546 PendingSubscriptionRequest::Unsubscribe(topic) => {
547 self.subscriptions.confirm_unsubscribe(&topic);
548 }
549 }
550 }
551
552 if is_benign {
553 log::warn!("Subscription state: {}", error.message);
554 } else {
555 log::error!("Received error from exchange: {}", error.message);
556 }
557 }
558 AxMdMessage::SubscriptionResponse(response) => {
559 let is_subscribe = response.result.subscribed.is_some()
560 || response.result.subscribed_candle.is_some();
561 let is_unsubscribe = response.result.unsubscribed.is_some()
562 || response.result.unsubscribed_candle.is_some();
563
564 if let Some(request) = self.pending_subscription_requests.remove(&response.rid) {
565 match request {
566 PendingSubscriptionRequest::Subscribe(topic) if is_subscribe => {
567 self.subscriptions.confirm_subscribe(&topic);
568 }
569 PendingSubscriptionRequest::Unsubscribe(topic) if is_unsubscribe => {
570 self.subscriptions.confirm_unsubscribe(&topic);
571 }
572 request => {
573 log::warn!(
574 "Unexpected subscription response for request: {request:?}, \
575 response={response:?}"
576 );
577 }
578 }
579 }
580
581 if let Some(symbol) = &response.result.subscribed {
582 log::debug!("Subscription confirmed for symbol: {symbol}");
583 } else if let Some(candle) = &response.result.subscribed_candle {
584 log::debug!("Candle subscription confirmed: {candle}");
585 } else if let Some(symbol) = &response.result.unsubscribed {
586 log::debug!("Unsubscription confirmed for symbol: {symbol}");
587 } else if let Some(candle) = &response.result.unsubscribed_candle {
588 log::debug!("Candle unsubscription confirmed: {candle}");
589 }
590 return None;
591 }
592 _ => {}
593 }
594
595 Some(AxDataWsMessage::MdMessage(message))
596 }
597}
598
599#[derive(Debug)]
600enum PendingSubscriptionRequest {
601 Subscribe(String),
602 Unsubscribe(String),
603}
604
605async fn send_json_on_connection<T: serde::Serialize>(
607 client: &WebSocketClient,
608 msg: &T,
609 epoch: u64,
610) -> Result<(), AxWsClientError> {
611 let payload =
612 serde_json::to_string(msg).map_err(|e| AxWsClientError::ClientError(e.to_string()))?;
613
614 client
615 .send_text_on_connection(payload, None, epoch)
616 .await
617 .map_err(|e| AxWsClientError::Transport(e.to_string()))
618}
619
620#[cfg(test)]
621mod tests {
622 use nautilus_network::websocket::SubscriptionState;
623 use rstest::rstest;
624
625 use super::*;
626 use crate::{
627 common::enums::AxMarketDataLevel,
628 websocket::messages::{AxMdSubscriptionResponse, AxMdSubscriptionResult, AxWsError},
629 };
630
631 const TOPIC: &str = "EURUSD-PERP:Level2:false:false";
632
633 #[rstest]
634 fn test_subscription_response_confirms_subscribe() {
635 let subscriptions = SubscriptionState::new(':');
636 subscriptions.mark_subscribe(TOPIC);
637 let mut handler = create_handler(subscriptions.clone());
638 handler
639 .pending_subscription_requests
640 .insert(1, PendingSubscriptionRequest::Subscribe(TOPIC.to_string()));
641
642 handler.handle_message(AxMdMessage::SubscriptionResponse(
643 AxMdSubscriptionResponse {
644 rid: 1,
645 result: AxMdSubscriptionResult {
646 subscribed: Some(Ustr::from("EURUSD-PERP")),
647 subscribed_candle: None,
648 unsubscribed: None,
649 unsubscribed_candle: None,
650 },
651 },
652 ));
653
654 assert_eq!(subscriptions.len(), 1);
655 assert!(subscriptions.pending_subscribe_topics().is_empty());
656 assert!(subscriptions.pending_unsubscribe_topics().is_empty());
657 }
658
659 #[rstest]
660 fn test_subscription_error_keeps_topic_pending_for_replay() {
661 let subscriptions = SubscriptionState::new(':');
662 subscriptions.mark_subscribe(TOPIC);
663 subscriptions.confirm_subscribe(TOPIC);
664 let mut handler = create_handler(subscriptions.clone());
665 handler
666 .pending_subscription_requests
667 .insert(2, PendingSubscriptionRequest::Subscribe(TOPIC.to_string()));
668
669 handler.handle_message(AxMdMessage::Error(AxWsError {
670 code: Some("400".to_string()),
671 message: "subscription failed".to_string(),
672 request_id: Some(2),
673 }));
674
675 assert_eq!(subscriptions.len(), 0);
676 assert_eq!(
677 subscriptions.pending_subscribe_topics(),
678 vec![TOPIC.to_string()]
679 );
680 assert!(subscriptions.pending_unsubscribe_topics().is_empty());
681 }
682
683 #[rstest]
684 fn test_already_subscribed_keeps_topic_pending_for_replay() {
685 let subscriptions = SubscriptionState::new(':');
686 subscriptions.mark_subscribe(TOPIC);
687 let mut handler = create_handler(subscriptions.clone());
688 handler
689 .pending_subscription_requests
690 .insert(3, PendingSubscriptionRequest::Subscribe(TOPIC.to_string()));
691
692 handler.handle_message(AxMdMessage::Error(AxWsError {
693 code: Some("400".to_string()),
694 message: "already subscribed".to_string(),
695 request_id: Some(3),
696 }));
697
698 assert_eq!(subscriptions.len(), 0);
699 assert_eq!(
700 subscriptions.pending_subscribe_topics(),
701 vec![TOPIC.to_string()]
702 );
703 assert!(subscriptions.pending_unsubscribe_topics().is_empty());
704 }
705
706 #[rstest]
707 #[case("not subscribed")]
708 #[case("subscription failed")]
709 fn test_unsubscribe_error_confirms_removal(#[case] message: &str) {
710 let subscriptions = SubscriptionState::new(':');
711 subscriptions.mark_subscribe(TOPIC);
712 subscriptions.confirm_subscribe(TOPIC);
713 subscriptions.mark_unsubscribe(TOPIC);
714 let mut handler = create_handler(subscriptions.clone());
715 handler.pending_subscription_requests.insert(
716 4,
717 PendingSubscriptionRequest::Unsubscribe(TOPIC.to_string()),
718 );
719
720 handler.handle_message(AxMdMessage::Error(AxWsError {
721 code: Some("400".to_string()),
722 message: message.to_string(),
723 request_id: Some(4),
724 }));
725
726 assert_eq!(subscriptions.len(), 0);
727 assert!(subscriptions.pending_subscribe_topics().is_empty());
728 assert!(subscriptions.pending_unsubscribe_topics().is_empty());
729 }
730
731 #[rstest]
732 fn test_subscription_response_confirms_unsubscribe() {
733 let subscriptions = SubscriptionState::new(':');
734 subscriptions.mark_subscribe(TOPIC);
735 subscriptions.confirm_subscribe(TOPIC);
736 subscriptions.mark_unsubscribe(TOPIC);
737 let mut handler = create_handler(subscriptions.clone());
738 handler.pending_subscription_requests.insert(
739 5,
740 PendingSubscriptionRequest::Unsubscribe(TOPIC.to_string()),
741 );
742
743 handler.handle_message(AxMdMessage::SubscriptionResponse(
744 AxMdSubscriptionResponse {
745 rid: 5,
746 result: AxMdSubscriptionResult {
747 subscribed: None,
748 subscribed_candle: None,
749 unsubscribed: Some(Ustr::from("EURUSD-PERP")),
750 unsubscribed_candle: None,
751 },
752 },
753 ));
754
755 assert_eq!(subscriptions.len(), 0);
756 assert!(subscriptions.pending_subscribe_topics().is_empty());
757 assert!(subscriptions.pending_unsubscribe_topics().is_empty());
758 }
759
760 #[rstest]
761 #[tokio::test]
762 async fn test_replay_clears_stale_requests_and_pending_unsubscribes() {
763 let subscriptions = SubscriptionState::new(':');
764 subscriptions.mark_subscribe(TOPIC);
765 subscriptions.confirm_subscribe(TOPIC);
766 subscriptions.mark_unsubscribe(TOPIC);
767 let mut handler = create_handler(subscriptions.clone());
768 handler.pending_subscription_requests.insert(
769 6,
770 PendingSubscriptionRequest::Unsubscribe(TOPIC.to_string()),
771 );
772
773 handler.replay_subscriptions().await;
774
775 assert_eq!(subscriptions.len(), 0);
776 assert!(subscriptions.all_topics().is_empty());
777 assert!(subscriptions.pending_subscribe_topics().is_empty());
778 assert!(subscriptions.pending_unsubscribe_topics().is_empty());
779 assert!(handler.pending_subscription_requests.is_empty());
780 }
781
782 #[rstest]
783 #[tokio::test]
784 async fn test_write_book_failure_keeps_gate_closed() {
785 let mut handler = create_handler(SubscriptionState::new(':'));
786 let gate = SnapshotGate::default();
787 gate.lock().close();
788 let (completion, receiver) = tokio::sync::oneshot::channel();
789
790 handler
792 .handle_command(HandlerCommand::WriteBook {
793 request_id: 7,
794 unsubscribe_request_id: Some(8),
795 symbol: Ustr::from("EURUSD-PERP"),
796 spec: AxMdSubscriptionSpec::new(
797 AxMarketDataLevel::Level2,
798 Some(false),
799 Some(false),
800 ),
801 cancel: CancellationToken::new(),
802 gate: gate.clone(),
803 completion,
804 })
805 .await;
806
807 let result = receiver.await.unwrap();
808
809 assert!(matches!(
810 result,
811 Err(AxWsClientError::Transport(message)) if message == "No WebSocket client available"
812 ));
813 assert!(gate.lock().is_closed());
814 assert!(handler.pending_subscription_requests.is_empty());
815 }
816
817 fn create_handler(subscriptions: SubscriptionState) -> AxMdWsFeedHandler {
818 let (_cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
819 let (_raw_tx, raw_rx) = tokio::sync::mpsc::unbounded_channel();
820
821 AxMdWsFeedHandler::new(
822 Arc::new(AtomicBool::new(false)),
823 cmd_rx,
824 raw_rx,
825 subscriptions,
826 )
827 }
828}