//! Unified HTTP server for all webhook routes. //! //! Composes route fragments from HttpChannel, WASM channel router, etc. //! into a single axum server. Channels define routes but never spawn servers. use std::net::SocketAddr; use axum::Router; use tokio::sync::oneshot; use tokio::task::JoinHandle; use crate::error::ChannelError; /// Configuration for the unified webhook server. pub struct WebhookServerConfig { /// Address to bind the server to. pub addr: SocketAddr, } /// A single HTTP server that hosts all webhook routes. /// /// Channels contribute route fragments via `add_routes()`, then a single /// `start()` call binds the listener and spawns the server task. pub struct WebhookServer { config: WebhookServerConfig, routes: Vec, shutdown_tx: Option>, handle: Option>, } impl WebhookServer { /// Create a new webhook server with the given bind address. pub fn new(config: WebhookServerConfig) -> Self { Self { config, routes: Vec::new(), shutdown_tx: None, handle: None, } } /// Accumulate a route fragment. Each fragment should already have its /// state applied via `.with_state()`. pub fn add_routes(&mut self, router: Router) { self.routes.push(router); } /// Bind the listener, merge all route fragments, and spawn the server. pub async fn start(&mut self) -> Result<(), ChannelError> { let mut app = Router::new(); for fragment in self.routes.drain(..) { app = app.merge(fragment); } let listener = tokio::net::TcpListener::bind(self.config.addr) .await .map_err(|e| ChannelError::StartupFailed { name: "webhook_server".to_string(), reason: format!("Failed to bind to {}: {}", self.config.addr, e), })?; tracing::info!("Webhook server listening on {}", self.config.addr); let (shutdown_tx, shutdown_rx) = oneshot::channel(); self.shutdown_tx = Some(shutdown_tx); let handle = tokio::spawn(async move { if let Err(e) = axum::serve(listener, app) .with_graceful_shutdown(async { let _ = shutdown_rx.await; tracing::info!("Webhook server shutting down"); }) .await { tracing::error!("Webhook server error: {}", e); } }); self.handle = Some(handle); Ok(()) } /// Signal graceful shutdown and wait for the server task to finish. pub async fn shutdown(&mut self) { if let Some(tx) = self.shutdown_tx.take() { let _ = tx.send(()); } if let Some(handle) = self.handle.take() { let _ = handle.await; } } }