Files
foxhunt/web-gateway/src/state.rs
jgrusewski 3c3bf30062 safety(web-gateway,web-dashboard): clippy clean, gRPC timeouts, health probes, responsive grids, error boundaries
- Fix all 98 clippy warnings (0 remaining with -D warnings)
- Add 30s default gRPC timeout on all channels via Endpoint::timeout()
- Replace readiness probe is_some() with actual gRPC health check (returns 503 when degraded)
- Add responsive Tailwind breakpoints to all 5 dashboard pages
- Add ComponentErrorBoundary for crash-prone chart components
- Log JWT validation errors at debug level instead of silently discarding

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 08:59:20 +01:00

102 lines
3.3 KiB
Rust

use std::sync::Arc;
use std::time::Duration;
use tokio::sync::broadcast;
use tonic::transport::Channel;
use crate::config::GatewayConfig;
/// Default gRPC request deadline — prevents indefinite hangs on slow/unresponsive services
const GRPC_TIMEOUT: Duration = Duration::from_secs(30);
/// Shared application state passed to all handlers via Axum's State extractor
#[derive(Clone)]
pub struct AppState {
pub config: Arc<GatewayConfig>,
pub trading_channel: Option<Channel>,
pub backtesting_channel: Option<Channel>,
pub ml_training_channel: Option<Channel>,
/// Broadcast sender for WebSocket events
pub ws_broadcast: broadcast::Sender<String>,
}
impl AppState {
pub async fn new(config: GatewayConfig) -> anyhow::Result<Self> {
let (ws_broadcast, _) = broadcast::channel(4096);
// Create lazy gRPC channels (connect on first use) with default timeout
let trading_channel = Channel::from_shared(config.trading_service_url.clone())
.ok()
.map(|c| c.timeout(GRPC_TIMEOUT).connect_lazy());
let backtesting_channel = Channel::from_shared(config.backtesting_service_url.clone())
.ok()
.map(|c| c.timeout(GRPC_TIMEOUT).connect_lazy());
let ml_training_channel = Channel::from_shared(config.ml_training_service_url.clone())
.ok()
.map(|c| c.timeout(GRPC_TIMEOUT).connect_lazy());
Ok(Self {
config: Arc::new(config),
trading_channel,
backtesting_channel,
ml_training_channel,
ws_broadcast,
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[tokio::test]
async fn test_new_creates_state_with_valid_config() {
let config = GatewayConfig::default();
let state = AppState::new(config).await.unwrap();
assert!(state.trading_channel.is_some());
assert!(state.backtesting_channel.is_some());
assert!(state.ml_training_channel.is_some());
}
#[tokio::test]
async fn test_new_with_empty_url_yields_none_channel() {
let config = GatewayConfig {
trading_service_url: String::new(),
..GatewayConfig::default()
};
let state = AppState::new(config).await.unwrap();
assert!(state.trading_channel.is_none());
// Other channels should still work
assert!(state.backtesting_channel.is_some());
}
#[tokio::test]
async fn test_broadcast_channel_sends_and_receives() {
let config = GatewayConfig::default();
let state = AppState::new(config).await.unwrap();
let mut rx = state.ws_broadcast.subscribe();
state.ws_broadcast.send("hello".to_owned()).unwrap();
let msg = rx.recv().await.unwrap();
assert_eq!(msg, "hello");
}
#[tokio::test]
async fn test_config_is_arc_wrapped() {
let config = GatewayConfig {
jwt_secret: "test-secret".to_owned(),
..GatewayConfig::default()
};
let state = AppState::new(config).await.unwrap();
assert_eq!(state.config.jwt_secret, "test-secret");
}
#[test]
fn test_state_is_clone() {
// AppState must be Clone for Axum's State extractor
fn assert_clone<T: Clone>() {}
assert_clone::<AppState>();
}
}