Skip to main content

nautilus_betfair/stream/
client.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Betfair Exchange Stream API client.
17//!
18//! Connects to the Betfair raw TLS stream (CRLF-delimited JSON), authenticates,
19//! and manages market/order subscriptions with automatic clk-based resubscription
20//! on reconnection.
21
22use std::{
23    sync::{
24        Arc, OnceLock,
25        atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering},
26    },
27    time::{Duration, Instant},
28};
29
30use bytes::Bytes;
31use nautilus_live::task::{SharedTaskSlot, TaskJoinOutcome};
32use nautilus_network::{
33    SocketState, SocketStateSink,
34    mode::ReconnectRequestOutcome,
35    socket::{
36        SocketClient, SocketConfig, SocketHeartbeat, SocketReconnectHandle, SocketReconnectReplay,
37        TcpMessageHandler, WriterCommand,
38    },
39};
40use parking_lot::{Mutex, MutexGuard};
41use tokio::sync::watch; // tokio-import-ok
42use tokio_tungstenite::tungstenite::stream::Mode;
43
44use super::{
45    config::{
46        BETFAIR_STREAM_HEARTBEAT_MAX_MS, BETFAIR_STREAM_HEARTBEAT_MIN_MS, BetfairStreamConfig,
47    },
48    error::BetfairStreamError,
49    messages::{
50        Authentication, CricketSubscription, MarketDataFilter, MarketSubscription, OrderFilter,
51        OrderSubscription, RaceSubscription, Status, StreamMarketFilter, StreamMessage,
52        stream_decode,
53    },
54};
55use crate::common::{
56    consts::{
57        BETFAIR_STREAM_SERVER_HEARTBEAT_MS, STREAM_OP_MARKET_SUBSCRIPTION,
58        STREAM_OP_ORDER_SUBSCRIPTION,
59    },
60    credential::BetfairCredential,
61    enums::{ChangeType, SegmentType, StatusErrorCode},
62};
63
64pub(crate) type StreamMessageHandler = Arc<dyn Fn(StreamMessage) + Send + Sync>;
65
66#[derive(Clone, Copy, Debug)]
67pub(crate) enum HeartbeatTimeoutSource {
68    Outbound,
69    Server,
70}
71
72const AUTH_REQUEST_ID: u64 = 1;
73const STREAM_STATUS_SUCCESS: &str = "SUCCESS";
74const STREAM_DEGRADED_STATUS: i32 = 503;
75const MARKET_SUBSCRIPTION_REPLAY_KEY: u64 = 1;
76const ORDER_SUBSCRIPTION_REPLAY_KEY: u64 = 2;
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79#[repr(u8)]
80pub enum StreamLifecycleState {
81    Disconnected,
82    Idle,
83    Pending,
84    Active,
85    Degraded,
86    Rejected,
87}
88
89impl StreamLifecycleState {
90    fn from_atomic(value: &AtomicU8) -> Self {
91        match value.load(Ordering::Acquire) {
92            1 => Self::Idle,
93            2 => Self::Pending,
94            3 => Self::Active,
95            4 => Self::Degraded,
96            5 => Self::Rejected,
97            _ => Self::Disconnected,
98        }
99    }
100}
101
102#[derive(Debug)]
103struct LifecycleState {
104    value: AtomicU8,
105    changed: watch::Sender<()>,
106}
107
108impl LifecycleState {
109    fn new(state: StreamLifecycleState) -> Self {
110        Self {
111            value: AtomicU8::new(state as u8),
112            changed: watch::channel(()).0,
113        }
114    }
115
116    fn get(&self) -> StreamLifecycleState {
117        StreamLifecycleState::from_atomic(&self.value)
118    }
119
120    fn set(&self, state: StreamLifecycleState) {
121        self.value.store(state as u8, Ordering::Release);
122        self.changed.send_replace(());
123    }
124}
125
126#[derive(Debug)]
127struct ProtocolLifecycle {
128    transport_connected: AtomicBool,
129    authenticated: LifecycleState,
130    market: LifecycleState,
131    market_was_current: AtomicBool,
132    market_requires_image: AtomicBool,
133    market_image_tainted: AtomicBool,
134    order: LifecycleState,
135    order_was_current: AtomicBool,
136    order_requires_image: AtomicBool,
137    order_image_tainted: AtomicBool,
138}
139
140impl Default for ProtocolLifecycle {
141    fn default() -> Self {
142        Self {
143            transport_connected: AtomicBool::new(false),
144            authenticated: LifecycleState::new(StreamLifecycleState::Disconnected),
145            market: LifecycleState::new(StreamLifecycleState::Idle),
146            market_was_current: AtomicBool::new(false),
147            market_requires_image: AtomicBool::new(false),
148            market_image_tainted: AtomicBool::new(false),
149            order: LifecycleState::new(StreamLifecycleState::Idle),
150            order_was_current: AtomicBool::new(false),
151            order_requires_image: AtomicBool::new(false),
152            order_image_tainted: AtomicBool::new(false),
153        }
154    }
155}
156
157impl ProtocolLifecycle {
158    fn on_transport(&self, state: SocketState, market_id: u64, order_id: u64) {
159        let connected = state == SocketState::Connected;
160        self.transport_connected.store(connected, Ordering::Release);
161        self.authenticated.set(if connected {
162            StreamLifecycleState::Pending
163        } else {
164            StreamLifecycleState::Disconnected
165        });
166        self.market
167            .set(subscription_transport_state(connected, market_id));
168        self.market_was_current.store(false, Ordering::Release);
169        self.market_image_tainted.store(false, Ordering::Release);
170        self.order
171            .set(subscription_transport_state(connected, order_id));
172        self.order_was_current.store(false, Ordering::Release);
173        self.order_image_tainted.store(false, Ordering::Release);
174    }
175
176    fn on_status(&self, status: &Status, market_id: u64, order_id: u64) {
177        let Some(id) = status.id else {
178            return;
179        };
180        let next = if status.status_code.as_deref() == Some(STREAM_STATUS_SUCCESS)
181            && status.error_code.is_none()
182        {
183            StreamLifecycleState::Active
184        } else {
185            StreamLifecycleState::Rejected
186        };
187
188        if id == AUTH_REQUEST_ID {
189            self.authenticated.set(next);
190        } else if id == market_id {
191            self.market.set(if next == StreamLifecycleState::Active {
192                StreamLifecycleState::Pending
193            } else {
194                next
195            });
196        } else if id == order_id {
197            self.order.set(if next == StreamLifecycleState::Active {
198                StreamLifecycleState::Pending
199            } else {
200                next
201            });
202        }
203    }
204
205    fn on_change(
206        state: &LifecycleState,
207        was_current: &AtomicBool,
208        requires_image: &AtomicBool,
209        status: Option<i32>,
210        change_type: Option<ChangeType>,
211        segment_type: Option<SegmentType>,
212    ) {
213        let complete = change_complete(segment_type);
214        let initial = change_type == Some(ChangeType::SubImage)
215            || (change_type == Some(ChangeType::ResubDelta)
216                && !requires_image.load(Ordering::Acquire));
217
218        if status == Some(STREAM_DEGRADED_STATUS) {
219            state.set(StreamLifecycleState::Degraded);
220            return;
221        }
222
223        let current = state.get();
224
225        if status.is_none()
226            && complete
227            && (initial
228                || (current == StreamLifecycleState::Degraded
229                    && was_current.load(Ordering::Acquire)))
230        {
231            if change_type == Some(ChangeType::SubImage) {
232                requires_image.store(false, Ordering::Release);
233            }
234            was_current.store(true, Ordering::Release);
235            state.set(StreamLifecycleState::Active);
236        }
237    }
238}
239
240const fn subscription_transport_state(connected: bool, id: u64) -> StreamLifecycleState {
241    if !connected {
242        StreamLifecycleState::Disconnected
243    } else if id == 0 {
244        StreamLifecycleState::Idle
245    } else {
246        StreamLifecycleState::Pending
247    }
248}
249
250async fn wait_for_lifecycle_state(state: &LifecycleState, expected: StreamLifecycleState) {
251    let mut changed_rx = state.changed.subscribe();
252    loop {
253        if state.get() == expected {
254            return;
255        }
256        changed_rx
257            .changed()
258            .await
259            .expect("lifecycle sender lives as long as the borrowed client");
260    }
261}
262
263/// Betfair Exchange Stream API client using raw TLS (CRLF-delimited JSON).
264///
265/// On connect, authenticates immediately. On reconnection, replays authentication
266/// and any active subscriptions with the latest `clk` token for delta resumption.
267///
268/// The auth bytes are stored in a watch channel so the caller can push refreshed
269/// session tokens via [`update_auth`](Self::update_auth) after keep-alive or HTTP
270/// reconnect. The `closed` flag distinguishes permanent shutdown from transient
271/// reconnect.
272#[derive(Debug)]
273pub struct BetfairStreamClient {
274    socket: SocketClient,
275    market_sub_tx: watch::Sender<Option<MarketSubscription>>,
276    market_clk_tx: watch::Sender<Option<String>>,
277    market_initial_clk_tx: watch::Sender<Option<String>>,
278    order_sub_tx: watch::Sender<Option<OrderSubscription>>,
279    order_clk_tx: watch::Sender<Option<String>>,
280    order_initial_clk_tx: watch::Sender<Option<String>>,
281    market_active_sub_id: Arc<AtomicU64>,
282    order_active_sub_id: Arc<AtomicU64>,
283    request_id: Arc<AtomicU64>,
284    market_state_lock: Arc<Mutex<()>>,
285    order_state_lock: Arc<Mutex<()>>,
286    auth_tx: watch::Sender<StreamAuth>,
287    reconnect_auth: Arc<ReconnectAuthState>,
288    lifecycle: Arc<ProtocolLifecycle>,
289    dead_peer_enabled: Arc<AtomicBool>,
290    dead_peer_timeout_ms: Arc<AtomicU64>,
291    dead_peer_timeout_override: bool,
292    dead_peer_task: SharedTaskSlot<()>,
293    closed: AtomicBool,
294}
295
296impl BetfairStreamClient {
297    /// Connects to the Betfair stream API and authenticates.
298    ///
299    /// # Errors
300    ///
301    /// Returns an error if the connection fails or authentication cannot be sent.
302    pub async fn connect(
303        credential: &BetfairCredential,
304        session_token: String,
305        handler: TcpMessageHandler,
306        config: BetfairStreamConfig,
307    ) -> Result<Self, BetfairStreamError> {
308        Self::connect_inner(
309            credential,
310            session_token,
311            StreamHandler::Raw(handler),
312            config,
313            HeartbeatTimeoutSource::Server,
314            None,
315        )
316        .await
317    }
318
319    /// Connects to the Betfair stream API and reports transport availability changes.
320    ///
321    /// # Errors
322    ///
323    /// Returns an error if the connection fails or authentication cannot be sent.
324    pub(crate) async fn connect_with_state_sink(
325        credential: &BetfairCredential,
326        session_token: String,
327        handler: StreamMessageHandler,
328        config: BetfairStreamConfig,
329        heartbeat_timeout_source: HeartbeatTimeoutSource,
330        state_sink: Option<SocketStateSink>,
331    ) -> Result<Self, BetfairStreamError> {
332        Self::connect_inner(
333            credential,
334            session_token,
335            StreamHandler::Decoded(handler),
336            config,
337            heartbeat_timeout_source,
338            state_sink,
339        )
340        .await
341    }
342
343    async fn connect_inner(
344        credential: &BetfairCredential,
345        session_token: String,
346        handler: StreamHandler,
347        config: BetfairStreamConfig,
348        heartbeat_timeout_source: HeartbeatTimeoutSource,
349        state_sink: Option<SocketStateSink>,
350    ) -> Result<Self, BetfairStreamError> {
351        config
352            .validate()
353            .map_err(|e| BetfairStreamError::ProtocolError(e.to_string()))?;
354        let auth = Authentication::with_id(
355            credential.app_key().to_string(),
356            session_token,
357            AUTH_REQUEST_ID,
358        );
359        let auth_bytes_vec = serde_json::to_vec(&auth)?;
360        let auth_bytes = Bytes::from(auth_bytes_vec.clone());
361        let reconnect_auth = Arc::new(ReconnectAuthState::default());
362        let (auth_tx, auth_rx) = watch::channel(StreamAuth {
363            generation: 0,
364            bytes: auth_bytes,
365        });
366        let mode = if config.use_tls {
367            Mode::Tls
368        } else {
369            Mode::Plain
370        };
371
372        let (market_clk_tx, market_clk_rx) = watch::channel(None::<String>);
373        let (market_initial_clk_tx, market_initial_clk_rx) = watch::channel(None::<String>);
374        let (order_clk_tx, order_clk_rx) = watch::channel(None::<String>);
375        let (order_initial_clk_tx, order_initial_clk_rx) = watch::channel(None::<String>);
376        let (market_sub_tx, market_sub_rx) = watch::channel(None::<MarketSubscription>);
377        let (order_sub_tx, order_sub_rx) = watch::channel(None::<OrderSubscription>);
378
379        // Clone senders for the handler; struct keeps originals to reset on re-subscribe.
380        let market_sub_tx_h = market_sub_tx.clone();
381        let order_sub_tx_h = order_sub_tx.clone();
382        let (market_clk_tx_h, market_initial_clk_tx_h) =
383            (market_clk_tx.clone(), market_initial_clk_tx.clone());
384        let (order_clk_tx_h, order_initial_clk_tx_h) =
385            (order_clk_tx.clone(), order_initial_clk_tx.clone());
386
387        let market_active_sub_id = Arc::new(AtomicU64::new(0));
388        let order_active_sub_id = Arc::new(AtomicU64::new(0));
389        let request_id = Arc::new(AtomicU64::new(AUTH_REQUEST_ID + 1));
390        let request_id_h = Arc::clone(&request_id);
391        let market_state_lock = Arc::new(Mutex::new(()));
392        let order_state_lock = Arc::new(Mutex::new(()));
393        let market_state_lock_h = Arc::clone(&market_state_lock);
394        let order_state_lock_h = Arc::clone(&order_state_lock);
395        let writer_tx_h = Arc::new(OnceLock::new());
396        let writer_tx_handler = Arc::clone(&writer_tx_h);
397        let market_active_sub_id_h = Arc::clone(&market_active_sub_id);
398        let order_active_sub_id_h = Arc::clone(&order_active_sub_id);
399        let reconnect_auth_h = Arc::clone(&reconnect_auth);
400        let lifecycle = Arc::new(ProtocolLifecycle::default());
401        let lifecycle_h = Arc::clone(&lifecycle);
402        let last_inbound = Arc::new(Mutex::new(Instant::now()));
403        let last_inbound_h = Arc::clone(&last_inbound);
404        let dead_peer_enabled = Arc::new(AtomicBool::new(false));
405        let dead_peer_timeout_ms = Arc::new(AtomicU64::new(
406            config.dead_peer_timeout_secs().saturating_mul(1_000),
407        ));
408        let dead_peer_timeout_ms_h = Arc::clone(&dead_peer_timeout_ms);
409        let timeout_override = config.heartbeat_timeout_secs.is_some();
410
411        let message_handler: TcpMessageHandler = Arc::new(move |data: &[u8]| {
412            *last_inbound_h.lock() = Instant::now();
413            let Some(msg) = handler.decode(data) else {
414                return;
415            };
416
417            match &msg {
418                StreamMessage::MarketChange(mcm) => {
419                    let _state = lock_stream_state(&market_state_lock_h);
420                    let active = market_active_sub_id_h.load(Ordering::SeqCst);
421                    let current = active == 0 || mcm.id.is_none_or(|id| id == active);
422                    if !current {
423                        return;
424                    }
425
426                    if mcm.status == Some(STREAM_DEGRADED_STATUS) {
427                        if mcm.segment_type.is_some() {
428                            lifecycle_h
429                                .market_image_tainted
430                                .store(true, Ordering::Release);
431                        }
432                        ProtocolLifecycle::on_change(
433                            &lifecycle_h.market,
434                            &lifecycle_h.market_was_current,
435                            &lifecycle_h.market_requires_image,
436                            mcm.status,
437                            mcm.ct,
438                            mcm.segment_type,
439                        );
440                        return;
441                    }
442
443                    let image_start = mcm.ct == Some(ChangeType::SubImage)
444                        && matches!(mcm.segment_type, None | Some(SegmentType::SegStart));
445                    let complete = change_complete(mcm.segment_type);
446                    if image_start && mcm.status.is_none() {
447                        lifecycle_h
448                            .market_image_tainted
449                            .store(false, Ordering::Release);
450                    } else if lifecycle_h.market_image_tainted.load(Ordering::Acquire) {
451                        if complete {
452                            reissue_market_subscription(
453                                &request_id_h,
454                                &market_active_sub_id_h,
455                                &lifecycle_h,
456                                &market_sub_tx_h,
457                                &market_clk_tx_h,
458                                &market_initial_clk_tx_h,
459                                writer_tx_handler.get(),
460                            );
461                        }
462                        return;
463                    }
464
465                    let lifecycle_state = lifecycle_h.market.get();
466                    if lifecycle_state == StreamLifecycleState::Degraded
467                        && mcm.ct != Some(ChangeType::SubImage)
468                    {
469                        if complete {
470                            reissue_market_subscription(
471                                &request_id_h,
472                                &market_active_sub_id_h,
473                                &lifecycle_h,
474                                &market_sub_tx_h,
475                                &market_clk_tx_h,
476                                &market_initial_clk_tx_h,
477                                writer_tx_handler.get(),
478                            );
479                        }
480                        return;
481                    }
482
483                    if lifecycle_h.market_requires_image.load(Ordering::Acquire)
484                        && mcm.ct == Some(ChangeType::ResubDelta)
485                    {
486                        return;
487                    }
488
489                    ProtocolLifecycle::on_change(
490                        &lifecycle_h.market,
491                        &lifecycle_h.market_was_current,
492                        &lifecycle_h.market_requires_image,
493                        mcm.status,
494                        mcm.ct,
495                        mcm.segment_type,
496                    );
497                    update_stream_state(
498                        &mcm.clk,
499                        &mcm.initial_clk,
500                        mcm.heartbeat_ms,
501                        &market_clk_tx_h,
502                        &market_initial_clk_tx_h,
503                        timeout_override,
504                        &dead_peer_timeout_ms_h,
505                    );
506                    handler.handle(data, msg);
507                }
508                StreamMessage::OrderChange(ocm) => {
509                    let _state = lock_stream_state(&order_state_lock_h);
510                    let active = order_active_sub_id_h.load(Ordering::SeqCst);
511                    let current = active == 0 || ocm.id.is_none_or(|id| id == active);
512                    if !current {
513                        return;
514                    }
515
516                    if ocm.status == Some(STREAM_DEGRADED_STATUS) {
517                        if ocm.segment_type.is_some() {
518                            lifecycle_h
519                                .order_image_tainted
520                                .store(true, Ordering::Release);
521                        }
522                        ProtocolLifecycle::on_change(
523                            &lifecycle_h.order,
524                            &lifecycle_h.order_was_current,
525                            &lifecycle_h.order_requires_image,
526                            ocm.status,
527                            ocm.ct,
528                            ocm.segment_type,
529                        );
530                        handler.handle(data, msg);
531                        return;
532                    }
533
534                    let image_start = ocm.ct == Some(ChangeType::SubImage)
535                        && matches!(ocm.segment_type, None | Some(SegmentType::SegStart));
536                    let complete = change_complete(ocm.segment_type);
537                    if image_start && ocm.status.is_none() {
538                        lifecycle_h
539                            .order_image_tainted
540                            .store(false, Ordering::Release);
541                    } else if lifecycle_h.order_image_tainted.load(Ordering::Acquire) {
542                        if complete {
543                            reissue_order_subscription(
544                                &request_id_h,
545                                &order_active_sub_id_h,
546                                &lifecycle_h,
547                                &order_sub_tx_h,
548                                &order_clk_tx_h,
549                                &order_initial_clk_tx_h,
550                                writer_tx_handler.get(),
551                            );
552                        }
553                        return;
554                    }
555
556                    let lifecycle_state = lifecycle_h.order.get();
557                    if lifecycle_state == StreamLifecycleState::Degraded
558                        && lifecycle_h.order_requires_image.load(Ordering::Acquire)
559                        && ocm.ct != Some(ChangeType::SubImage)
560                    {
561                        if complete {
562                            reissue_order_subscription(
563                                &request_id_h,
564                                &order_active_sub_id_h,
565                                &lifecycle_h,
566                                &order_sub_tx_h,
567                                &order_clk_tx_h,
568                                &order_initial_clk_tx_h,
569                                writer_tx_handler.get(),
570                            );
571                        }
572                        return;
573                    }
574
575                    if lifecycle_h.order_requires_image.load(Ordering::Acquire)
576                        && ocm.ct == Some(ChangeType::ResubDelta)
577                    {
578                        return;
579                    }
580
581                    ProtocolLifecycle::on_change(
582                        &lifecycle_h.order,
583                        &lifecycle_h.order_was_current,
584                        &lifecycle_h.order_requires_image,
585                        ocm.status,
586                        ocm.ct,
587                        ocm.segment_type,
588                    );
589                    update_stream_state(
590                        &ocm.clk,
591                        &ocm.initial_clk,
592                        ocm.heartbeat_ms,
593                        &order_clk_tx_h,
594                        &order_initial_clk_tx_h,
595                        timeout_override,
596                        &dead_peer_timeout_ms_h,
597                    );
598                    handler.handle(data, msg);
599                }
600                StreamMessage::Status(status) => {
601                    let _market_state = lock_stream_state(&market_state_lock_h);
602                    let _order_state = lock_stream_state(&order_state_lock_h);
603                    let market_id = market_active_sub_id_h.load(Ordering::Acquire);
604                    let order_id = order_active_sub_id_h.load(Ordering::Acquire);
605                    lifecycle_h.on_status(status, market_id, order_id);
606                    // Clear rejected clocks so the next reconnect requests a full image
607                    if status.error_code == Some(StatusErrorCode::InvalidClock) {
608                        if market_id > 0 && status.id == Some(market_id) {
609                            let _ = market_clk_tx_h.send(None);
610                            let _ = market_initial_clk_tx_h.send(None);
611                            lifecycle_h
612                                .market_requires_image
613                                .store(true, Ordering::Release);
614                            lifecycle_h
615                                .market_image_tainted
616                                .store(false, Ordering::Release);
617                            log::warn!(
618                                "Betfair market stream INVALID_CLOCK: clocks cleared, \
619                                 next reconnect will request a full image",
620                            );
621                        } else if order_id > 0 && status.id == Some(order_id) {
622                            let _ = order_clk_tx_h.send(None);
623                            let _ = order_initial_clk_tx_h.send(None);
624                            lifecycle_h
625                                .order_requires_image
626                                .store(true, Ordering::Release);
627                            lifecycle_h
628                                .order_image_tainted
629                                .store(false, Ordering::Release);
630                            log::warn!(
631                                "Betfair order stream INVALID_CLOCK: clocks cleared, \
632                                 next reconnect will request a full image",
633                            );
634                        }
635                    } else if status.connection_closed {
636                        log::warn!(
637                            "Betfair stream connection closed by server: {:?} - {:?}",
638                            status.error_code,
639                            status.error_message,
640                        );
641                    } else if status.error_code.is_some() {
642                        log::warn!(
643                            "Betfair stream status error: {:?} - {:?}",
644                            status.error_code,
645                            status.error_message,
646                        );
647                    }
648                    handler.handle(data, msg);
649                }
650                StreamMessage::Connection(_) => {
651                    reconnect_auth_h.request_pending();
652                    handler.handle(data, msg);
653                }
654                _ => {
655                    handler.handle(data, msg);
656                }
657            }
658        });
659
660        let auth_reconnect = auth_rx;
661        let reconnect_auth_replay = Arc::clone(&reconnect_auth);
662        let market_state_replay = Arc::clone(&market_state_lock);
663        let order_state_replay = Arc::clone(&order_state_lock);
664        let reconnect_replay: SocketReconnectReplay = Arc::new(move || {
665            let mut replay = Vec::with_capacity(3);
666            let auth = auth_reconnect.borrow().clone();
667            reconnect_auth_replay.record_replay(auth.generation);
668
669            replay.push(auth.bytes);
670
671            {
672                let _state = lock_stream_state(&market_state_replay);
673
674                if let Some(mut sub) = market_sub_rx.borrow().clone() {
675                    sub.clk = market_clk_rx.borrow().clone();
676                    sub.initial_clk = market_initial_clk_rx.borrow().clone();
677                    if let Ok(sub_bytes) = serde_json::to_vec(&sub) {
678                        replay.push(Bytes::from(sub_bytes));
679                    }
680                }
681            }
682
683            {
684                let _state = lock_stream_state(&order_state_replay);
685
686                if let Some(mut sub) = order_sub_rx.borrow().clone() {
687                    sub.clk = order_clk_rx.borrow().clone();
688                    sub.initial_clk = order_initial_clk_rx.borrow().clone();
689                    if let Ok(sub_bytes) = serde_json::to_vec(&sub) {
690                        replay.push(Bytes::from(sub_bytes));
691                    }
692                }
693            }
694
695            replay
696        });
697
698        let url = format!("{}:{}", config.host, config.port);
699        let lifecycle_sink = Arc::clone(&lifecycle);
700        let market_id_sink = Arc::clone(&market_active_sub_id);
701        let order_id_sink = Arc::clone(&order_active_sub_id);
702        let last_inbound_sink = Arc::clone(&last_inbound);
703        let market_state_sink = Arc::clone(&market_state_lock);
704        let order_state_sink = Arc::clone(&order_state_lock);
705        let lifecycle_callback = move |state| {
706            let _market_state = lock_stream_state(&market_state_sink);
707            let _order_state = lock_stream_state(&order_state_sink);
708            lifecycle_sink.on_transport(
709                state,
710                market_id_sink.load(Ordering::Acquire),
711                order_id_sink.load(Ordering::Acquire),
712            );
713            *last_inbound_sink.lock() = Instant::now();
714        };
715        let state_sink = match state_sink {
716            Some(sink) => sink.with_callback(lifecycle_callback),
717            None => SocketStateSink::new(lifecycle_callback),
718        };
719        let socket_config = SocketConfig {
720            url,
721            mode,
722            suffix: b"\r\n".to_vec(),
723            message_handler: Some(message_handler),
724            heartbeat: outbound_heartbeat(config.heartbeat_secs),
725            connect_timeout_ms: None,
726            reconnect_delay_initial_ms: Some(config.reconnect_delay_initial_ms),
727            reconnect_delay_max_ms: Some(config.reconnect_delay_max_ms),
728            reconnect_backoff_factor: None,
729            reconnect_jitter_ms: None,
730            connection_max_retries: None,
731            reconnect_max_attempts: None,
732            heartbeat_timeout_secs: heartbeat_timeout(
733                heartbeat_timeout_source,
734                config.heartbeat_secs,
735                config.heartbeat_timeout_secs,
736            ),
737            certs_dir: None,
738        };
739
740        let socket = SocketClient::builder()
741            .config(socket_config)
742            .state_sink(state_sink)
743            .reconnect_replay(reconnect_replay)
744            .connect()
745            .await
746            .map_err(|e| BetfairStreamError::ConnectionFailed(e.to_string()))?;
747        writer_tx_h
748            .set(socket.writer_tx.clone())
749            .expect("Betfair stream writer must only be initialized once");
750        reconnect_auth.set_handle(socket.reconnect_handle());
751
752        socket
753            .send_bytes(auth_bytes_vec)
754            .await
755            .map_err(|e| BetfairStreamError::ConnectionFailed(e.to_string()))?;
756
757        let dead_peer_task = SharedTaskSlot::new();
758
759        if matches!(heartbeat_timeout_source, HeartbeatTimeoutSource::Server) {
760            let reconnect = socket.reconnect_handle();
761            let enabled = Arc::clone(&dead_peer_enabled);
762            let last = Arc::clone(&last_inbound);
763            let timeout_ms = Arc::clone(&dead_peer_timeout_ms);
764
765            dead_peer_task
766                .spawn(async move {
767                    loop {
768                        tokio::time::sleep(Duration::from_millis(100)).await;
769
770                        if !enabled.load(Ordering::Acquire) {
771                            continue;
772                        }
773                        let timeout = Duration::from_millis(timeout_ms.load(Ordering::Acquire));
774                        if last.lock().elapsed() >= timeout {
775                            let _ = reconnect.request_reconnect();
776                        }
777                    }
778                })
779                .map_err(|e| {
780                    BetfairStreamError::ConnectionFailed(format!(
781                        "failed to start dead-peer monitor: {e}"
782                    ))
783                })?;
784        }
785
786        Ok(Self {
787            socket,
788            market_sub_tx,
789            market_clk_tx,
790            market_initial_clk_tx,
791            order_sub_tx,
792            order_clk_tx,
793            order_initial_clk_tx,
794            market_active_sub_id,
795            order_active_sub_id,
796            request_id,
797            market_state_lock,
798            order_state_lock,
799            auth_tx,
800            reconnect_auth,
801            lifecycle,
802            dead_peer_enabled,
803            dead_peer_timeout_ms,
804            dead_peer_timeout_override: timeout_override,
805            dead_peer_task,
806            closed: AtomicBool::new(false),
807        })
808    }
809
810    /// Subscribes to market data for the given filter and data fields.
811    ///
812    /// Stores the subscription for automatic replay on reconnection.
813    ///
814    /// # Errors
815    ///
816    /// Returns an error if serialization or sending fails.
817    pub async fn subscribe_markets(
818        &self,
819        market_filter: StreamMarketFilter,
820        data_filter: MarketDataFilter,
821        heartbeat_ms: Option<u64>,
822        conflate_ms: Option<u64>,
823    ) -> Result<(), BetfairStreamError> {
824        if self.closed.load(Ordering::SeqCst) || self.socket.is_closed() {
825            return Err(BetfairStreamError::Disconnected(
826                "stream client is closed".to_string(),
827            ));
828        }
829        let heartbeat_ms = heartbeat_ms.unwrap_or(BETFAIR_STREAM_SERVER_HEARTBEAT_MS);
830        validate_subscription_heartbeat(heartbeat_ms)?;
831        self.update_dead_peer_timeout(heartbeat_ms);
832        let _state = lock_stream_state(&self.market_state_lock);
833        let id = self.request_id.fetch_add(1, Ordering::Relaxed);
834        // Advance the active ID before clearing clocks so that any in-flight MCMs
835        // from the previous subscription are immediately rejected by the handler.
836        self.market_active_sub_id.store(id, Ordering::SeqCst);
837        self.lifecycle.market.set(StreamLifecycleState::Pending);
838        self.lifecycle
839            .market_was_current
840            .store(false, Ordering::Release);
841        self.lifecycle
842            .market_requires_image
843            .store(true, Ordering::Release);
844        self.lifecycle
845            .market_image_tainted
846            .store(false, Ordering::Release);
847        self.dead_peer_enabled.store(true, Ordering::Release);
848        let sub = MarketSubscription {
849            op: STREAM_OP_MARKET_SUBSCRIPTION.to_string(),
850            id: Some(id),
851            market_filter,
852            market_data_filter: data_filter,
853            clk: None,
854            conflate_ms,
855            heartbeat_ms: Some(heartbeat_ms),
856            initial_clk: None,
857            segmentation_enabled: Some(true),
858        };
859
860        // Reset clocks so a disconnect before the first MCM response doesn't replay
861        // stale tokens from a previous subscription with different filters.
862        let _ = self.market_clk_tx.send(None);
863        let _ = self.market_initial_clk_tx.send(None);
864        let _ = self.market_sub_tx.send(Some(sub.clone()));
865
866        let data = Bytes::from(serde_json::to_vec(&sub)?);
867        self.socket
868            .writer_tx
869            .send(WriterCommand::SendOrReplay {
870                key: MARKET_SUBSCRIPTION_REPLAY_KEY,
871                data,
872            })
873            .map_err(|e| BetfairStreamError::ConnectionFailed(e.to_string()))?;
874        Ok(())
875    }
876
877    /// Subscribes to order updates.
878    ///
879    /// Stores the subscription for automatic replay on reconnection.
880    ///
881    /// # Errors
882    ///
883    /// Returns an error if serialization or sending fails.
884    pub async fn subscribe_orders(
885        &self,
886        order_filter: Option<OrderFilter>,
887        heartbeat_ms: Option<u64>,
888    ) -> Result<(), BetfairStreamError> {
889        if self.closed.load(Ordering::SeqCst) || self.socket.is_closed() {
890            return Err(BetfairStreamError::Disconnected(
891                "stream client is closed".to_string(),
892            ));
893        }
894        let heartbeat_ms = heartbeat_ms.unwrap_or(BETFAIR_STREAM_SERVER_HEARTBEAT_MS);
895        validate_subscription_heartbeat(heartbeat_ms)?;
896        self.update_dead_peer_timeout(heartbeat_ms);
897        let _state = lock_stream_state(&self.order_state_lock);
898        let id = self.request_id.fetch_add(1, Ordering::Relaxed);
899        self.order_active_sub_id.store(id, Ordering::SeqCst);
900        self.lifecycle.order.set(StreamLifecycleState::Pending);
901        self.lifecycle
902            .order_was_current
903            .store(false, Ordering::Release);
904        self.lifecycle
905            .order_requires_image
906            .store(true, Ordering::Release);
907        self.lifecycle
908            .order_image_tainted
909            .store(false, Ordering::Release);
910        self.dead_peer_enabled.store(true, Ordering::Release);
911        let sub = OrderSubscription {
912            op: STREAM_OP_ORDER_SUBSCRIPTION.to_string(),
913            id: Some(id),
914            order_filter,
915            clk: None,
916            conflate_ms: None,
917            heartbeat_ms: Some(heartbeat_ms),
918            initial_clk: None,
919            segmentation_enabled: Some(true),
920        };
921
922        // Reset clocks so a disconnect before the first OCM response doesn't replay
923        // stale tokens from a previous subscription with different filters.
924        let _ = self.order_clk_tx.send(None);
925        let _ = self.order_initial_clk_tx.send(None);
926        let _ = self.order_sub_tx.send(Some(sub.clone()));
927
928        let data = Bytes::from(serde_json::to_vec(&sub)?);
929        self.socket
930            .writer_tx
931            .send(WriterCommand::SendOrReplay {
932                key: ORDER_SUBSCRIPTION_REPLAY_KEY,
933                data,
934            })
935            .map_err(|e| BetfairStreamError::ConnectionFailed(e.to_string()))?;
936        Ok(())
937    }
938
939    fn update_dead_peer_timeout(&self, heartbeat_ms: u64) {
940        if !self.dead_peer_timeout_override {
941            self.dead_peer_timeout_ms
942                .store(heartbeat_ms.saturating_mul(2), Ordering::Release);
943        }
944    }
945
946    /// Returns `true` if the connection is active.
947    #[must_use]
948    pub fn is_active(&self) -> bool {
949        self.socket.is_active()
950    }
951
952    #[must_use]
953    pub fn authentication_state(&self) -> StreamLifecycleState {
954        self.lifecycle.authenticated.get()
955    }
956
957    #[must_use]
958    pub fn market_subscription_state(&self) -> StreamLifecycleState {
959        self.lifecycle.market.get()
960    }
961
962    #[must_use]
963    pub fn order_subscription_state(&self) -> StreamLifecycleState {
964        self.lifecycle.order.get()
965    }
966
967    /// Waits for the authentication lifecycle component to equal `expected`.
968    ///
969    /// Returns immediately if the component is already in the exact expected state;
970    /// otherwise waits for a later transition. A transient expected state that is
971    /// replaced before this task observes it can be missed because transitions are not
972    /// recorded as history. This method has no internal timeout; callers wanting a
973    /// bound should wrap it in [`tokio::time::timeout`].
974    pub async fn wait_for_authentication_state(&self, expected: StreamLifecycleState) {
975        wait_for_lifecycle_state(&self.lifecycle.authenticated, expected).await;
976    }
977
978    /// Waits for the market subscription lifecycle component to equal `expected`.
979    ///
980    /// Returns immediately if the component is already in the exact expected state;
981    /// otherwise waits for a later transition. A transient expected state that is
982    /// replaced before this task observes it can be missed because transitions are not
983    /// recorded as history. This method has no internal timeout; callers wanting a
984    /// bound should wrap it in [`tokio::time::timeout`].
985    pub async fn wait_for_market_subscription_state(&self, expected: StreamLifecycleState) {
986        wait_for_lifecycle_state(&self.lifecycle.market, expected).await;
987    }
988
989    /// Waits for the order subscription lifecycle component to equal `expected`.
990    ///
991    /// Returns immediately if the component is already in the exact expected state;
992    /// otherwise waits for a later transition. A transient expected state that is
993    /// replaced before this task observes it can be missed because transitions are not
994    /// recorded as history. This method has no internal timeout; callers wanting a
995    /// bound should wrap it in [`tokio::time::timeout`].
996    pub async fn wait_for_order_subscription_state(&self, expected: StreamLifecycleState) {
997        wait_for_lifecycle_state(&self.lifecycle.order, expected).await;
998    }
999
1000    #[must_use]
1001    pub fn is_authenticated(&self) -> bool {
1002        self.socket.is_active() && self.authentication_state() == StreamLifecycleState::Active
1003    }
1004
1005    #[must_use]
1006    pub fn is_market_ready(&self) -> bool {
1007        self.is_authenticated() && self.market_subscription_state() == StreamLifecycleState::Active
1008    }
1009
1010    #[must_use]
1011    pub fn is_order_ready(&self) -> bool {
1012        self.is_authenticated() && self.order_subscription_state() == StreamLifecycleState::Active
1013    }
1014
1015    /// Pushes refreshed auth bytes so the next reconnection or subscription uses
1016    /// the current session token instead of the one from initial connect.
1017    pub fn update_auth(&self, app_key: &str, session_token: String) {
1018        update_auth_state(
1019            &self.auth_tx,
1020            &Authentication::with_id(app_key.to_string(), session_token, AUTH_REQUEST_ID),
1021        );
1022    }
1023
1024    /// Requests replacement of the active stream transport.
1025    ///
1026    /// Returns `true` only when this call starts a reconnect. Duplicate requests and requests after
1027    /// close return `false`.
1028    #[must_use]
1029    pub fn request_reconnect(&self) -> bool {
1030        self.request_reconnect_outcome() == ReconnectRequestOutcome::Accepted
1031    }
1032
1033    pub(crate) fn request_reconnect_outcome(&self) -> ReconnectRequestOutcome {
1034        if self.closed.load(Ordering::SeqCst) {
1035            return ReconnectRequestOutcome::Closed;
1036        }
1037        self.reconnect_auth
1038            .request(self.auth_tx.borrow().generation)
1039    }
1040
1041    pub(crate) fn begin_shutdown(&self) {
1042        self.closed.store(true, Ordering::SeqCst);
1043        self.dead_peer_enabled.store(false, Ordering::Release);
1044        self.dead_peer_task.abort();
1045        self.socket.begin_shutdown();
1046    }
1047
1048    /// Closes the stream connection.
1049    ///
1050    /// # Errors
1051    ///
1052    /// Returns an error if the dead-peer task fails or does not stop after abort.
1053    pub async fn close(&self) -> Result<(), BetfairStreamError> {
1054        self.begin_shutdown();
1055        self.socket.close().await;
1056
1057        if let Some(outcome) = self
1058            .dead_peer_task
1059            .finish(Duration::from_secs(1), Duration::from_secs(2))
1060            .await
1061        {
1062            match outcome {
1063                TaskJoinOutcome::Completed(()) | TaskJoinOutcome::Aborted => Ok(()),
1064                TaskJoinOutcome::Failed(e) => Err(BetfairStreamError::Disconnected(format!(
1065                    "dead-peer task failed: {e}"
1066                ))),
1067                TaskJoinOutcome::Incomplete => Err(BetfairStreamError::Timeout(
1068                    "dead-peer task did not stop after abort".to_string(),
1069                )),
1070            }
1071        } else {
1072            Ok(())
1073        }
1074    }
1075}
1076
1077impl Drop for BetfairStreamClient {
1078    fn drop(&mut self) {
1079        self.dead_peer_enabled.store(false, Ordering::Release);
1080
1081        self.dead_peer_task.abort();
1082    }
1083}
1084
1085fn lock_stream_state(lock: &Mutex<()>) -> MutexGuard<'_, ()> {
1086    lock.lock()
1087}
1088
1089/// Betfair race stream client for Total Performance Data (TPD).
1090///
1091/// Connects to `sports-data-stream-api.betfair.com` and subscribes to Race Change
1092/// Messages (RCM) with live GPS tracking data. Simpler than [`BetfairStreamClient`]:
1093/// no clk-based delta resumption, just auth + raceSubscription on (re)connect.
1094#[derive(Debug)]
1095pub struct BetfairRaceStreamClient {
1096    socket: SocketClient,
1097    auth_tx: watch::Sender<StreamAuth>,
1098    reconnect_auth: Arc<ReconnectAuthState>,
1099    closed: AtomicBool,
1100}
1101
1102impl BetfairRaceStreamClient {
1103    /// Connects to the Betfair race stream and subscribes.
1104    ///
1105    /// The `race_fatal_tx` channel receives a signal when the server returns a
1106    /// fatal status error (e.g. NOT_AUTHORIZED, no TPD entitlement). The caller
1107    /// should monitor this channel and close the client when it fires.
1108    ///
1109    /// # Errors
1110    ///
1111    /// Returns an error if the connection fails or the initial send fails.
1112    pub async fn connect(
1113        credential: &BetfairCredential,
1114        session_token: String,
1115        handler: TcpMessageHandler,
1116        config: BetfairStreamConfig,
1117        race_fatal_tx: tokio::sync::mpsc::UnboundedSender<()>,
1118    ) -> Result<Self, BetfairStreamError> {
1119        let subscription = AuxiliaryStreamSubscription::race(race_fatal_tx)?;
1120        Self::connect_with_subscription(
1121            credential,
1122            session_token,
1123            StreamHandler::Raw(handler),
1124            config,
1125            subscription,
1126            None,
1127        )
1128        .await
1129    }
1130
1131    pub(crate) async fn connect_decoded(
1132        credential: &BetfairCredential,
1133        session_token: String,
1134        handler: StreamMessageHandler,
1135        config: BetfairStreamConfig,
1136        race_fatal_tx: tokio::sync::mpsc::UnboundedSender<()>,
1137        state_sink: Option<SocketStateSink>,
1138    ) -> Result<Self, BetfairStreamError> {
1139        let subscription = AuxiliaryStreamSubscription::race(race_fatal_tx)?;
1140        Self::connect_with_subscription(
1141            credential,
1142            session_token,
1143            StreamHandler::Decoded(handler),
1144            config,
1145            subscription,
1146            state_sink,
1147        )
1148        .await
1149    }
1150
1151    /// Connects to the Betfair sports data stream and subscribes to cricket.
1152    ///
1153    /// The `cricket_fatal_tx` channel receives a signal when the server returns
1154    /// a fatal status error.
1155    ///
1156    /// # Errors
1157    ///
1158    /// Returns an error if the connection fails or the initial send fails.
1159    pub async fn connect_cricket(
1160        credential: &BetfairCredential,
1161        session_token: String,
1162        handler: TcpMessageHandler,
1163        config: BetfairStreamConfig,
1164        cricket_fatal_tx: tokio::sync::mpsc::UnboundedSender<()>,
1165    ) -> Result<Self, BetfairStreamError> {
1166        let subscription = AuxiliaryStreamSubscription::cricket(cricket_fatal_tx)?;
1167        Self::connect_with_subscription(
1168            credential,
1169            session_token,
1170            StreamHandler::Raw(handler),
1171            config,
1172            subscription,
1173            None,
1174        )
1175        .await
1176    }
1177
1178    pub(crate) async fn connect_cricket_decoded(
1179        credential: &BetfairCredential,
1180        session_token: String,
1181        handler: StreamMessageHandler,
1182        config: BetfairStreamConfig,
1183        cricket_fatal_tx: tokio::sync::mpsc::UnboundedSender<()>,
1184        state_sink: Option<SocketStateSink>,
1185    ) -> Result<Self, BetfairStreamError> {
1186        let subscription = AuxiliaryStreamSubscription::cricket(cricket_fatal_tx)?;
1187        Self::connect_with_subscription(
1188            credential,
1189            session_token,
1190            StreamHandler::Decoded(handler),
1191            config,
1192            subscription,
1193            state_sink,
1194        )
1195        .await
1196    }
1197
1198    async fn connect_with_subscription(
1199        credential: &BetfairCredential,
1200        session_token: String,
1201        handler: StreamHandler,
1202        config: BetfairStreamConfig,
1203        subscription: AuxiliaryStreamSubscription,
1204        state_sink: Option<SocketStateSink>,
1205    ) -> Result<Self, BetfairStreamError> {
1206        let AuxiliaryStreamSubscription {
1207            bytes: sub_bytes,
1208            label,
1209            fatal_hint,
1210            fatal_tx,
1211        } = subscription;
1212
1213        let auth = Authentication::new(credential.app_key().to_string(), session_token);
1214        let auth_bytes_vec = serde_json::to_vec(&auth)?;
1215        let auth_bytes = Bytes::from(auth_bytes_vec.clone());
1216        let reconnect_auth = Arc::new(ReconnectAuthState::default());
1217        let (auth_tx, auth_rx) = watch::channel(StreamAuth {
1218            generation: 0,
1219            bytes: auth_bytes,
1220        });
1221
1222        let mode = if config.use_tls {
1223            Mode::Tls
1224        } else {
1225            Mode::Plain
1226        };
1227
1228        let reconnect_auth_h = Arc::clone(&reconnect_auth);
1229        let message_handler: TcpMessageHandler = Arc::new(move |data: &[u8]| {
1230            let Some(msg) = handler.decode(data) else {
1231                return;
1232            };
1233
1234            if let StreamMessage::Status(status) = &msg {
1235                if let Some(ref code) = status.error_code
1236                    && code.is_race_stream_fatal()
1237                {
1238                    log::error!(
1239                        "Betfair {label} stream fatal error: {:?} - {:?} ({fatal_hint})",
1240                        status.error_code,
1241                        status.error_message,
1242                    );
1243                    let _ = fatal_tx.send(());
1244                    return;
1245                }
1246
1247                if status.connection_closed {
1248                    log::warn!(
1249                        "Betfair {label} stream closed: {:?} - {:?}",
1250                        status.error_code,
1251                        status.error_message,
1252                    );
1253                } else if status.error_code.is_some() {
1254                    log::warn!(
1255                        "Betfair {label} stream status: {:?} - {:?}",
1256                        status.error_code,
1257                        status.error_message,
1258                    );
1259                }
1260            }
1261
1262            if matches!(msg, StreamMessage::Connection(_)) {
1263                reconnect_auth_h.request_pending();
1264            }
1265
1266            handler.handle(data, msg);
1267        });
1268
1269        let auth_reconnect = auth_rx;
1270        let reconnect_auth_replay = Arc::clone(&reconnect_auth);
1271        let sub_reconnect = sub_bytes.clone();
1272        let reconnect_replay: SocketReconnectReplay = Arc::new(move || {
1273            let auth = auth_reconnect.borrow().clone();
1274            reconnect_auth_replay.record_replay(auth.generation);
1275            let mut combined = Vec::with_capacity(auth.bytes.len() + 2 + sub_reconnect.len());
1276            combined.extend_from_slice(&auth.bytes);
1277            combined.extend_from_slice(b"\r\n");
1278            combined.extend_from_slice(&sub_reconnect);
1279            vec![Bytes::from(combined)]
1280        });
1281
1282        let url = format!("{}:{}", config.host, config.port);
1283        let socket_config = SocketConfig {
1284            url,
1285            mode,
1286            suffix: b"\r\n".to_vec(),
1287            message_handler: Some(message_handler),
1288            heartbeat: outbound_heartbeat(config.heartbeat_secs),
1289            connect_timeout_ms: None,
1290            reconnect_delay_initial_ms: Some(config.reconnect_delay_initial_ms),
1291            reconnect_delay_max_ms: Some(config.reconnect_delay_max_ms),
1292            reconnect_backoff_factor: None,
1293            reconnect_jitter_ms: None,
1294            connection_max_retries: None,
1295            reconnect_max_attempts: None,
1296            heartbeat_timeout_secs: heartbeat_timeout(
1297                HeartbeatTimeoutSource::Outbound,
1298                config.heartbeat_secs,
1299                config.heartbeat_timeout_secs,
1300            ),
1301            certs_dir: None,
1302        };
1303
1304        let socket = SocketClient::builder()
1305            .config(socket_config)
1306            .maybe_state_sink(state_sink)
1307            .reconnect_replay(reconnect_replay)
1308            .connect()
1309            .await
1310            .map_err(|e| BetfairStreamError::ConnectionFailed(e.to_string()))?;
1311        reconnect_auth.set_handle(socket.reconnect_handle());
1312
1313        let mut combined = Vec::with_capacity(auth_bytes_vec.len() + 2 + sub_bytes.len());
1314        combined.extend_from_slice(&auth_bytes_vec);
1315        combined.extend_from_slice(b"\r\n");
1316        combined.extend_from_slice(&sub_bytes);
1317        socket
1318            .send_bytes(combined)
1319            .await
1320            .map_err(|e| BetfairStreamError::ConnectionFailed(e.to_string()))?;
1321
1322        Ok(Self {
1323            socket,
1324            auth_tx,
1325            reconnect_auth,
1326            closed: AtomicBool::new(false),
1327        })
1328    }
1329
1330    /// Returns `true` if the connection is active.
1331    #[must_use]
1332    pub fn is_active(&self) -> bool {
1333        self.socket.is_active()
1334    }
1335
1336    /// Pushes refreshed auth bytes so the next reconnection uses
1337    /// the current session token instead of the one from initial connect.
1338    pub fn update_auth(&self, app_key: &str, session_token: String) {
1339        update_auth_state(
1340            &self.auth_tx,
1341            &Authentication::new(app_key.to_string(), session_token),
1342        );
1343    }
1344
1345    /// Requests replacement of the active stream transport.
1346    ///
1347    /// Returns `true` only when this call starts a reconnect. Duplicate requests and requests after
1348    /// close return `false`.
1349    #[must_use]
1350    pub fn request_reconnect(&self) -> bool {
1351        self.request_reconnect_outcome() == ReconnectRequestOutcome::Accepted
1352    }
1353
1354    /// Requests transport replacement and returns the exact controller outcome.
1355    pub(crate) fn request_reconnect_outcome(&self) -> ReconnectRequestOutcome {
1356        if self.closed.load(Ordering::SeqCst) {
1357            return ReconnectRequestOutcome::Closed;
1358        }
1359        self.reconnect_auth
1360            .request(self.auth_tx.borrow().generation)
1361    }
1362
1363    pub(crate) fn begin_shutdown(&self) {
1364        self.closed.store(true, Ordering::SeqCst);
1365        self.socket.begin_shutdown();
1366    }
1367
1368    /// Closes the race stream connection.
1369    pub async fn close(&self) {
1370        self.begin_shutdown();
1371        self.socket.close().await;
1372    }
1373}
1374
1375fn update_auth_state(auth_tx: &watch::Sender<StreamAuth>, auth: &Authentication) {
1376    let Ok(bytes) = serde_json::to_vec(auth) else {
1377        return;
1378    };
1379    let bytes = Bytes::from(bytes);
1380    auth_tx.send_if_modified(|current| {
1381        if current.bytes == bytes {
1382            return false;
1383        }
1384        *current = StreamAuth {
1385            generation: current.generation.wrapping_add(1),
1386            bytes,
1387        };
1388        true
1389    });
1390}
1391
1392enum StreamHandler {
1393    Raw(TcpMessageHandler),
1394    Decoded(StreamMessageHandler),
1395}
1396
1397impl StreamHandler {
1398    fn decode(&self, data: &[u8]) -> Option<StreamMessage> {
1399        match stream_decode(data) {
1400            Ok(message) => Some(message),
1401            Err(e) => {
1402                match self {
1403                    Self::Raw(handler) => handler(data),
1404                    Self::Decoded(_) => log::warn!("Failed to decode stream message: {e}"),
1405                }
1406                None
1407            }
1408        }
1409    }
1410
1411    fn handle(&self, data: &[u8], message: StreamMessage) {
1412        match self {
1413            Self::Raw(handler) => handler(data),
1414            Self::Decoded(handler) => handler(message),
1415        }
1416    }
1417}
1418
1419const fn change_complete(segment_type: Option<SegmentType>) -> bool {
1420    matches!(segment_type, None | Some(SegmentType::SegEnd))
1421}
1422
1423fn reissue_market_subscription(
1424    request_id: &AtomicU64,
1425    active_id: &AtomicU64,
1426    lifecycle: &ProtocolLifecycle,
1427    sub_tx: &watch::Sender<Option<MarketSubscription>>,
1428    clk_tx: &watch::Sender<Option<String>>,
1429    initial_clk_tx: &watch::Sender<Option<String>>,
1430    writer_tx: Option<&tokio::sync::mpsc::UnboundedSender<WriterCommand>>,
1431) {
1432    let Some(writer_tx) = writer_tx else {
1433        log::error!("Cannot recover Betfair market stream before writer initialization");
1434        return;
1435    };
1436    let Some(mut sub) = sub_tx.borrow().clone() else {
1437        log::error!("Cannot recover Betfair market stream without a retained subscription");
1438        return;
1439    };
1440    let id = request_id.fetch_add(1, Ordering::Relaxed);
1441    sub.id = Some(id);
1442    sub.clk = None;
1443    sub.initial_clk = None;
1444    let data = match serde_json::to_vec(&sub) {
1445        Ok(data) => Bytes::from(data),
1446        Err(e) => {
1447            log::error!("Failed to serialize Betfair market recovery subscription: {e}");
1448            return;
1449        }
1450    };
1451
1452    active_id.store(id, Ordering::SeqCst);
1453    lifecycle.market.set(StreamLifecycleState::Pending);
1454    lifecycle.market_was_current.store(false, Ordering::Release);
1455    lifecycle
1456        .market_requires_image
1457        .store(true, Ordering::Release);
1458    lifecycle
1459        .market_image_tainted
1460        .store(false, Ordering::Release);
1461    let _ = clk_tx.send(None);
1462    let _ = initial_clk_tx.send(None);
1463    let _ = sub_tx.send(Some(sub));
1464
1465    if let Err(e) = writer_tx.send(WriterCommand::SendOrReplay {
1466        key: MARKET_SUBSCRIPTION_REPLAY_KEY,
1467        data,
1468    }) {
1469        log::error!("Failed to queue Betfair market recovery subscription: {e}");
1470    }
1471}
1472
1473fn reissue_order_subscription(
1474    request_id: &AtomicU64,
1475    active_id: &AtomicU64,
1476    lifecycle: &ProtocolLifecycle,
1477    sub_tx: &watch::Sender<Option<OrderSubscription>>,
1478    clk_tx: &watch::Sender<Option<String>>,
1479    initial_clk_tx: &watch::Sender<Option<String>>,
1480    writer_tx: Option<&tokio::sync::mpsc::UnboundedSender<WriterCommand>>,
1481) {
1482    let Some(writer_tx) = writer_tx else {
1483        log::error!("Cannot recover Betfair order stream before writer initialization");
1484        return;
1485    };
1486    let Some(mut sub) = sub_tx.borrow().clone() else {
1487        log::error!("Cannot recover Betfair order stream without a retained subscription");
1488        return;
1489    };
1490    let id = request_id.fetch_add(1, Ordering::Relaxed);
1491    sub.id = Some(id);
1492    sub.clk = None;
1493    sub.initial_clk = None;
1494    let data = match serde_json::to_vec(&sub) {
1495        Ok(data) => Bytes::from(data),
1496        Err(e) => {
1497            log::error!("Failed to serialize Betfair order recovery subscription: {e}");
1498            return;
1499        }
1500    };
1501
1502    active_id.store(id, Ordering::SeqCst);
1503    lifecycle.order.set(StreamLifecycleState::Pending);
1504    lifecycle.order_was_current.store(false, Ordering::Release);
1505    lifecycle
1506        .order_requires_image
1507        .store(true, Ordering::Release);
1508    lifecycle
1509        .order_image_tainted
1510        .store(false, Ordering::Release);
1511    let _ = clk_tx.send(None);
1512    let _ = initial_clk_tx.send(None);
1513    let _ = sub_tx.send(Some(sub));
1514
1515    if let Err(e) = writer_tx.send(WriterCommand::SendOrReplay {
1516        key: ORDER_SUBSCRIPTION_REPLAY_KEY,
1517        data,
1518    }) {
1519        log::error!("Failed to queue Betfair order recovery subscription: {e}");
1520    }
1521}
1522
1523fn update_stream_state(
1524    clk: &Option<String>,
1525    initial_clk: &Option<String>,
1526    heartbeat_ms: Option<u64>,
1527    clk_tx: &watch::Sender<Option<String>>,
1528    initial_clk_tx: &watch::Sender<Option<String>>,
1529    timeout_override: bool,
1530    dead_peer_timeout_ms: &AtomicU64,
1531) {
1532    if clk.is_some() {
1533        let _ = clk_tx.send(clk.clone());
1534    }
1535
1536    if initial_clk.is_some() {
1537        let _ = initial_clk_tx.send(initial_clk.clone());
1538    }
1539    update_negotiated_heartbeat(heartbeat_ms, timeout_override, dead_peer_timeout_ms);
1540}
1541
1542fn update_negotiated_heartbeat(
1543    interval_ms: Option<u64>,
1544    timeout_override: bool,
1545    dead_peer_timeout_ms: &AtomicU64,
1546) {
1547    if !timeout_override
1548        && let Some(interval_ms) = interval_ms
1549        && (BETFAIR_STREAM_HEARTBEAT_MIN_MS..=BETFAIR_STREAM_HEARTBEAT_MAX_MS)
1550            .contains(&interval_ms)
1551    {
1552        dead_peer_timeout_ms.store(interval_ms.saturating_mul(2), Ordering::Release);
1553    }
1554}
1555
1556fn outbound_heartbeat(interval_secs: Option<u64>) -> Option<SocketHeartbeat> {
1557    interval_secs.map(|interval_secs| SocketHeartbeat {
1558        interval_secs,
1559        payload: b"{\"op\":\"heartbeat\"}".to_vec(),
1560    })
1561}
1562
1563fn heartbeat_timeout(
1564    source: HeartbeatTimeoutSource,
1565    interval_secs: Option<u64>,
1566    timeout_secs: Option<u64>,
1567) -> Option<u64> {
1568    match source {
1569        HeartbeatTimeoutSource::Outbound => {
1570            interval_secs.map(|interval| timeout_secs.unwrap_or(interval.saturating_mul(2)))
1571        }
1572        HeartbeatTimeoutSource::Server => None,
1573    }
1574}
1575
1576fn validate_subscription_heartbeat(heartbeat_ms: u64) -> Result<(), BetfairStreamError> {
1577    if !(BETFAIR_STREAM_HEARTBEAT_MIN_MS..=BETFAIR_STREAM_HEARTBEAT_MAX_MS).contains(&heartbeat_ms)
1578    {
1579        return Err(BetfairStreamError::ProtocolError(format!(
1580            "subscription heartbeat must be in range [{BETFAIR_STREAM_HEARTBEAT_MIN_MS}, \
1581             {BETFAIR_STREAM_HEARTBEAT_MAX_MS}] ms, was {heartbeat_ms} ms",
1582        )));
1583    }
1584
1585    Ok(())
1586}
1587
1588struct AuxiliaryStreamSubscription {
1589    bytes: Bytes,
1590    label: &'static str,
1591    fatal_hint: &'static str,
1592    fatal_tx: tokio::sync::mpsc::UnboundedSender<()>,
1593}
1594
1595impl AuxiliaryStreamSubscription {
1596    fn race(fatal_tx: tokio::sync::mpsc::UnboundedSender<()>) -> Result<Self, serde_json::Error> {
1597        Ok(Self {
1598            bytes: Bytes::from(serde_json::to_vec(&RaceSubscription::new(1))?),
1599            label: "race",
1600            fatal_hint: "check TPD entitlement on your Betfair app key",
1601            fatal_tx,
1602        })
1603    }
1604
1605    fn cricket(
1606        fatal_tx: tokio::sync::mpsc::UnboundedSender<()>,
1607    ) -> Result<Self, serde_json::Error> {
1608        Ok(Self {
1609            bytes: Bytes::from(serde_json::to_vec(&CricketSubscription::new(1))?),
1610            label: "cricket",
1611            fatal_hint: "check cricket data entitlement on your Betfair app key",
1612            fatal_tx,
1613        })
1614    }
1615}
1616
1617#[derive(Clone, Debug)]
1618struct StreamAuth {
1619    generation: u64,
1620    bytes: Bytes,
1621}
1622
1623#[derive(Debug, Default)]
1624struct ReconnectAuthState {
1625    replay_generation: AtomicU64,
1626    pending_generation: AtomicU64,
1627    reconnect_handle: OnceLock<SocketReconnectHandle>,
1628}
1629
1630impl ReconnectAuthState {
1631    fn set_handle(&self, handle: SocketReconnectHandle) {
1632        let result = self.reconnect_handle.set(handle);
1633        debug_assert!(result.is_ok(), "reconnect handle is set only once");
1634    }
1635
1636    fn record_replay(&self, generation: u64) {
1637        self.replay_generation.store(generation, Ordering::SeqCst);
1638        let _ = self
1639            .pending_generation
1640            .try_update(Ordering::SeqCst, Ordering::SeqCst, |pending| {
1641                (pending != 0 && pending <= generation).then_some(0)
1642            });
1643    }
1644
1645    fn request(&self, auth_generation: u64) -> ReconnectRequestOutcome {
1646        let Some(handle) = self.reconnect_handle.get() else {
1647            return ReconnectRequestOutcome::Unsupported;
1648        };
1649
1650        let outcome = handle.request_reconnect();
1651        if outcome == ReconnectRequestOutcome::AlreadyReconnecting
1652            && auth_generation > self.replay_generation.load(Ordering::SeqCst)
1653        {
1654            self.pending_generation
1655                .fetch_max(auth_generation, Ordering::SeqCst);
1656        }
1657
1658        outcome
1659    }
1660
1661    fn request_pending(&self) {
1662        let pending_generation = self.pending_generation.load(Ordering::SeqCst);
1663        if pending_generation == 0
1664            || pending_generation <= self.replay_generation.load(Ordering::SeqCst)
1665        {
1666            return;
1667        }
1668
1669        let Some(handle) = self.reconnect_handle.get() else {
1670            return;
1671        };
1672
1673        match handle.request_reconnect() {
1674            ReconnectRequestOutcome::Accepted => {
1675                let _ = self.pending_generation.compare_exchange(
1676                    pending_generation,
1677                    0,
1678                    Ordering::SeqCst,
1679                    Ordering::SeqCst,
1680                );
1681            }
1682            ReconnectRequestOutcome::AlreadyReconnecting => {}
1683            ReconnectRequestOutcome::Disconnected
1684            | ReconnectRequestOutcome::Closed
1685            | ReconnectRequestOutcome::Unsupported => {
1686                self.pending_generation.store(0, Ordering::SeqCst);
1687            }
1688        }
1689    }
1690}
1691
1692#[cfg(test)]
1693mod tests {
1694    use nautilus_network::SocketState;
1695    use rstest::rstest;
1696
1697    use super::*;
1698    use crate::stream::messages::{
1699        Authentication, CricketSubscription, MarketDataFilter, RaceSubscription, StreamMarketFilter,
1700    };
1701
1702    #[rstest]
1703    #[case::no_source(HeartbeatTimeoutSource::Outbound, None, None, None)]
1704    #[case::outbound_override(HeartbeatTimeoutSource::Outbound, Some(5), Some(60), Some(60))]
1705    #[case::outbound_derived(HeartbeatTimeoutSource::Outbound, Some(5), None, Some(10))]
1706    #[case::server(HeartbeatTimeoutSource::Server, None, None, None)]
1707    fn test_heartbeat_timeout(
1708        #[case] source: HeartbeatTimeoutSource,
1709        #[case] interval_secs: Option<u64>,
1710        #[case] timeout_secs: Option<u64>,
1711        #[case] expected: Option<u64>,
1712    ) {
1713        assert_eq!(
1714            heartbeat_timeout(source, interval_secs, timeout_secs),
1715            expected
1716        );
1717    }
1718
1719    #[rstest]
1720    fn test_reissue_before_writer_initialization_stays_fail_closed() {
1721        let request_id = AtomicU64::new(17);
1722        let market_active_id = AtomicU64::new(11);
1723        let order_active_id = AtomicU64::new(13);
1724        let lifecycle = ProtocolLifecycle::default();
1725        lifecycle.market.set(StreamLifecycleState::Degraded);
1726        lifecycle.order.set(StreamLifecycleState::Degraded);
1727        let (market_sub_tx, _market_sub_rx) = watch::channel(None::<MarketSubscription>);
1728        let (order_sub_tx, _order_sub_rx) = watch::channel(None::<OrderSubscription>);
1729        let (market_clk_tx, _market_clk_rx) = watch::channel(None::<String>);
1730        let (market_initial_clk_tx, _market_initial_clk_rx) = watch::channel(None::<String>);
1731        let (order_clk_tx, _order_clk_rx) = watch::channel(None::<String>);
1732        let (order_initial_clk_tx, _order_initial_clk_rx) = watch::channel(None::<String>);
1733
1734        reissue_market_subscription(
1735            &request_id,
1736            &market_active_id,
1737            &lifecycle,
1738            &market_sub_tx,
1739            &market_clk_tx,
1740            &market_initial_clk_tx,
1741            None,
1742        );
1743        reissue_order_subscription(
1744            &request_id,
1745            &order_active_id,
1746            &lifecycle,
1747            &order_sub_tx,
1748            &order_clk_tx,
1749            &order_initial_clk_tx,
1750            None,
1751        );
1752
1753        assert_eq!(
1754            (
1755                request_id.load(Ordering::Acquire),
1756                market_active_id.load(Ordering::Acquire),
1757                order_active_id.load(Ordering::Acquire),
1758                lifecycle.market.get(),
1759                lifecycle.order.get(),
1760            ),
1761            (
1762                17,
1763                11,
1764                13,
1765                StreamLifecycleState::Degraded,
1766                StreamLifecycleState::Degraded,
1767            ),
1768        );
1769    }
1770
1771    #[rstest]
1772    fn test_invalid_clock_status_resets_clocks() {
1773        let (market_clk_tx, market_clk_rx) = watch::channel(Some("old-market-clk".to_string()));
1774        let (market_initial_clk_tx, market_initial_clk_rx) =
1775            watch::channel(Some("old-market-iclk".to_string()));
1776        let (order_clk_tx, order_clk_rx) = watch::channel(Some("old-order-clk".to_string()));
1777        let (order_initial_clk_tx, order_initial_clk_rx) =
1778            watch::channel(Some("old-order-iclk".to_string()));
1779
1780        let handler: TcpMessageHandler = Arc::new(move |data: &[u8]| {
1781            if let Ok(msg) = stream_decode(data)
1782                && let StreamMessage::Status(status) = &msg
1783                && status.error_code == Some(StatusErrorCode::InvalidClock)
1784            {
1785                let _ = market_clk_tx.send(None);
1786                let _ = market_initial_clk_tx.send(None);
1787                let _ = order_clk_tx.send(None);
1788                let _ = order_initial_clk_tx.send(None);
1789            }
1790        });
1791
1792        handler(
1793            br#"{"op":"status","statusCode":"503","errorCode":"INVALID_CLOCK","connectionClosed":true}"#,
1794        );
1795
1796        assert!(
1797            market_clk_rx.borrow().is_none(),
1798            "market clk must be cleared"
1799        );
1800        assert!(
1801            market_initial_clk_rx.borrow().is_none(),
1802            "market initialClk must be cleared"
1803        );
1804        assert!(order_clk_rx.borrow().is_none(), "order clk must be cleared");
1805        assert!(
1806            order_initial_clk_rx.borrow().is_none(),
1807            "order initialClk must be cleared"
1808        );
1809    }
1810
1811    #[rstest]
1812    fn test_auth_message_serialization() {
1813        let auth = Authentication::new("my-app-key".to_string(), "my-session".to_string());
1814        let json = serde_json::to_string(&auth).unwrap();
1815        assert!(json.contains("\"op\":\"authentication\""));
1816        assert!(json.contains("\"appKey\":\"my-app-key\""));
1817        assert!(json.contains("\"session\":\"my-session\""));
1818    }
1819
1820    #[rstest]
1821    #[case::exchange(true)]
1822    #[case::auxiliary(false)]
1823    fn test_update_auth_state_changes_once_per_distinct_payload(#[case] with_id: bool) {
1824        let make_auth = |session: &str| {
1825            if with_id {
1826                Authentication::with_id(
1827                    "test-app-key".to_string(),
1828                    session.to_string(),
1829                    AUTH_REQUEST_ID,
1830                )
1831            } else {
1832                Authentication::new("test-app-key".to_string(), session.to_string())
1833            }
1834        };
1835        let initial = make_auth("initial");
1836        let initial_bytes = Bytes::from(serde_json::to_vec(&initial).unwrap());
1837        let (auth_tx, auth_rx) = watch::channel(StreamAuth {
1838            generation: 7,
1839            bytes: initial_bytes.clone(),
1840        });
1841
1842        update_auth_state(&auth_tx, &initial);
1843        assert_eq!(auth_rx.borrow().generation, 7);
1844        assert_eq!(auth_rx.borrow().bytes, initial_bytes);
1845
1846        let replacement = make_auth("replacement");
1847        let replacement_bytes = Bytes::from(serde_json::to_vec(&replacement).unwrap());
1848        update_auth_state(&auth_tx, &replacement);
1849        assert_eq!(auth_rx.borrow().generation, 8);
1850        assert_eq!(auth_rx.borrow().bytes, replacement_bytes);
1851
1852        update_auth_state(&auth_tx, &replacement);
1853        assert_eq!(auth_rx.borrow().generation, 8);
1854    }
1855
1856    #[rstest]
1857    fn test_clk_is_updated_from_mcm() {
1858        let (market_clk_tx, market_clk_rx) = watch::channel(None::<String>);
1859        let (market_initial_clk_tx, market_initial_clk_rx) = watch::channel(None::<String>);
1860        let (order_clk_tx, order_clk_rx) = watch::channel(None::<String>);
1861        let (order_initial_clk_tx, order_initial_clk_rx) = watch::channel(None::<String>);
1862        let market_active_sub_id = Arc::new(AtomicU64::new(5));
1863        let order_active_sub_id = Arc::new(AtomicU64::new(6));
1864
1865        let handler: TcpMessageHandler = Arc::new(move |data: &[u8]| {
1866            if let Ok(msg) = stream_decode(data) {
1867                match &msg {
1868                    StreamMessage::MarketChange(mcm) => {
1869                        let active = market_active_sub_id.load(Ordering::SeqCst);
1870                        if active > 0 && mcm.id.is_none_or(|id| id == active) {
1871                            if mcm.clk.is_some() {
1872                                let _ = market_clk_tx.send(mcm.clk.clone());
1873                            }
1874
1875                            if mcm.initial_clk.is_some() {
1876                                let _ = market_initial_clk_tx.send(mcm.initial_clk.clone());
1877                            }
1878                        }
1879                    }
1880                    StreamMessage::OrderChange(ocm) => {
1881                        let active = order_active_sub_id.load(Ordering::SeqCst);
1882                        if active > 0 && ocm.id.is_none_or(|id| id == active) {
1883                            if ocm.clk.is_some() {
1884                                let _ = order_clk_tx.send(ocm.clk.clone());
1885                            }
1886
1887                            if ocm.initial_clk.is_some() {
1888                                let _ = order_initial_clk_tx.send(ocm.initial_clk.clone());
1889                            }
1890                        }
1891                    }
1892                    _ => {}
1893                }
1894            }
1895        });
1896
1897        // MCM/OCM with matching subscription id update clocks.
1898        handler(br#"{"op":"mcm","id":5,"pt":1000,"initialClk":"mcm-iclk","clk":"mcm-clk"}"#);
1899        handler(br#"{"op":"ocm","id":6,"pt":2000,"initialClk":"ocm-iclk","clk":"ocm-clk"}"#);
1900
1901        assert_eq!(market_clk_rx.borrow().as_deref(), Some("mcm-clk"));
1902        assert_eq!(market_initial_clk_rx.borrow().as_deref(), Some("mcm-iclk"));
1903        assert_eq!(order_clk_rx.borrow().as_deref(), Some("ocm-clk"));
1904        assert_eq!(order_initial_clk_rx.borrow().as_deref(), Some("ocm-iclk"));
1905
1906        // MCM without an id (e.g. heartbeat) is accepted for the active subscription.
1907        handler(br#"{"op":"mcm","pt":1001,"clk":"hb-clk"}"#);
1908        assert_eq!(market_clk_rx.borrow().as_deref(), Some("hb-clk"));
1909
1910        // MCM from a stale subscription (explicit wrong id) must not overwrite stored clocks.
1911        handler(br#"{"op":"mcm","id":4,"pt":1002,"clk":"stale-clk"}"#);
1912        assert_eq!(market_clk_rx.borrow().as_deref(), Some("hb-clk"));
1913    }
1914
1915    #[rstest]
1916    fn test_reconnect_callback_sends_auth_and_subscription() {
1917        let (market_clk_tx, market_clk_rx) = watch::channel(Some("mcm-clk1".to_string()));
1918        let (market_initial_clk_tx, market_initial_clk_rx) =
1919            watch::channel(Some("mcm-iclk1".to_string()));
1920        let (order_clk_tx, order_clk_rx) = watch::channel(Some("ocm-clk1".to_string()));
1921        let (order_initial_clk_tx, order_initial_clk_rx) =
1922            watch::channel(Some("ocm-iclk1".to_string()));
1923        let (market_sub_tx, market_sub_rx) = watch::channel(None::<MarketSubscription>);
1924        let (order_sub_tx, order_sub_rx) = watch::channel(None::<OrderSubscription>);
1925
1926        let auth = Authentication::new("key".to_string(), "token".to_string());
1927        let auth_bytes = Bytes::from(serde_json::to_vec(&auth).unwrap());
1928
1929        let _ = market_sub_tx.send(Some(MarketSubscription {
1930            op: STREAM_OP_MARKET_SUBSCRIPTION.to_string(),
1931            id: Some(1),
1932            market_filter: StreamMarketFilter::default(),
1933            market_data_filter: MarketDataFilter::default(),
1934            clk: None,
1935            conflate_ms: None,
1936            heartbeat_ms: Some(BETFAIR_STREAM_HEARTBEAT_MAX_MS),
1937            initial_clk: None,
1938            segmentation_enabled: Some(true),
1939        }));
1940        let _ = order_sub_tx.send(Some(OrderSubscription {
1941            op: STREAM_OP_ORDER_SUBSCRIPTION.to_string(),
1942            id: Some(2),
1943            order_filter: None,
1944            clk: None,
1945            conflate_ms: None,
1946            heartbeat_ms: Some(BETFAIR_STREAM_HEARTBEAT_MAX_MS),
1947            initial_clk: None,
1948            segmentation_enabled: Some(true),
1949        }));
1950
1951        let auth_bytes_reconnect = auth_bytes;
1952        let reconnect_replay: SocketReconnectReplay = Arc::new(move || {
1953            let mut replay = Vec::with_capacity(3);
1954            let market_sub = market_sub_rx.borrow().clone();
1955            let order_sub = order_sub_rx.borrow().clone();
1956
1957            replay.push(auth_bytes_reconnect.clone());
1958
1959            if let Some(mut sub) = market_sub {
1960                sub.clk = market_clk_rx.borrow().clone();
1961                sub.initial_clk = market_initial_clk_rx.borrow().clone();
1962                if let Ok(sub_bytes) = serde_json::to_vec(&sub) {
1963                    replay.push(Bytes::from(sub_bytes));
1964                }
1965            }
1966
1967            if let Some(mut sub) = order_sub {
1968                sub.clk = order_clk_rx.borrow().clone();
1969                sub.initial_clk = order_initial_clk_rx.borrow().clone();
1970                if let Ok(sub_bytes) = serde_json::to_vec(&sub) {
1971                    replay.push(Bytes::from(sub_bytes));
1972                }
1973            }
1974
1975            replay
1976        });
1977
1978        drop(market_clk_tx);
1979        drop(market_initial_clk_tx);
1980        drop(order_clk_tx);
1981        drop(order_initial_clk_tx);
1982
1983        let replay = reconnect_replay();
1984        let [auth_bytes, market_bytes, order_bytes] = replay.as_slice() else {
1985            panic!("expected auth, market, and order replay messages");
1986        };
1987
1988        let auth_str = std::str::from_utf8(auth_bytes).unwrap();
1989        let market_str = std::str::from_utf8(market_bytes).unwrap();
1990        let order_str = std::str::from_utf8(order_bytes).unwrap();
1991
1992        assert!(auth_str.contains("\"op\":\"authentication\""));
1993        assert!(market_str.contains("\"op\":\"marketSubscription\""));
1994        // Both clk and initialClk must be injected into each resubscription
1995        assert!(market_str.contains("\"clk\":\"mcm-clk1\""));
1996        assert!(market_str.contains("\"initialClk\":\"mcm-iclk1\""));
1997
1998        assert!(order_str.contains("\"op\":\"orderSubscription\""));
1999        assert!(order_str.contains("\"clk\":\"ocm-clk1\""));
2000        assert!(order_str.contains("\"initialClk\":\"ocm-iclk1\""));
2001    }
2002
2003    #[rstest]
2004    #[tokio::test]
2005    async fn test_auth_update_after_replay_snapshot_requests_follow_up() {
2006        use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
2007
2008        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2009        let port = listener.local_addr().unwrap().port();
2010        let server = tokio::spawn(async move {
2011            let (socket, _) = listener.accept().await.unwrap();
2012            let (read_half, _write_half) = socket.into_split();
2013            let mut reader = BufReader::new(read_half);
2014            let mut line = String::new();
2015            reader.read_line(&mut line).await.unwrap();
2016            line.clear();
2017            reader.read_line(&mut line).await.unwrap();
2018
2019            let (socket, _) = listener.accept().await.unwrap();
2020            let (read_half, mut write_half) = socket.into_split();
2021            let mut reader = BufReader::new(read_half);
2022            line.clear();
2023            reader.read_line(&mut line).await.unwrap();
2024            let auth: serde_json::Value = serde_json::from_str(&line).unwrap();
2025            assert_eq!(auth["session"], "replacement-1");
2026            line.clear();
2027            reader.read_line(&mut line).await.unwrap();
2028            write_half
2029                .write_all(b"{\"op\":\"connection\",\"connectionId\":\"replacement-1\"}\r\n")
2030                .await
2031                .unwrap();
2032
2033            let (socket, _) = listener.accept().await.unwrap();
2034            let (read_half, _write_half) = socket.into_split();
2035            let mut reader = BufReader::new(read_half);
2036            line.clear();
2037            reader.read_line(&mut line).await.unwrap();
2038            let auth: serde_json::Value = serde_json::from_str(&line).unwrap();
2039            assert_eq!(auth["session"], "replacement-2");
2040            line.clear();
2041            reader.read_line(&mut line).await.unwrap();
2042            let subscription: serde_json::Value = serde_json::from_str(&line).unwrap();
2043            assert_eq!(subscription["op"], "orderSubscription");
2044        });
2045
2046        let credential = BetfairCredential::new(
2047            "testuser".to_string(),
2048            "testpass".to_string(),
2049            "test-app-key".to_string(),
2050        );
2051        let config = BetfairStreamConfig {
2052            host: "127.0.0.1".to_string(),
2053            port,
2054            heartbeat_secs: None,
2055            heartbeat_timeout_secs: Some(60),
2056            reconnect_delay_initial_ms: 200,
2057            reconnect_delay_max_ms: 1_000,
2058            use_tls: false,
2059        };
2060        let client = BetfairStreamClient::connect(
2061            &credential,
2062            "initial".to_string(),
2063            Arc::new(|_| {}),
2064            config,
2065        )
2066        .await
2067        .unwrap();
2068        client.subscribe_orders(None, Some(5_000)).await.unwrap();
2069
2070        client.update_auth("test-app-key", "replacement-1".to_string());
2071        assert!(client.request_reconnect());
2072        tokio::time::timeout(std::time::Duration::from_secs(2), async {
2073            while client
2074                .reconnect_auth
2075                .replay_generation
2076                .load(Ordering::SeqCst)
2077                < 1
2078            {
2079                tokio::task::yield_now().await;
2080            }
2081        })
2082        .await
2083        .unwrap();
2084
2085        client.update_auth("test-app-key", "replacement-2".to_string());
2086        assert!(!client.request_reconnect());
2087        assert_eq!(
2088            client
2089                .reconnect_auth
2090                .pending_generation
2091                .load(Ordering::SeqCst),
2092            2,
2093        );
2094
2095        tokio::time::timeout(std::time::Duration::from_secs(5), server)
2096            .await
2097            .unwrap()
2098            .unwrap();
2099        client.close().await.expect("close stream");
2100    }
2101
2102    #[rstest]
2103    fn test_race_subscription_serialization() {
2104        let sub = RaceSubscription::new(42);
2105        let json = serde_json::to_string(&sub).unwrap();
2106        assert!(json.contains("\"op\":\"raceSubscription\""));
2107        assert!(json.contains("\"id\":42"));
2108    }
2109
2110    #[rstest]
2111    fn test_cricket_subscription_serialization() {
2112        let sub = CricketSubscription::new(42);
2113        let json = serde_json::to_string(&sub).unwrap();
2114        assert!(json.contains("\"op\":\"cricketSubscription\""));
2115        assert!(json.contains("\"id\":42"));
2116    }
2117
2118    #[rstest]
2119    #[case::race(false, "raceSubscription")]
2120    #[case::cricket(true, "cricketSubscription")]
2121    #[tokio::test]
2122    async fn test_auxiliary_stream_state_and_controller_reconnect(
2123        #[case] cricket: bool,
2124        #[case] subscription_op: &'static str,
2125    ) {
2126        use std::time::Duration;
2127
2128        use parking_lot::Mutex;
2129        use tokio::io::{AsyncBufReadExt, BufReader};
2130
2131        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2132        let port = listener.local_addr().unwrap().port();
2133        let (initial_tx, initial_rx) = tokio::sync::oneshot::channel();
2134        let (replacement_tx, replacement_rx) = tokio::sync::oneshot::channel();
2135        let (done_tx, done_rx) = tokio::sync::oneshot::channel();
2136
2137        let server = tokio::spawn(async move {
2138            let (socket, _) = listener.accept().await.unwrap();
2139            let (read_half, initial_write_half) = socket.into_split();
2140            let mut initial_reader = BufReader::new(read_half);
2141            let mut auth = String::new();
2142            let mut subscription = String::new();
2143            initial_reader.read_line(&mut auth).await.unwrap();
2144            initial_reader.read_line(&mut subscription).await.unwrap();
2145            let auth: serde_json::Value = serde_json::from_str(&auth).unwrap();
2146            let subscription: serde_json::Value = serde_json::from_str(&subscription).unwrap();
2147            assert_eq!(auth["session"], "test-session");
2148            assert_eq!(subscription["op"], subscription_op);
2149            initial_tx.send(()).unwrap();
2150
2151            let (socket, _) = listener.accept().await.unwrap();
2152            let (read_half, replacement_write_half) = socket.into_split();
2153            let mut replacement_reader = BufReader::new(read_half);
2154            let mut replay_auth = String::new();
2155            let mut replay_subscription = String::new();
2156            replacement_reader
2157                .read_line(&mut replay_auth)
2158                .await
2159                .unwrap();
2160            replacement_reader
2161                .read_line(&mut replay_subscription)
2162                .await
2163                .unwrap();
2164            let replay_auth: serde_json::Value = serde_json::from_str(&replay_auth).unwrap();
2165            let replay_subscription: serde_json::Value =
2166                serde_json::from_str(&replay_subscription).unwrap();
2167            assert_eq!(replay_auth, auth);
2168            assert_eq!(replay_subscription, subscription);
2169            replacement_tx.send(()).unwrap();
2170
2171            let _initial_connection = (initial_reader, initial_write_half);
2172            let _replacement_connection = (replacement_reader, replacement_write_half);
2173            let _ = done_rx.await;
2174        });
2175
2176        let states = Arc::new(Mutex::new(Vec::new()));
2177        let states_sink = Arc::clone(&states);
2178        let state_sink = SocketStateSink::new(move |state| {
2179            states_sink.lock().push(state);
2180        });
2181        let credential = BetfairCredential::new(
2182            "testuser".to_string(),
2183            "testpass".to_string(),
2184            "test-app-key".to_string(),
2185        );
2186        let config = BetfairStreamConfig {
2187            host: "127.0.0.1".to_string(),
2188            port,
2189            heartbeat_secs: Some(5),
2190            heartbeat_timeout_secs: Some(60),
2191            reconnect_delay_initial_ms: 100,
2192            reconnect_delay_max_ms: 500,
2193            use_tls: false,
2194        };
2195        let (fatal_tx, _fatal_rx) = tokio::sync::mpsc::unbounded_channel();
2196        let client = if cricket {
2197            BetfairRaceStreamClient::connect_cricket_decoded(
2198                &credential,
2199                "test-session".to_string(),
2200                Arc::new(|_| {}),
2201                config,
2202                fatal_tx,
2203                Some(state_sink),
2204            )
2205            .await
2206            .unwrap()
2207        } else {
2208            BetfairRaceStreamClient::connect_decoded(
2209                &credential,
2210                "test-session".to_string(),
2211                Arc::new(|_| {}),
2212                config,
2213                fatal_tx,
2214                Some(state_sink),
2215            )
2216            .await
2217            .unwrap()
2218        };
2219
2220        initial_rx.await.unwrap();
2221        assert_eq!(
2222            client.request_reconnect_outcome(),
2223            ReconnectRequestOutcome::Accepted,
2224        );
2225        tokio::time::timeout(Duration::from_secs(5), replacement_rx)
2226            .await
2227            .unwrap()
2228            .unwrap();
2229        tokio::time::timeout(Duration::from_secs(5), async {
2230            while states.lock().len() < 3 {
2231                tokio::task::yield_now().await;
2232            }
2233        })
2234        .await
2235        .unwrap();
2236        client.close().await;
2237
2238        assert_eq!(
2239            *states.lock(),
2240            vec![
2241                SocketState::Connected,
2242                SocketState::Disconnected,
2243                SocketState::Connected,
2244            ],
2245        );
2246        assert_eq!(
2247            client.request_reconnect_outcome(),
2248            ReconnectRequestOutcome::Closed,
2249        );
2250
2251        let _ = done_tx.send(());
2252        server.await.unwrap();
2253    }
2254
2255    #[rstest]
2256    fn test_race_stream_reconnect_replays_auth_and_subscription() {
2257        let auth = Authentication::new("key".to_string(), "token".to_string());
2258        let auth_bytes = Bytes::from(serde_json::to_vec(&auth).unwrap());
2259        let race_sub = RaceSubscription::new(1);
2260        let race_sub_bytes = Bytes::from(serde_json::to_vec(&race_sub).unwrap());
2261
2262        let auth_reconnect = auth_bytes;
2263        let sub_reconnect = race_sub_bytes;
2264        let reconnect_replay: SocketReconnectReplay = Arc::new(move || {
2265            let mut combined = Vec::with_capacity(auth_reconnect.len() + 2 + sub_reconnect.len());
2266            combined.extend_from_slice(&auth_reconnect);
2267            combined.extend_from_slice(b"\r\n");
2268            combined.extend_from_slice(&sub_reconnect);
2269            vec![Bytes::from(combined)]
2270        });
2271
2272        let replay = reconnect_replay();
2273        let [bytes] = replay.as_slice() else {
2274            panic!("expected one combined replay message");
2275        };
2276
2277        let text = std::str::from_utf8(bytes).unwrap();
2278        let (auth_part, sub_part) = text
2279            .split_once("\r\n")
2280            .expect("CRLF separator in combined message");
2281
2282        assert!(auth_part.contains("\"op\":\"authentication\""));
2283        assert!(sub_part.contains("\"op\":\"raceSubscription\""));
2284    }
2285
2286    #[rstest]
2287    fn test_race_stream_handler_fatal_status_sends_kill_signal() {
2288        let (race_fatal_tx, mut race_fatal_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
2289        let inner_handler: TcpMessageHandler = Arc::new(|_data: &[u8]| {});
2290
2291        let handler: TcpMessageHandler = Arc::new(move |data: &[u8]| {
2292            if let Ok(StreamMessage::Status(status)) = stream_decode(data)
2293                && let Some(ref code) = status.error_code
2294                && code.is_race_stream_fatal()
2295            {
2296                let _ = race_fatal_tx.send(());
2297                return;
2298            }
2299            inner_handler(data);
2300        });
2301
2302        // Fatal: NOT_AUTHORIZED
2303        handler(
2304            br#"{"op":"status","statusCode":"503","errorCode":"NOT_AUTHORIZED","connectionClosed":true}"#,
2305        );
2306        assert!(
2307            race_fatal_rx.try_recv().is_ok(),
2308            "fatal error must send kill signal"
2309        );
2310
2311        // Non-fatal: INVALID_CLOCK
2312        handler(
2313            br#"{"op":"status","statusCode":"503","errorCode":"INVALID_CLOCK","connectionClosed":true}"#,
2314        );
2315        assert!(
2316            race_fatal_rx.try_recv().is_err(),
2317            "non-fatal error must not send kill signal"
2318        );
2319    }
2320}