infinity_bridge_host/
hub.rs1use std::collections::HashMap;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::time::Duration;
5
6use infinity_bridge_wire::{AckPayload, BridgeError, CmdPayload, EventPayload, HelloPayload, WireMsg};
7use serde_json::Value;
8use tokio::sync::{Mutex, broadcast, mpsc, oneshot, watch};
9use uuid::Uuid;
10
11use crate::client::Client;
12
13#[derive(Debug, Clone)]
14pub struct ClientInfo {
15 pub id: u64,
16 pub hello: Option<HelloPayload>,
17}
18
19pub(crate) struct Hub {
20 next_id: AtomicU64,
21 clients: Mutex<HashMap<u64, Client>>,
22 pending: Mutex<HashMap<String, oneshot::Sender<AckPayload>>>,
23
24 event_tx: broadcast::Sender<EventPayload>,
25
26 connection_tx: watch::Sender<bool>,
27 connection_rx: watch::Receiver<bool>,
28
29 connect_notify: tokio::sync::Notify,
30}
31
32impl Hub {
33 pub fn new(event_capacity: usize) -> Arc<Self> {
34 let (event_tx, _) = broadcast::channel(event_capacity);
35 let (connection_tx, connection_rx) = watch::channel(false);
36
37 Arc::new(Self {
38 next_id: AtomicU64::new(1),
39 clients: Mutex::new(HashMap::new()),
40 pending: Mutex::new(HashMap::new()),
41 event_tx,
42 connection_tx,
43 connection_rx,
44 connect_notify: tokio::sync::Notify::new(),
45 })
46 }
47
48 pub async fn register_client(&self, tx: mpsc::UnboundedSender<String>) -> u64 {
49 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
50 let mut clients = self.clients.lock().await;
51 clients.insert(
52 id,
53 Client {
54 tx,
55 hello: None,
56 last_seen: tokio::time::Instant::now(),
57 },
58 );
59 self.connection_tx.send_replace(true);
60 self.connect_notify.notify_waiters();
61 id
62 }
63
64 pub async fn set_client_hello(&self, id: u64, hello: HelloPayload) {
65 let mut clients = self.clients.lock().await;
66 if let Some(c) = clients.get_mut(&id) {
67 c.hello = Some(hello);
68 }
69 }
70
71 pub async fn unregister_client(&self, id: u64) {
72 let mut clients = self.clients.lock().await;
73 clients.remove(&id);
74 let connected = !clients.is_empty();
75 self.connection_tx.send_replace(connected);
76 }
77
78 pub async fn touch_client(&self, id: u64) {
79 let mut clients = self.clients.lock().await;
80 if let Some(c) = clients.get_mut(&id) {
81 c.last_seen = tokio::time::Instant::now();
82 }
83 }
84
85 pub async fn reap_dead_clients(&self, timeout: Duration) -> Vec<u64> {
86 let now = tokio::time::Instant::now();
87 let mut clients = self.clients.lock().await;
88 let dead: Vec<u64> = clients
89 .iter()
90 .filter(|(_, c)| now.duration_since(c.last_seen) > timeout)
91 .map(|(&id, _)| id)
92 .collect();
93
94 for &id in &dead {
95 clients.remove(&id);
96 }
97
98 if !dead.is_empty() {
99 let connected = !clients.is_empty();
100 self.connection_tx.send_replace(connected);
101 }
102
103 dead
104 }
105
106 pub async fn is_connected(&self) -> bool {
107 !self.clients.lock().await.is_empty()
108 }
109
110 pub async fn wait_connected(&self) {
111 loop {
112 if self.is_connected().await {
113 return;
114 }
115 self.connect_notify.notified().await;
116 }
117 }
118
119 pub fn subscribe_connection_status(&self) -> watch::Receiver<bool> {
120 self.connection_rx.clone()
121 }
122
123 pub async fn connected_clients(&self) -> Vec<ClientInfo> {
124 let clients = self.clients.lock().await;
125 clients
126 .iter()
127 .map(|(&id, c)| ClientInfo {
128 id,
129 hello: c.hello.clone(),
130 })
131 .collect()
132 }
133
134 pub fn subscribe_events(&self) -> broadcast::Receiver<EventPayload> {
135 self.event_tx.subscribe()
136 }
137
138 pub fn dispatch_event(&self, event: EventPayload) {
139 let _ = self.event_tx.send(event);
140 }
141
142 pub async fn emit(&self, name: impl Into<String>, data: Value) -> Result<(), BridgeError> {
143 let msg = WireMsg::Event(EventPayload::new(name, data));
144 let json = msg.to_json()?;
145
146 let clients = self.clients.lock().await;
147 if clients.is_empty() {
148 return Err(BridgeError::no_clients(
149 "no gauges connected — event dropped",
150 ));
151 }
152
153 let mut send_failures = 0u32;
154 for client in clients.values() {
155 if client.tx.send(json.clone()).is_err() {
156 send_failures += 1;
157 }
158 }
159
160 if send_failures > 0 && send_failures as usize == clients.len() {
161 return Err(BridgeError::transport(
162 "all gauge connections failed to accept event",
163 ));
164 }
165
166 Ok(())
167 }
168
169 pub async fn command(
170 &self,
171 name: Option<&str>,
172 payload: Value,
173 timeout: Duration,
174 ) -> Result<Value, BridgeError> {
175 let id = Uuid::new_v4().to_string();
176
177 let cmd = match name {
178 Some(n) => CmdPayload::named(id.clone(), n, payload),
179 None => CmdPayload::new(id.clone(), payload),
180 };
181 let msg = WireMsg::Cmd(cmd);
182 let json = msg.to_json()?;
183
184 let (ack_tx, ack_rx) = oneshot::channel();
185 self.pending.lock().await.insert(id.clone(), ack_tx);
186
187 {
188 let clients = self.clients.lock().await;
189 if clients.is_empty() {
190 self.pending.lock().await.remove(&id);
191 return Err(BridgeError::no_clients(
192 "no gauges connected — cannot send command",
193 ));
194 }
195 for client in clients.values() {
196 let _ = client.tx.send(json.clone());
197 }
198 }
199
200 let ack_result = tokio::time::timeout(timeout, ack_rx).await;
201
202 self.pending.lock().await.remove(&id);
203
204 match ack_result {
205 Ok(Ok(ack)) => {
206 if ack.ok {
207 Ok(ack.response.unwrap_or(Value::Null))
208 } else {
209 Err(BridgeError::application(
210 ack.error.unwrap_or_else(|| "unknown error".into()),
211 ))
212 }
213 }
214 Ok(Err(_)) => Err(BridgeError::transport(
215 "all gauge connections dropped before ack",
216 )),
217 Err(_) => Err(BridgeError::timeout(format!(
218 "no ack received within {timeout:?}"
219 ))),
220 }
221 }
222
223 pub async fn dispatch_ack(&self, ack: AckPayload) {
224 let tx = self.pending.lock().await.remove(&ack.id);
225 if let Some(tx) = tx {
226 let _ = tx.send(ack);
227 }
228 }
229
230 pub async fn send_to(&self, client_id: u64, json: String) -> Result<(), BridgeError> {
231 let clients = self.clients.lock().await;
232 let client = clients
233 .get(&client_id)
234 .ok_or_else(|| BridgeError::transport(format!("client {client_id} not found")))?;
235 client
236 .tx
237 .send(json)
238 .map_err(|_| BridgeError::transport(format!("client {client_id} channel closed")))
239 }
240
241 pub async fn broadcast(&self, json: String) -> Result<(), BridgeError> {
242 let clients = self.clients.lock().await;
243 if clients.is_empty() {
244 return Err(BridgeError::no_clients("no gauges connected"));
245 }
246 for client in clients.values() {
247 let _ = client.tx.send(json.clone());
248 }
249 Ok(())
250 }
251}