nautilus_network/websocket/config.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//! Configuration for WebSocket client connections.
17//!
18//! # Reconnection Strategy
19//!
20//! The default configuration uses unlimited reconnection attempts (`reconnect_max_attempts: None`).
21//! This is intentional for trading systems because:
22//! - Venues may be down for extended periods but eventually recover.
23//! - Exponential backoff already prevents resource waste.
24//! - Automatic recovery can be useful when manual intervention is not desirable.
25//!
26//! Use `Some(n)` primarily for testing, development, or non-critical connections.
27
28use std::fmt::Debug;
29
30use serde::{Deserialize, Serialize};
31
32/// WebSocket transport backend selection.
33///
34/// Selection is runtime so multiple backends can compile side-by-side without
35/// a `compile_error!` collision under `--all-features`.
36///
37/// `Tungstenite` supports custom HTTP upgrade headers on the WebSocket
38/// handshake (see [`WebSocketConfig::headers`]). `Sockudo` is gated on the
39/// `transport-sockudo` Cargo feature and uses a local HTTP/1.1 handshake helper
40/// to pass the same upgrade headers through.
41#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum TransportBackend {
44 /// `tokio-tungstenite` backed transport (default).
45 #[default]
46 Tungstenite,
47 /// `sockudo-ws` backed transport (gated on `transport-sockudo` feature).
48 Sockudo,
49}
50
51/// Configuration for WebSocket client connections.
52///
53/// This struct contains only static configuration settings. Runtime callbacks
54/// (message handler, ping handler) are passed separately to `connect()`.
55///
56/// # Connection Modes
57///
58/// ## Handler Mode
59///
60/// - Use with [`crate::websocket::WebSocketClient::connect`].
61/// - Pass a message handler to `connect()` to receive messages via callback.
62/// - Client spawns internal task to read messages and call handler.
63/// - Supports automatic reconnection with exponential backoff.
64/// - Reconnection config fields (`reconnect_*`) are active.
65/// - Best for long-lived connections, Python bindings, callback-based APIs.
66///
67/// ## Stream Mode
68///
69/// - Use with [`crate::websocket::WebSocketClient::connect_stream`].
70/// - Returns a [`MessageReader`](super::types::MessageReader) stream for the caller to read from.
71/// - **Does NOT support automatic reconnection** (reader owned by caller).
72/// - Reconnection config fields are ignored.
73/// - On disconnect, client transitions to CLOSED state and caller must manually reconnect.
74#[cfg_attr(
75 feature = "python",
76 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.network", from_py_object)
77)]
78#[cfg_attr(
79 feature = "python",
80 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.network")
81)]
82#[allow(
83 clippy::unsafe_derive_deserialize,
84 reason = "PyO3-backed config still needs serde deserialization for strict config decoding"
85)]
86#[derive(Clone, Debug, Serialize, Deserialize, bon::Builder)]
87#[serde(deny_unknown_fields)]
88pub struct WebSocketConfig {
89 /// The URL to connect to.
90 pub url: String,
91 /// The default headers.
92 #[serde(default)]
93 #[builder(default)]
94 pub headers: Vec<(String, String)>,
95 /// The optional heartbeat interval (seconds).
96 #[serde(default)]
97 pub heartbeat: Option<u64>,
98 /// The optional heartbeat message.
99 #[serde(default)]
100 pub heartbeat_msg: Option<String>,
101 /// The timeout (milliseconds) for reconnection attempts.
102 /// **Note**: Only applies to handler mode. Ignored in stream mode.
103 #[serde(default)]
104 pub reconnect_timeout_ms: Option<u64>,
105 /// The initial reconnection delay (milliseconds) for reconnects.
106 /// **Note**: Only applies to handler mode. Ignored in stream mode.
107 #[serde(default)]
108 pub reconnect_delay_initial_ms: Option<u64>,
109 /// The maximum reconnect delay (milliseconds) for exponential backoff.
110 /// **Note**: Only applies to handler mode. Ignored in stream mode.
111 #[serde(default)]
112 pub reconnect_delay_max_ms: Option<u64>,
113 /// The exponential backoff factor for reconnection delays.
114 /// **Note**: Only applies to handler mode. Ignored in stream mode.
115 #[serde(default)]
116 pub reconnect_backoff_factor: Option<f64>,
117 /// The maximum jitter (milliseconds) added to reconnection delays.
118 /// **Note**: Only applies to handler mode. Ignored in stream mode.
119 #[serde(default)]
120 pub reconnect_jitter_ms: Option<u64>,
121 /// The maximum number of reconnection attempts before giving up.
122 /// **Note**: Only applies to handler mode. Ignored in stream mode.
123 /// - `None`: Unlimited reconnection attempts (default, recommended for production).
124 /// - `Some(n)`: After n failed attempts, transition to CLOSED state.
125 #[serde(default)]
126 pub reconnect_max_attempts: Option<u32>,
127 /// The idle timeout (milliseconds) for the read task.
128 /// When set, the read task will break and trigger reconnection if no data
129 /// is received within this duration. Useful for detecting silently dead
130 /// connections where the server stops sending without closing.
131 /// **Note**: Only applies to handler mode. Ignored in stream mode.
132 #[serde(default)]
133 pub idle_timeout_ms: Option<u64>,
134 /// The transport backend to use for the WebSocket connection.
135 ///
136 /// Defaults to [`TransportBackend::Tungstenite`]. Selecting
137 /// [`TransportBackend::Sockudo`] requires the `transport-sockudo` Cargo
138 /// feature; otherwise `connect_with_server` returns an error. Both backends
139 /// pass `headers` into the HTTP upgrade request.
140 #[serde(default)]
141 #[builder(default)]
142 pub backend: TransportBackend,
143 /// Optional forward proxy URL for the WebSocket connection.
144 ///
145 /// Routes the connection through an HTTP `CONNECT` tunnel. Accepts
146 /// `http://` and `https://` schemes; SOCKS schemes are not yet supported.
147 #[serde(default)]
148 pub proxy_url: Option<String>,
149}
150
151#[cfg(test)]
152mod tests {
153 use rstest::rstest;
154 use serde_json::json;
155
156 use super::WebSocketConfig;
157
158 #[rstest]
159 fn test_deserialize_websocket_config_rejects_unknown_field() {
160 let config = json!({
161 "url": "wss://example.com/ws",
162 "unexpected": true,
163 });
164
165 let error = serde_json::from_value::<WebSocketConfig>(config).unwrap_err();
166
167 assert!(error.to_string().contains("unknown field `unexpected`"));
168 }
169}