use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use axum::{
extract::{
ws::{Message, WebSocket, WebSocketUpgrade},
State,
},
response::Response,
};
use futures_util::{SinkExt, StreamExt};
use parking_lot::Mutex;
use tokio::sync::broadcast;
use uuid::Uuid;
use super::dto::{WebSocketMessage, WebSocketRequest};
use super::state::AppState;
#[tracing::instrument(skip(state, ws))]
pub async fn ws_handler(State(state): State<AppState>, ws: WebSocketUpgrade) -> Response {
ws.on_upgrade(move |socket| handle_socket(socket, state))
}
async fn handle_socket(socket: WebSocket, state: AppState) {
let (mut sender, mut receiver) = socket.split();
let subscriptions: Arc<Mutex<SubscriptionState>> =
Arc::new(Mutex::new(SubscriptionState::new()));
let mut broadcast_rx = state.subscribe();
let subs_clone = subscriptions.clone();
let forward_task = tokio::spawn(async move {
loop {
tokio::select! {
result = broadcast_rx.recv() => {
match result {
Ok(msg) => {
if subs_clone.lock().should_receive(&msg) {
if let Ok(json) = serde_json::to_string(&msg) {
if sender.send(Message::Text(json)).await.is_err() {
break;
}
}
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
tracing::warn!(lagged = n, "WebSocket client lagged, messages dropped");
let error = WebSocketMessage::Error {
code: "MESSAGES_DROPPED".to_string(),
message: format!("{} messages were dropped due to slow client", n),
};
if let Ok(json) = serde_json::to_string(&error) {
if sender.send(Message::Text(json)).await.is_err() {
break;
}
}
}
Err(broadcast::error::RecvError::Closed) => {
break;
}
}
}
_ = tokio::time::sleep(Duration::from_secs(30)) => {
let heartbeat = WebSocketMessage::Heartbeat {
timestamp: chrono::Utc::now(),
};
if let Ok(json) = serde_json::to_string(&heartbeat) {
if sender.send(Message::Ping(json.into_bytes())).await.is_err() {
break;
}
}
}
}
}
});
let subs_clone = subscriptions.clone();
let state_clone = state.clone();
while let Some(Ok(msg)) = receiver.next().await {
match msg {
Message::Text(text) => {
if let Err(e) = handle_client_message(&text, &subs_clone, &state_clone).await {
tracing::warn!(error = %e, "Failed to handle WebSocket message");
}
}
Message::Binary(_) => {
tracing::debug!("Ignoring binary WebSocket message");
}
Message::Ping(data) => {
tracing::trace!(len = data.len(), "Received ping");
}
Message::Pong(_) => {
tracing::trace!("Received pong");
}
Message::Close(_) => {
tracing::debug!("Client closed WebSocket connection");
break;
}
}
}
forward_task.abort();
tracing::debug!("WebSocket connection closed");
}
async fn handle_client_message(
text: &str,
subscriptions: &Arc<Mutex<SubscriptionState>>,
state: &AppState,
) -> Result<(), Box<dyn std::error::Error>> {
let request: WebSocketRequest = serde_json::from_str(text)?;
match request {
WebSocketRequest::Subscribe { event_id } => {
if state.get_event(event_id).is_some() {
subscriptions.lock().subscribe(event_id);
tracing::debug!(event_id = %event_id, "Client subscribed to event");
}
}
WebSocketRequest::Unsubscribe { event_id } => {
subscriptions.lock().unsubscribe(&event_id);
tracing::debug!(event_id = %event_id, "Client unsubscribed from event");
}
WebSocketRequest::SubscribeAll => {
subscriptions.lock().subscribe_all();
tracing::debug!("Client subscribed to all events");
}
WebSocketRequest::GetState { event_id } => {
tracing::debug!(event_id = %event_id, "Client requested state");
}
}
Ok(())
}
struct SubscriptionState {
event_ids: HashSet<Uuid>,
all_events: bool,
}
impl SubscriptionState {
fn new() -> Self {
Self {
event_ids: HashSet::new(),
all_events: true, }
}
fn subscribe(&mut self, event_id: Uuid) {
self.all_events = false;
self.event_ids.insert(event_id);
}
fn unsubscribe(&mut self, event_id: &Uuid) {
self.event_ids.remove(event_id);
if self.event_ids.is_empty() {
self.all_events = true;
}
}
fn subscribe_all(&mut self) {
self.all_events = true;
self.event_ids.clear();
}
fn should_receive(&self, msg: &WebSocketMessage) -> bool {
if self.all_events {
return true;
}
let event_id = match msg {
WebSocketMessage::SurvivorDetected { event_id, .. } => Some(*event_id),
WebSocketMessage::SurvivorUpdated { event_id, .. } => Some(*event_id),
WebSocketMessage::SurvivorLost { event_id, .. } => Some(*event_id),
WebSocketMessage::AlertCreated { event_id, .. } => Some(*event_id),
WebSocketMessage::AlertUpdated { event_id, .. } => Some(*event_id),
WebSocketMessage::ZoneScanComplete { event_id, .. } => Some(*event_id),
WebSocketMessage::EventStatusChanged { event_id, .. } => Some(*event_id),
WebSocketMessage::Heartbeat { .. } => None, WebSocketMessage::Error { .. } => None, };
match event_id {
Some(id) => self.event_ids.contains(&id),
None => true, }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_subscription_state() {
let mut state = SubscriptionState::new();
assert!(state.all_events);
let event_id = Uuid::new_v4();
state.subscribe(event_id);
assert!(!state.all_events);
assert!(state.event_ids.contains(&event_id));
state.unsubscribe(&event_id);
assert!(state.all_events);
}
#[test]
fn test_should_receive() {
let mut state = SubscriptionState::new();
let event_id = Uuid::new_v4();
let other_id = Uuid::new_v4();
let msg = WebSocketMessage::Heartbeat {
timestamp: chrono::Utc::now(),
};
assert!(state.should_receive(&msg));
state.subscribe(event_id);
let msg = WebSocketMessage::SurvivorLost {
event_id,
survivor_id: Uuid::new_v4(),
};
assert!(state.should_receive(&msg));
let msg = WebSocketMessage::SurvivorLost {
event_id: other_id,
survivor_id: Uuid::new_v4(),
};
assert!(!state.should_receive(&msg));
let msg = WebSocketMessage::Heartbeat {
timestamp: chrono::Utc::now(),
};
assert!(state.should_receive(&msg));
}
}