1use std::collections::HashMap;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::time::Duration;
5
6use infinity_bridge_wire::{
7 AckPayload, BridgeError, CmdPayload, EventPayload, HelloPayload, WireMsg,
8};
9use serde_json::Value;
10use tokio::sync::{Mutex, broadcast, mpsc, oneshot, watch};
11use uuid::Uuid;
12
13use crate::client::Client;
14
15#[derive(Debug, Clone)]
16pub struct ClientInfo {
17 pub id: u64,
18 pub hello: Option<HelloPayload>,
19}
20
21pub(crate) struct Hub {
22 next_id: AtomicU64,
23 clients: Mutex<HashMap<u64, Client>>,
24 pending: Mutex<HashMap<String, oneshot::Sender<AckPayload>>>,
25
26 event_tx: broadcast::Sender<EventPayload>,
27
28 connection_tx: watch::Sender<bool>,
29 connection_rx: watch::Receiver<bool>,
30
31 connect_notify: tokio::sync::Notify,
32}
33
34impl Hub {
35 pub fn new(event_capacity: usize) -> Arc<Self> {
36 let (event_tx, _) = broadcast::channel(event_capacity);
37 let (connection_tx, connection_rx) = watch::channel(false);
38
39 Arc::new(Self {
40 next_id: AtomicU64::new(1),
41 clients: Mutex::new(HashMap::new()),
42 pending: Mutex::new(HashMap::new()),
43 event_tx,
44 connection_tx,
45 connection_rx,
46 connect_notify: tokio::sync::Notify::new(),
47 })
48 }
49
50 pub async fn register_client(&self, tx: mpsc::UnboundedSender<String>) -> u64 {
51 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
52 let mut clients = self.clients.lock().await;
53 clients.insert(
54 id,
55 Client {
56 tx,
57 hello: None,
58 last_seen: tokio::time::Instant::now(),
59 ready: false,
60 },
61 );
62 self.connection_tx.send_replace(true);
63 self.connect_notify.notify_waiters();
64 id
65 }
66
67 pub async fn set_client_hello(&self, id: u64, hello: HelloPayload) {
68 let mut clients = self.clients.lock().await;
69 if let Some(c) = clients.get_mut(&id) {
70 c.hello = Some(hello);
71 }
72 }
73
74 pub async fn unregister_client(&self, id: u64) {
75 let mut clients = self.clients.lock().await;
76 clients.remove(&id);
77 let connected = !clients.is_empty();
78 self.connection_tx.send_replace(connected);
79 }
80
81 pub async fn touch_client(&self, id: u64) {
82 let mut clients = self.clients.lock().await;
83 if let Some(c) = clients.get_mut(&id) {
84 c.last_seen = tokio::time::Instant::now();
85 }
86 }
87
88 pub async fn reap_dead_clients(&self, timeout: Duration) -> Vec<u64> {
89 let now = tokio::time::Instant::now();
90 let mut clients = self.clients.lock().await;
91 let dead: Vec<u64> = clients
92 .iter()
93 .filter(|(_, c)| now.duration_since(c.last_seen) > timeout)
94 .map(|(&id, _)| id)
95 .collect();
96
97 for &id in &dead {
98 clients.remove(&id);
99 }
100
101 if !dead.is_empty() {
102 let connected = !clients.is_empty();
103 self.connection_tx.send_replace(connected);
104 }
105
106 dead
107 }
108
109 pub async fn set_client_ready(&self, id: u64, ready: bool) {
112 let mut clients = self.clients.lock().await;
113 if let Some(c) = clients.get_mut(&id) {
114 c.ready = ready;
115 }
116 }
117
118 pub async fn is_connected(&self) -> bool {
119 !self.clients.lock().await.is_empty()
120 }
121
122 pub async fn is_ready(&self) -> bool {
128 self.clients.lock().await.values().any(|c| c.ready)
129 }
130
131 pub async fn wait_connected(&self) {
132 loop {
133 if self.is_connected().await {
134 return;
135 }
136 self.connect_notify.notified().await;
137 }
138 }
139
140 pub fn subscribe_connection_status(&self) -> watch::Receiver<bool> {
141 self.connection_rx.clone()
142 }
143
144 pub async fn connected_clients(&self) -> Vec<ClientInfo> {
145 let clients = self.clients.lock().await;
146 clients
147 .iter()
148 .map(|(&id, c)| ClientInfo {
149 id,
150 hello: c.hello.clone(),
151 })
152 .collect()
153 }
154
155 pub fn subscribe_events(&self) -> broadcast::Receiver<EventPayload> {
156 self.event_tx.subscribe()
157 }
158
159 pub fn dispatch_event(&self, event: EventPayload) {
160 let _ = self.event_tx.send(event);
161 }
162
163 pub async fn emit(&self, name: impl Into<String>, data: Value) -> Result<(), BridgeError> {
164 let msg = WireMsg::Event(EventPayload::new(name, data));
165 let json = msg.to_json()?;
166
167 let clients = self.clients.lock().await;
168 if clients.is_empty() {
169 return Err(BridgeError::no_clients(
170 "no gauges connected — event dropped",
171 ));
172 }
173
174 let mut send_failures = 0u32;
175 for client in clients.values() {
176 if client.tx.send(json.clone()).is_err() {
177 send_failures += 1;
178 }
179 }
180
181 if send_failures > 0 && send_failures as usize == clients.len() {
182 return Err(BridgeError::transport(
183 "all gauge connections failed to accept event",
184 ));
185 }
186
187 Ok(())
188 }
189
190 pub async fn command(
191 &self,
192 name: Option<&str>,
193 payload: Value,
194 timeout: Duration,
195 ) -> Result<Value, BridgeError> {
196 let id = Uuid::new_v4().to_string();
197
198 let cmd = match name {
199 Some(n) => CmdPayload::named(id.clone(), n, payload),
200 None => CmdPayload::new(id.clone(), payload),
201 };
202 let msg = WireMsg::Cmd(cmd);
203 let json = msg.to_json()?;
204
205 let (ack_tx, ack_rx) = oneshot::channel();
206 self.pending.lock().await.insert(id.clone(), ack_tx);
207
208 {
209 let clients = self.clients.lock().await;
210 if clients.is_empty() {
211 self.pending.lock().await.remove(&id);
212 return Err(BridgeError::no_clients(
213 "no gauges connected — cannot send command",
214 ));
215 }
216
217 let any_ready = clients.values().any(|c| c.ready);
223 let mut delivered = 0usize;
224 for client in clients.values() {
225 if any_ready && !client.ready {
226 continue;
227 }
228 if client.tx.send(json.clone()).is_ok() {
229 delivered += 1;
230 }
231 }
232
233 if delivered == 0 {
237 self.pending.lock().await.remove(&id);
238 return Err(BridgeError::transport(
239 "no gauge connection accepted the command",
240 ));
241 }
242 }
243
244 let ack_result = tokio::time::timeout(timeout, ack_rx).await;
245
246 self.pending.lock().await.remove(&id);
247
248 match ack_result {
249 Ok(Ok(ack)) => {
250 if ack.ok {
251 Ok(ack.response.unwrap_or(Value::Null))
252 } else {
253 Err(BridgeError::application(
254 ack.error.unwrap_or_else(|| "unknown error".into()),
255 ))
256 }
257 }
258 Ok(Err(_)) => Err(BridgeError::transport(
259 "all gauge connections dropped before ack",
260 )),
261 Err(_) => Err(BridgeError::timeout(format!(
262 "no ack received within {timeout:?}"
263 ))),
264 }
265 }
266
267 pub async fn dispatch_ack(&self, ack: AckPayload) {
268 let tx = self.pending.lock().await.remove(&ack.id);
269 if let Some(tx) = tx {
270 let _ = tx.send(ack);
271 }
272 }
273
274 pub async fn send_to(&self, client_id: u64, json: String) -> Result<(), BridgeError> {
275 let clients = self.clients.lock().await;
276 let client = clients
277 .get(&client_id)
278 .ok_or_else(|| BridgeError::transport(format!("client {client_id} not found")))?;
279 client
280 .tx
281 .send(json)
282 .map_err(|_| BridgeError::transport(format!("client {client_id} channel closed")))
283 }
284
285 pub async fn broadcast(&self, json: String) -> Result<(), BridgeError> {
286 let clients = self.clients.lock().await;
287 if clients.is_empty() {
288 return Err(BridgeError::no_clients("no gauges connected"));
289 }
290 for client in clients.values() {
291 let _ = client.tx.send(json.clone());
292 }
293 Ok(())
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use infinity_bridge_wire::ErrorKind;
301
302 const NO_ACK: Duration = Duration::from_millis(50);
305
306 #[tokio::test]
307 async fn readiness_is_tracked_per_client() {
308 let hub = Hub::new(16);
309 let (tx_a, _rx_a) = mpsc::unbounded_channel();
310 let (tx_b, _rx_b) = mpsc::unbounded_channel();
311 let a = hub.register_client(tx_a).await;
312 let _b = hub.register_client(tx_b).await;
313
314 assert!(hub.is_connected().await);
315 assert!(!hub.is_ready().await, "a fresh client is not yet ready");
316
317 hub.set_client_ready(a, true).await;
318 assert!(hub.is_ready().await);
319
320 hub.set_client_ready(a, false).await;
321 assert!(!hub.is_ready().await);
322 assert!(hub.is_connected().await, "readiness is not connectedness");
323 }
324
325 #[tokio::test]
326 async fn a_command_skips_clients_that_are_not_ready() {
327 let hub = Hub::new(16);
328 let (tx_stale, mut rx_stale) = mpsc::unbounded_channel();
329 let (tx_live, mut rx_live) = mpsc::unbounded_channel();
330 let _stale = hub.register_client(tx_stale).await;
331 let live = hub.register_client(tx_live).await;
332 hub.set_client_ready(live, true).await;
333
334 let err = hub
335 .command(Some("ping"), Value::Null, NO_ACK)
336 .await
337 .expect_err("nothing acks in this test");
338 assert_eq!(err.kind(), ErrorKind::Timeout);
339
340 assert!(
341 rx_live.try_recv().is_ok(),
342 "the ready client got the command"
343 );
344 assert!(
345 rx_stale.try_recv().is_err(),
346 "a relay that never reported ready must not absorb the timeout budget"
347 );
348 }
349
350 #[tokio::test]
351 async fn a_command_goes_everywhere_while_readiness_is_unknown() {
352 let hub = Hub::new(16);
355 let (tx_a, mut rx_a) = mpsc::unbounded_channel();
356 let (tx_b, mut rx_b) = mpsc::unbounded_channel();
357 hub.register_client(tx_a).await;
358 hub.register_client(tx_b).await;
359
360 let _ = hub.command(Some("ping"), Value::Null, NO_ACK).await;
361
362 assert!(rx_a.try_recv().is_ok());
363 assert!(rx_b.try_recv().is_ok());
364 }
365
366 #[tokio::test]
367 async fn a_command_fails_fast_when_every_connection_is_gone() {
368 let hub = Hub::new(16);
372 let (tx, rx) = mpsc::unbounded_channel();
373 hub.register_client(tx).await;
374 drop(rx);
375
376 let start = tokio::time::Instant::now();
377 let err = hub
378 .command(Some("ping"), Value::Null, Duration::from_secs(30))
379 .await
380 .expect_err("the only receiver is gone");
381 assert_eq!(err.kind(), ErrorKind::Transport);
382 assert!(
383 start.elapsed() < Duration::from_secs(1),
384 "should not have waited out the timeout"
385 );
386 }
387
388 #[tokio::test]
389 async fn a_timed_out_command_leaves_no_pending_entry() {
390 let hub = Hub::new(16);
391 let (tx, _rx) = mpsc::unbounded_channel();
392 hub.register_client(tx).await;
393
394 let _ = hub.command(Some("ping"), Value::Null, NO_ACK).await;
395 assert!(hub.pending.lock().await.is_empty());
396 }
397
398 #[tokio::test(start_paused = true)]
399 async fn a_silent_client_is_reaped_and_named() {
400 let hub = Hub::new(16);
401 let (tx, _rx) = mpsc::unbounded_channel();
402 let id = hub.register_client(tx).await;
403
404 tokio::time::advance(Duration::from_secs(31)).await;
405 let dead = hub.reap_dead_clients(Duration::from_secs(30)).await;
406
407 assert_eq!(dead, vec![id], "the reaper names who it dropped");
408 assert!(!hub.is_connected().await);
409 }
410}