bbx_net/websocket/
connection.rs

1//! WebSocket connection state management.
2
3use std::time::Instant;
4
5use crate::address::NodeId;
6
7/// Connection state for a single WebSocket client.
8#[derive(Debug, Clone)]
9pub struct ConnectionState {
10    /// Unique identifier for this connection.
11    pub node_id: NodeId,
12    /// Room code the client joined.
13    pub room_code: String,
14    /// Client-provided name (optional).
15    pub client_name: Option<String>,
16    /// When the connection was established.
17    pub connected_at: Instant,
18    /// When the last ping/pong was received.
19    pub last_ping: Instant,
20    /// Measured round-trip latency in microseconds.
21    pub latency_us: u64,
22    /// Number of reconnections for this node.
23    pub reconnect_count: u32,
24    /// Clock offset in microseconds (positive = client ahead).
25    pub clock_offset: i64,
26}
27
28impl ConnectionState {
29    /// Create a new connection state.
30    pub fn new(node_id: NodeId, room_code: String, client_name: Option<String>) -> Self {
31        let now = Instant::now();
32        Self {
33            node_id,
34            room_code,
35            client_name,
36            connected_at: now,
37            last_ping: now,
38            latency_us: 0,
39            reconnect_count: 0,
40            clock_offset: 0,
41        }
42    }
43
44    /// Update latency from ping/pong exchange.
45    ///
46    /// # Arguments
47    ///
48    /// * `rtt_us` - Round-trip time in microseconds
49    pub fn update_latency(&mut self, rtt_us: u64) {
50        self.latency_us = rtt_us / 2;
51        self.last_ping = Instant::now();
52    }
53
54    /// Update clock offset from ping/pong exchange.
55    ///
56    /// Uses NTP-style calculation to determine the difference between
57    /// client and server clocks.
58    pub fn update_clock_offset(
59        &mut self,
60        client_send: u64,
61        server_receive: u64,
62        server_send: u64,
63        client_receive: u64,
64    ) {
65        let t1 = client_send as i64;
66        let t2 = server_receive as i64;
67        let t3 = server_send as i64;
68        let t4 = client_receive as i64;
69
70        self.clock_offset = ((t2 - t1) + (t3 - t4)) / 2;
71        self.latency_us = ((t4 - t1) - (t3 - t2)) as u64 / 2;
72        self.last_ping = Instant::now();
73    }
74
75    /// Check if the connection should be considered stale.
76    pub fn is_stale(&self, timeout: std::time::Duration) -> bool {
77        Instant::now().duration_since(self.last_ping) > timeout
78    }
79
80    /// Mark as reconnected, incrementing the counter.
81    pub fn mark_reconnected(&mut self) {
82        self.reconnect_count += 1;
83        self.last_ping = Instant::now();
84    }
85
86    /// Convert a client timestamp to server time using the clock offset.
87    pub fn client_to_server_time(&self, client_time: u64) -> u64 {
88        (client_time as i64 - self.clock_offset) as u64
89    }
90
91    /// Convert a server timestamp to client time using the clock offset.
92    pub fn server_to_client_time(&self, server_time: u64) -> u64 {
93        (server_time as i64 + self.clock_offset) as u64
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_connection_state_new() {
103        let node_id = NodeId::from_parts(1, 2);
104        let state = ConnectionState::new(node_id, "123456".to_string(), None);
105
106        assert_eq!(state.node_id, node_id);
107        assert_eq!(state.room_code, "123456");
108        assert_eq!(state.reconnect_count, 0);
109        assert_eq!(state.latency_us, 0);
110    }
111
112    #[test]
113    fn test_update_latency() {
114        let node_id = NodeId::from_parts(1, 2);
115        let mut state = ConnectionState::new(node_id, "123456".to_string(), None);
116
117        state.update_latency(20000);
118
119        assert_eq!(state.latency_us, 10000);
120    }
121
122    #[test]
123    fn test_update_clock_offset() {
124        let node_id = NodeId::from_parts(1, 2);
125        let mut state = ConnectionState::new(node_id, "123456".to_string(), None);
126
127        state.update_clock_offset(100, 200, 200, 300);
128
129        assert_eq!(state.clock_offset, 0);
130    }
131
132    #[test]
133    fn test_clock_offset_client_ahead() {
134        let node_id = NodeId::from_parts(1, 2);
135        let mut state = ConnectionState::new(node_id, "123456".to_string(), None);
136
137        state.update_clock_offset(200, 100, 100, 200);
138
139        assert!(state.clock_offset < 0);
140    }
141
142    #[test]
143    fn test_mark_reconnected() {
144        let node_id = NodeId::from_parts(1, 2);
145        let mut state = ConnectionState::new(node_id, "123456".to_string(), None);
146
147        state.mark_reconnected();
148        state.mark_reconnected();
149
150        assert_eq!(state.reconnect_count, 2);
151    }
152
153    #[test]
154    fn test_time_conversion() {
155        let node_id = NodeId::from_parts(1, 2);
156        let mut state = ConnectionState::new(node_id, "123456".to_string(), None);
157
158        state.clock_offset = 100;
159
160        let server_time = state.client_to_server_time(1000);
161        assert_eq!(server_time, 900);
162
163        let client_time = state.server_to_client_time(900);
164        assert_eq!(client_time, 1000);
165    }
166
167    #[test]
168    fn test_is_stale() {
169        use std::time::Duration;
170
171        let node_id = NodeId::from_parts(1, 2);
172        let state = ConnectionState::new(node_id, "123456".to_string(), None);
173
174        assert!(!state.is_stale(Duration::from_secs(60)));
175    }
176}