nautilus_coinapi/book/mod.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//! Order book synchronization and recovery for CoinAPI `book` and `book_l3` streams.
17//!
18//! - [`sync`] owns sequence validation, snapshot acceptance, and recovery state transitions.
19//! - `orders` keeps the resting orders an L3 stream changes by identifier.
20//! - `recovery` restarts a book's stream through the shared retry runner.
21//!
22//! CoinAPI numbers each (type, symbol) stream from one per connection and sends a snapshot when
23//! the pair enters the session scope. It replays nothing for a repeated scope, so recovery takes
24//! the pair out of the scope and restores it, and the venue stream delivers the snapshot.
25//! Fixed-depth `book5`, `book20`, and `book50` messages each list complete top levels, so they need
26//! no synchronization.
27
28pub mod sync;
29
30pub(crate) mod orders;
31pub(crate) mod recovery;
32
33use crate::websocket::error::Error as WsError;
34
35/// Error from a CoinAPI book recovery attempt, classified for the shared retry runner.
36#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
37pub(crate) enum CoinApiBookError {
38 /// A later attempt can resolve the failure.
39 #[error("{0}")]
40 Retryable(String),
41 /// Another attempt cannot resolve the failure.
42 #[error("{0}")]
43 Permanent(String),
44}
45
46impl CoinApiBookError {
47 pub(crate) fn is_retryable(&self) -> bool {
48 matches!(self, Self::Retryable(_))
49 }
50}
51
52impl From<WsError> for CoinApiBookError {
53 fn from(error: WsError) -> Self {
54 match error {
55 WsError::Validation(_) => Self::Permanent(error.to_string()),
56 WsError::Transport(_) | WsError::Send(_) => Self::Retryable(error.to_string()),
57 }
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use nautilus_network::{TransportError, error::SendError};
64 use rstest::rstest;
65
66 use super::*;
67
68 #[rstest]
69 #[case(
70 WsError::Validation("book pair is not subscribed".into()),
71 CoinApiBookError::Permanent(
72 "Invalid CoinAPI WebSocket request: book pair is not subscribed".into()
73 ),
74 false
75 )]
76 #[case(
77 WsError::Transport(TransportError::ConnectionReset),
78 CoinApiBookError::Retryable("connection reset".into()),
79 true
80 )]
81 #[case(
82 WsError::Send(SendError::BrokenPipe("writer closed".into())),
83 CoinApiBookError::Retryable("send failed: broken pipe (writer closed)".into()),
84 true
85 )]
86 fn websocket_errors_preserve_context_and_retry_classification(
87 #[case] error: WsError,
88 #[case] expected: CoinApiBookError,
89 #[case] retryable: bool,
90 ) {
91 let mapped = CoinApiBookError::from(error);
92
93 assert_eq!(mapped, expected);
94 assert_eq!(mapped.is_retryable(), retryable);
95 }
96}