use crate::monitoring::{MonitoringService, WorkflowMetrics};
use crate::workflow::WorkflowId;
use axum::{
extract::{
ws::{Message, WebSocket},
State, WebSocketUpgrade,
},
response::Response,
};
use futures::{sink::SinkExt, stream::StreamExt};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::broadcast;
use tracing::{debug, error, info};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum WorkflowEvent {
WorkflowStarted {
workflow_id: WorkflowId,
workflow_name: String,
},
WorkflowCompleted {
workflow_id: WorkflowId,
success: bool,
},
TaskStateChanged {
workflow_id: WorkflowId,
task_id: crate::task::TaskId,
task_name: String,
state: crate::task::TaskState,
},
ProgressUpdate {
workflow_id: WorkflowId,
progress: f64,
completed_tasks: usize,
total_tasks: usize,
},
MetricsUpdate {
metrics: WorkflowMetrics,
},
Error {
workflow_id: WorkflowId,
message: String,
},
}
#[derive(Clone)]
pub struct WebSocketState {
pub broadcaster: Arc<broadcast::Sender<WorkflowEvent>>,
pub monitoring: Arc<MonitoringService>,
}
impl WebSocketState {
#[must_use]
pub fn new(monitoring: Arc<MonitoringService>) -> Self {
let (tx, _) = broadcast::channel(1000);
Self {
broadcaster: Arc::new(tx),
monitoring,
}
}
pub fn broadcast(&self, event: WorkflowEvent) {
let _ = self.broadcaster.send(event);
}
pub fn broadcast_workflow_started(&self, workflow_id: WorkflowId, workflow_name: String) {
self.broadcast(WorkflowEvent::WorkflowStarted {
workflow_id,
workflow_name,
});
}
pub fn broadcast_workflow_completed(&self, workflow_id: WorkflowId, success: bool) {
self.broadcast(WorkflowEvent::WorkflowCompleted {
workflow_id,
success,
});
}
pub fn broadcast_task_state_changed(
&self,
workflow_id: WorkflowId,
task_id: crate::task::TaskId,
task_name: String,
state: crate::task::TaskState,
) {
self.broadcast(WorkflowEvent::TaskStateChanged {
workflow_id,
task_id,
task_name,
state,
});
}
pub fn broadcast_progress_update(
&self,
workflow_id: WorkflowId,
progress: f64,
completed_tasks: usize,
total_tasks: usize,
) {
self.broadcast(WorkflowEvent::ProgressUpdate {
workflow_id,
progress,
completed_tasks,
total_tasks,
});
}
pub fn broadcast_metrics_update(&self, metrics: WorkflowMetrics) {
self.broadcast(WorkflowEvent::MetricsUpdate { metrics });
}
pub fn broadcast_error(&self, workflow_id: WorkflowId, message: String) {
self.broadcast(WorkflowEvent::Error {
workflow_id,
message,
});
}
}
pub async fn websocket_handler(
ws: WebSocketUpgrade,
State(state): State<WebSocketState>,
) -> Response {
ws.on_upgrade(|socket| handle_websocket(socket, state))
}
async fn handle_websocket(socket: WebSocket, state: WebSocketState) {
let (mut sender, mut receiver) = socket.split();
let mut event_rx = state.broadcaster.subscribe();
info!("WebSocket client connected");
let active_workflows = state.monitoring.get_active_workflows();
for metrics in active_workflows {
let event = WorkflowEvent::MetricsUpdate { metrics };
if let Ok(json) = serde_json::to_string(&event) {
if sender.send(Message::Text(json.into())).await.is_err() {
error!("Failed to send initial state");
return;
}
}
}
let mut send_task = tokio::spawn(async move {
while let Ok(event) = event_rx.recv().await {
if let Ok(json) = serde_json::to_string(&event) {
if sender.send(Message::Text(json.into())).await.is_err() {
break;
}
}
}
});
let mut recv_task = tokio::spawn(async move {
while let Some(Ok(msg)) = receiver.next().await {
match msg {
Message::Text(text) => {
debug!("Received WebSocket message: {}", text);
}
Message::Close(_) => {
info!("WebSocket client disconnected");
break;
}
Message::Ping(_data) => {
debug!("Received ping");
}
_ => {}
}
}
});
tokio::select! {
_ = &mut send_task => {
recv_task.abort();
}
_ = &mut recv_task => {
send_task.abort();
}
}
info!("WebSocket connection closed");
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ClientMessage {
Subscribe {
workflow_id: WorkflowId,
},
Unsubscribe {
workflow_id: WorkflowId,
},
SubscribeAll,
Ping,
}
pub struct WebSocketManager {
state: WebSocketState,
}
impl WebSocketManager {
#[must_use]
pub fn new(monitoring: Arc<MonitoringService>) -> Self {
Self {
state: WebSocketState::new(monitoring),
}
}
#[must_use]
pub fn state(&self) -> WebSocketState {
self.state.clone()
}
pub fn broadcast(&self, event: WorkflowEvent) {
self.state.broadcast(event);
}
#[must_use]
pub fn subscriber_count(&self) -> usize {
self.state.broadcaster.receiver_count()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_websocket_state_creation() {
let monitoring = Arc::new(MonitoringService::new());
let state = WebSocketState::new(monitoring);
assert_eq!(state.broadcaster.receiver_count(), 0);
}
#[test]
fn test_broadcast_event() {
let monitoring = Arc::new(MonitoringService::new());
let state = WebSocketState::new(monitoring);
let mut rx = state.broadcaster.subscribe();
let workflow_id = WorkflowId::new();
state.broadcast_workflow_started(workflow_id, "test-workflow".to_string());
tokio::runtime::Runtime::new()
.expect("should succeed in test")
.block_on(async {
let event = rx.recv().await.expect("should succeed in test");
match event {
WorkflowEvent::WorkflowStarted {
workflow_id: id,
workflow_name,
} => {
assert_eq!(id, workflow_id);
assert_eq!(workflow_name, "test-workflow");
}
_ => panic!("Wrong event type"),
}
});
}
#[test]
fn test_websocket_manager() {
let monitoring = Arc::new(MonitoringService::new());
let manager = WebSocketManager::new(monitoring);
assert_eq!(manager.subscriber_count(), 0);
}
#[test]
fn test_workflow_event_serialization() {
let event = WorkflowEvent::WorkflowStarted {
workflow_id: WorkflowId::new(),
workflow_name: "test".to_string(),
};
let json = serde_json::to_string(&event).expect("should succeed in test");
assert!(json.contains("workflow_started"));
}
#[test]
fn test_client_message_serialization() {
let msg = ClientMessage::Subscribe {
workflow_id: WorkflowId::new(),
};
let json = serde_json::to_string(&msg).expect("should succeed in test");
assert!(json.contains("subscribe"));
}
}