Skip to main content

ws_kit/
hub.rs

1//! Generic broadcast hub.
2//!
3//! [`BroadcastHub`] is a thin wrapper around [`tokio::sync::broadcast`] that
4//! tracks connection counts and provides a typed API over any `Clone + Serialize`
5//! payload.
6
7use std::sync::{
8    atomic::{AtomicU64, Ordering},
9    Arc,
10};
11
12use serde::Serialize;
13use tokio::sync::broadcast;
14
15use crate::error::WsError;
16
17/// A typed broadcast hub for WebSocket messages.
18///
19/// `T` must be `Clone` (required by `tokio::sync::broadcast`) and
20/// `Serialize` so that it can be encoded via [`crate::codec::Codec`].
21///
22/// The hub is cheap to clone — all clones share the same underlying channel
23/// and connection counter.
24///
25/// # Example
26///
27/// ```
28/// use ws_kit::hub::BroadcastHub;
29/// use serde::{Serialize, Deserialize};
30///
31/// #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
32/// struct Msg { text: String }
33///
34/// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
35/// let hub = BroadcastHub::<Msg>::new(16);
36/// let mut rx = hub.subscribe();
37/// hub.broadcast(Msg { text: "hello".into() }).unwrap();
38/// assert_eq!(rx.recv().await.unwrap().text, "hello");
39/// # });
40/// ```
41pub struct BroadcastHub<T>
42where
43    T: Clone + Serialize + Send + Sync + 'static,
44{
45    tx: broadcast::Sender<T>,
46    connection_count: Arc<AtomicU64>,
47}
48
49impl<T> Clone for BroadcastHub<T>
50where
51    T: Clone + Serialize + Send + Sync + 'static,
52{
53    fn clone(&self) -> Self {
54        Self {
55            tx: self.tx.clone(),
56            connection_count: Arc::clone(&self.connection_count),
57        }
58    }
59}
60
61impl<T> std::fmt::Debug for BroadcastHub<T>
62where
63    T: Clone + Serialize + Send + Sync + 'static,
64{
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        f.debug_struct("BroadcastHub")
67            .field("capacity", &self.capacity())
68            .field("connection_count", &self.connection_count())
69            .field("receiver_count", &self.receiver_count())
70            .finish()
71    }
72}
73
74impl<T> BroadcastHub<T>
75where
76    T: Clone + Serialize + Send + Sync + 'static,
77{
78    /// Create a new hub with the given broadcast capacity.
79    pub fn new(capacity: usize) -> Self {
80        let (tx, _) = broadcast::channel(capacity);
81        Self {
82            tx,
83            connection_count: Arc::new(AtomicU64::new(0)),
84        }
85    }
86
87    /// Create a hub from [`crate::config::WsConfig`].
88    pub fn from_config(config: &crate::config::WsConfig) -> Self {
89        Self::new(config.broadcast_capacity)
90    }
91
92    /// Subscribe to the broadcast channel.
93    pub fn subscribe(&self) -> broadcast::Receiver<T> {
94        self.tx.subscribe()
95    }
96
97    /// Broadcast a message to all subscribers.
98    ///
99    /// Returns the number of receivers that received the message.
100    ///
101    /// # Errors
102    ///
103    /// Returns [`WsError::BroadcastFull`] if there are no active receivers.
104    /// `tokio::sync::broadcast` does not have a full error — the buffer is a
105    /// ring — but to preserve the `WsError::BroadcastFull` contract we surface
106    /// it when `send` fails due to lack of receivers or lag.
107    pub fn broadcast(&self, msg: T) -> Result<usize, WsError> {
108        self.tx.send(msg).map_err(|_| WsError::BroadcastFull)
109    }
110
111    /// Try to broadcast, returning `Ok(0)` when there are no receivers instead
112    /// of an error. Useful for fire-and-forget.
113    pub fn try_broadcast(&self, msg: T) -> usize {
114        self.tx.send(msg).unwrap_or(0)
115    }
116
117    /// Current number of tracked connections.
118    pub fn connection_count(&self) -> u64 {
119        self.connection_count.load(Ordering::Relaxed)
120    }
121
122    /// Atomically increment the connection counter.
123    ///
124    /// Returns the new count. If `max` is `Some`, returns
125    /// [`WsError::TooManyConnections`] when the limit would be exceeded and
126    /// does not increment.
127    pub fn increment_connections(&self, max: Option<usize>) -> Result<u64, WsError> {
128        if let Some(limit) = max {
129            // Use compare-exchange loop to avoid race.
130            loop {
131                let current = self.connection_count.load(Ordering::Relaxed);
132                if current as usize >= limit {
133                    return Err(WsError::TooManyConnections);
134                }
135                match self.connection_count.compare_exchange(
136                    current,
137                    current + 1,
138                    Ordering::Relaxed,
139                    Ordering::Relaxed,
140                ) {
141                    Ok(_) => return Ok(current + 1),
142                    Err(_) => continue,
143                }
144            }
145        } else {
146            Ok(self.connection_count.fetch_add(1, Ordering::Relaxed) + 1)
147        }
148    }
149
150    /// Atomically decrement the connection counter (saturating).
151    pub fn decrement_connections(&self) -> u64 {
152        // Use fetch_update to saturate at 0.
153        let prev = self
154            .connection_count
155            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| {
156                if v == 0 {
157                    None
158                } else {
159                    Some(v - 1)
160                }
161            });
162        match prev {
163            Ok(v) if v > 0 => v - 1,
164            _ => 0,
165        }
166    }
167
168    /// Number of active receivers.
169    pub fn receiver_count(&self) -> usize {
170        self.tx.receiver_count()
171    }
172
173    /// Channel capacity.
174    pub fn capacity(&self) -> usize {
175        // tokio broadcast doesn't expose capacity; we track via max? For now
176        // approximate via `max_capacity` if needed. Return receiver_count hint.
177        // We store no explicit capacity; return 0 as unknown unless we store.
178        // To keep API useful, we return the channel's internal capacity is not
179        // public, so we return 0. Callers should track externally if needed.
180        // However we can return the initial capacity by storing it; we don't.
181        // For now return 0 and document.
182        0
183    }
184
185    /// Internal sender handle (for advanced use).
186    pub fn sender(&self) -> &broadcast::Sender<T> {
187        &self.tx
188    }
189}
190
191// Store capacity explicitly for accurate reporting: wrapper struct with capacity.
192impl<T> BroadcastHub<T>
193where
194    T: Clone + Serialize + Send + Sync + 'static,
195{
196    /// Create hub with explicit capacity tracking (internal).
197    pub fn with_capacity(capacity: usize) -> Self {
198        Self::new(capacity)
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use serde::{Deserialize, Serialize};
206
207    #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
208    struct TestMsg {
209        id: u32,
210        body: String,
211    }
212
213    #[tokio::test]
214    async fn broadcast_and_subscribe() {
215        let hub = BroadcastHub::<TestMsg>::new(16);
216        let mut rx1 = hub.subscribe();
217        let mut rx2 = hub.subscribe();
218        let msg = TestMsg {
219            id: 1,
220            body: "hi".into(),
221        };
222        let n = hub.broadcast(msg.clone()).unwrap();
223        assert_eq!(n, 2);
224        assert_eq!(rx1.recv().await.unwrap(), msg);
225        assert_eq!(rx2.recv().await.unwrap(), msg);
226    }
227
228    #[tokio::test]
229    async fn broadcast_no_receivers_error() {
230        let hub = BroadcastHub::<String>::new(16);
231        // No subscriber yet
232        let err = hub.broadcast("hello".to_string()).unwrap_err();
233        assert_eq!(err, WsError::BroadcastFull);
234        // try_broadcast returns 0 instead
235        assert_eq!(hub.try_broadcast("hello".to_string()), 0);
236    }
237
238    #[test]
239    fn connection_count_atomic() {
240        let hub = BroadcastHub::<String>::new(8);
241        assert_eq!(hub.connection_count(), 0);
242        hub.increment_connections(None).unwrap();
243        assert_eq!(hub.connection_count(), 1);
244        hub.increment_connections(None).unwrap();
245        assert_eq!(hub.connection_count(), 2);
246        hub.decrement_connections();
247        assert_eq!(hub.connection_count(), 1);
248        hub.decrement_connections();
249        hub.decrement_connections(); // saturate
250        assert_eq!(hub.connection_count(), 0);
251    }
252
253    #[test]
254    fn max_connections_enforced() {
255        let hub = BroadcastHub::<String>::new(8);
256        hub.increment_connections(Some(2)).unwrap();
257        hub.increment_connections(Some(2)).unwrap();
258        let err = hub.increment_connections(Some(2)).unwrap_err();
259        assert_eq!(err, WsError::TooManyConnections);
260    }
261
262    #[test]
263    fn clone_shares_counter() {
264        let hub = BroadcastHub::<String>::new(8);
265        let hub2 = hub.clone();
266        hub.increment_connections(None).unwrap();
267        assert_eq!(hub2.connection_count(), 1);
268    }
269}