Skip to main content

infinity_bridge_host/
server.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use axum::Router;
5use axum::extract::State;
6use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
7use axum::response::IntoResponse;
8use axum::routing::get;
9use futures::{SinkExt, StreamExt};
10use infinity_bridge_wire::{BridgeError, EventPayload, WireMsg};
11use serde_json::Value;
12use tokio::sync::{broadcast, mpsc, watch};
13
14use crate::hub::{ClientInfo, Hub};
15
16pub struct ServerConfig {
17    pub bind_addr: String,
18    pub ws_path: String,
19    pub event_capacity: usize,
20    pub ping_interval: Duration,
21    pub ping_timeout: Duration,
22}
23
24impl ServerConfig {
25    pub fn new(bind_addr: impl Into<String>, ws_path: impl Into<String>) -> Self {
26        Self {
27            bind_addr: bind_addr.into(),
28            ws_path: ws_path.into(),
29            event_capacity: 256,
30            ping_interval: Duration::from_secs(5),
31            ping_timeout: Duration::from_secs(15),
32        }
33    }
34}
35
36#[derive(Clone)]
37pub struct BridgeServer {
38    hub: Arc<Hub>,
39    config: Arc<ServerConfig>,
40}
41
42impl BridgeServer {
43    /// Start the bridge server and begin accepting connections.
44    ///
45    /// This spawns a tokio task running the axum HTTP server.
46    /// The returned `BridgeServer` handle can be used to interact with
47    /// connected gauges.
48    pub async fn start(config: ServerConfig) -> Result<Self, BridgeError> {
49        let hub = Hub::new(config.event_capacity);
50        let config = Arc::new(config);
51
52        let server = Self {
53            hub: Arc::clone(&hub),
54            config: Arc::clone(&config),
55        };
56
57        let app_state = AppState {
58            hub: Arc::clone(&hub),
59            config: Arc::clone(&config),
60        };
61
62        let app = Router::new()
63            .route(&config.ws_path, get(ws_upgrade))
64            .route("/health", get(health))
65            .with_state(app_state);
66
67        let listener = tokio::net::TcpListener::bind(&config.bind_addr)
68            .await
69            .map_err(|e| BridgeError::transport(format!("bind failed: {e}")))?;
70
71        tokio::spawn(async move {
72            if let Err(e) = axum::serve(listener, app).await {
73                eprintln!("[infinity-bridge-host] Server error: {e}");
74            }
75        });
76
77        Ok(server)
78    }
79
80    /// Build an axum [`Router`] without starting a listener.
81    ///
82    /// Useful when you want to mount the bridge as a nested route
83    /// inside an existing axum application.
84    pub fn router(config: ServerConfig) -> (Self, Router) {
85        let hub = Hub::new(config.event_capacity);
86        let config = Arc::new(config);
87
88        let server = Self {
89            hub: Arc::clone(&hub),
90            config: Arc::clone(&config),
91        };
92
93        let app_state = AppState {
94            hub: Arc::clone(&hub),
95            config: Arc::clone(&config),
96        };
97
98        let router = Router::new()
99            .route(&config.ws_path, get(ws_upgrade))
100            .route("/health", get(health))
101            .with_state(app_state);
102
103        (server, router)
104    }
105
106    // ── Commands ─────────────────────────────────────────────────────
107
108    /// Send a named command to all connected gauges and await the first ack.
109    pub async fn command(
110        &self,
111        name: &str,
112        payload: Value,
113        timeout: Duration,
114    ) -> Result<Value, BridgeError> {
115        self.hub.command(Some(name), payload, timeout).await
116    }
117
118    /// Send an unnamed command (payload-only) and await the first ack.
119    pub async fn command_raw(
120        &self,
121        payload: Value,
122        timeout: Duration,
123    ) -> Result<Value, BridgeError> {
124        self.hub.command(None, payload, timeout).await
125    }
126
127    // ── Events ───────────────────────────────────────────────────────
128
129    /// Send a fire-and-forget event to all connected gauges.
130    ///
131    /// Returns `Ok(())` if the event was dispatched to at least one gauge.
132    /// Returns `Err(NoClients)` if no gauges are connected.
133    pub async fn emit(&self, name: impl Into<String>, data: Value) -> Result<(), BridgeError> {
134        self.hub.emit(name, data).await
135    }
136
137    /// Subscribe to events received from gauges.
138    ///
139    /// Returns a broadcast receiver. Events are delivered as they arrive
140    /// from any connected gauge. If the receiver falls behind by
141    /// `event_capacity` messages, older events are dropped.
142    pub fn subscribe_events(&self) -> broadcast::Receiver<EventPayload> {
143        self.hub.subscribe_events()
144    }
145
146    // ── Connection status ────────────────────────────────────────────
147
148    /// Returns `true` if at least one gauge is connected.
149    pub async fn is_connected(&self) -> bool {
150        self.hub.is_connected().await
151    }
152
153    /// Wait until at least one gauge connects.
154    pub async fn wait_connected(&self) {
155        self.hub.wait_connected().await
156    }
157
158    /// Get a watch channel that tracks connection status.
159    ///
160    /// The value is `true` when at least one gauge is connected,
161    /// `false` when all gauges have disconnected.
162    pub fn connection_status(&self) -> watch::Receiver<bool> {
163        self.hub.subscribe_connection_status()
164    }
165
166    /// List all currently connected clients.
167    pub async fn clients(&self) -> Vec<ClientInfo> {
168        self.hub.connected_clients().await
169    }
170}
171
172#[derive(Clone)]
173struct AppState {
174    hub: Arc<Hub>,
175    config: Arc<ServerConfig>,
176}
177
178async fn health() -> impl IntoResponse {
179    "ok"
180}
181
182async fn ws_upgrade(ws: WebSocketUpgrade, State(state): State<AppState>) -> impl IntoResponse {
183    ws.on_upgrade(move |socket| handle_socket(socket, state))
184}
185
186async fn handle_socket(socket: WebSocket, state: AppState) {
187    let (mut ws_tx, mut ws_rx) = socket.split();
188    let (out_tx, mut out_rx) = mpsc::unbounded_channel::<String>();
189
190    let client_id = state.hub.register_client(out_tx.clone()).await;
191
192    let writer = tokio::spawn(async move {
193        while let Some(text) = out_rx.recv().await {
194            if ws_tx.send(Message::Text(text.into())).await.is_err() {
195                break;
196            }
197        }
198    });
199
200    let hub_for_ping = Arc::clone(&state.hub);
201    let ping_interval = state.config.ping_interval;
202    let ping_timeout = state.config.ping_timeout;
203    let ping_task = tokio::spawn(async move {
204        let mut interval = tokio::time::interval(ping_interval);
205        interval.tick().await;
206        loop {
207            interval.tick().await;
208
209            {
210                let dead = hub_for_ping.reap_dead_clients(ping_timeout).await;
211                if dead.contains(&client_id) {
212                    break;
213                }
214            }
215
216            let ping = WireMsg::Ping {
217                ts: Some(
218                    std::time::SystemTime::now()
219                        .duration_since(std::time::UNIX_EPOCH)
220                        .unwrap_or_default()
221                        .as_millis() as u64,
222                ),
223            };
224            let json = match ping.to_json() {
225                Ok(j) => j,
226                Err(_) => continue,
227            };
228            if hub_for_ping.send_to(client_id, json).await.is_err() {
229                break;
230            }
231        }
232    });
233
234    while let Some(Ok(msg)) = ws_rx.next().await {
235        match msg {
236            Message::Text(text) => {
237                state.hub.touch_client(client_id).await;
238
239                let wire = match WireMsg::from_json(&text) {
240                    Ok(w) => w,
241                    Err(_) => continue,
242                };
243
244                match wire {
245                    WireMsg::Hello(hello) => {
246                        state.hub.set_client_hello(client_id, hello).await;
247                    }
248                    WireMsg::Ack(ack) => {
249                        state.hub.dispatch_ack(ack).await;
250                    }
251                    WireMsg::Event(event) => {
252                        state.hub.dispatch_event(event);
253                    }
254                    WireMsg::Pong { .. } => {
255                        // last_seen already updated above via touch_client
256                    }
257                    WireMsg::Cmd(cmd) => {
258                        state.hub.dispatch_event(EventPayload::new(
259                            cmd.name.unwrap_or_else(|| "cmd".into()),
260                            cmd.payload,
261                        ));
262                    }
263                    WireMsg::Ping { ts } => {
264                        let pong = WireMsg::Pong { ts };
265                        if let Ok(json) = pong.to_json() {
266                            let _ = state.hub.send_to(client_id, json).await;
267                        }
268                    }
269                }
270            }
271            Message::Close(_) => break,
272            _ => {}
273        }
274    }
275
276    state.hub.unregister_client(client_id).await;
277    ping_task.abort();
278    drop(out_tx);
279    let _ = writer.await;
280}