Skip to main content

oximedia_workflow/
websocket.rs

1//! WebSocket support for real-time workflow updates.
2
3use crate::monitoring::{MonitoringService, WorkflowMetrics};
4use crate::workflow::WorkflowId;
5use axum::{
6    extract::{
7        ws::{Message, WebSocket},
8        State, WebSocketUpgrade,
9    },
10    response::Response,
11};
12use futures::{sink::SinkExt, stream::StreamExt};
13use serde::{Deserialize, Serialize};
14use std::sync::Arc;
15use tokio::sync::broadcast;
16use tracing::{debug, error, info};
17
18/// WebSocket event types.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(tag = "type", rename_all = "snake_case")]
21pub enum WorkflowEvent {
22    /// Workflow started.
23    WorkflowStarted {
24        /// Workflow ID.
25        workflow_id: WorkflowId,
26        /// Workflow name.
27        workflow_name: String,
28    },
29
30    /// Workflow completed.
31    WorkflowCompleted {
32        /// Workflow ID.
33        workflow_id: WorkflowId,
34        /// Success status.
35        success: bool,
36    },
37
38    /// Task state changed.
39    TaskStateChanged {
40        /// Workflow ID.
41        workflow_id: WorkflowId,
42        /// Task ID.
43        task_id: crate::task::TaskId,
44        /// Task name.
45        task_name: String,
46        /// New state.
47        state: crate::task::TaskState,
48    },
49
50    /// Progress update.
51    ProgressUpdate {
52        /// Workflow ID.
53        workflow_id: WorkflowId,
54        /// Progress percentage (0-100).
55        progress: f64,
56        /// Completed tasks.
57        completed_tasks: usize,
58        /// Total tasks.
59        total_tasks: usize,
60    },
61
62    /// Metrics update.
63    MetricsUpdate {
64        /// Workflow metrics.
65        metrics: WorkflowMetrics,
66    },
67
68    /// Error occurred.
69    Error {
70        /// Workflow ID.
71        workflow_id: WorkflowId,
72        /// Error message.
73        message: String,
74    },
75}
76
77/// WebSocket state.
78#[derive(Clone)]
79pub struct WebSocketState {
80    /// Event broadcaster.
81    pub broadcaster: Arc<broadcast::Sender<WorkflowEvent>>,
82    /// Monitoring service.
83    pub monitoring: Arc<MonitoringService>,
84}
85
86impl WebSocketState {
87    /// Create a new WebSocket state.
88    #[must_use]
89    pub fn new(monitoring: Arc<MonitoringService>) -> Self {
90        let (tx, _) = broadcast::channel(1000);
91        Self {
92            broadcaster: Arc::new(tx),
93            monitoring,
94        }
95    }
96
97    /// Broadcast an event to all connected clients.
98    pub fn broadcast(&self, event: WorkflowEvent) {
99        let _ = self.broadcaster.send(event);
100    }
101
102    /// Broadcast workflow started event.
103    pub fn broadcast_workflow_started(&self, workflow_id: WorkflowId, workflow_name: String) {
104        self.broadcast(WorkflowEvent::WorkflowStarted {
105            workflow_id,
106            workflow_name,
107        });
108    }
109
110    /// Broadcast workflow completed event.
111    pub fn broadcast_workflow_completed(&self, workflow_id: WorkflowId, success: bool) {
112        self.broadcast(WorkflowEvent::WorkflowCompleted {
113            workflow_id,
114            success,
115        });
116    }
117
118    /// Broadcast task state changed event.
119    pub fn broadcast_task_state_changed(
120        &self,
121        workflow_id: WorkflowId,
122        task_id: crate::task::TaskId,
123        task_name: String,
124        state: crate::task::TaskState,
125    ) {
126        self.broadcast(WorkflowEvent::TaskStateChanged {
127            workflow_id,
128            task_id,
129            task_name,
130            state,
131        });
132    }
133
134    /// Broadcast progress update.
135    pub fn broadcast_progress_update(
136        &self,
137        workflow_id: WorkflowId,
138        progress: f64,
139        completed_tasks: usize,
140        total_tasks: usize,
141    ) {
142        self.broadcast(WorkflowEvent::ProgressUpdate {
143            workflow_id,
144            progress,
145            completed_tasks,
146            total_tasks,
147        });
148    }
149
150    /// Broadcast metrics update.
151    pub fn broadcast_metrics_update(&self, metrics: WorkflowMetrics) {
152        self.broadcast(WorkflowEvent::MetricsUpdate { metrics });
153    }
154
155    /// Broadcast error event.
156    pub fn broadcast_error(&self, workflow_id: WorkflowId, message: String) {
157        self.broadcast(WorkflowEvent::Error {
158            workflow_id,
159            message,
160        });
161    }
162}
163
164/// WebSocket handler.
165pub async fn websocket_handler(
166    ws: WebSocketUpgrade,
167    State(state): State<WebSocketState>,
168) -> Response {
169    ws.on_upgrade(|socket| handle_websocket(socket, state))
170}
171
172async fn handle_websocket(socket: WebSocket, state: WebSocketState) {
173    let (mut sender, mut receiver) = socket.split();
174
175    // Subscribe to events
176    let mut event_rx = state.broadcaster.subscribe();
177
178    info!("WebSocket client connected");
179
180    // Send initial state
181    let active_workflows = state.monitoring.get_active_workflows();
182    for metrics in active_workflows {
183        let event = WorkflowEvent::MetricsUpdate { metrics };
184        if let Ok(json) = serde_json::to_string(&event) {
185            if sender.send(Message::Text(json.into())).await.is_err() {
186                error!("Failed to send initial state");
187                return;
188            }
189        }
190    }
191
192    // Spawn event broadcaster task
193    let mut send_task = tokio::spawn(async move {
194        while let Ok(event) = event_rx.recv().await {
195            if let Ok(json) = serde_json::to_string(&event) {
196                if sender.send(Message::Text(json.into())).await.is_err() {
197                    break;
198                }
199            }
200        }
201    });
202
203    // Handle incoming messages
204    let mut recv_task = tokio::spawn(async move {
205        while let Some(Ok(msg)) = receiver.next().await {
206            match msg {
207                Message::Text(text) => {
208                    debug!("Received WebSocket message: {}", text);
209                    // Handle client messages (e.g., subscribe to specific workflows)
210                }
211                Message::Close(_) => {
212                    info!("WebSocket client disconnected");
213                    break;
214                }
215                Message::Ping(_data) => {
216                    debug!("Received ping");
217                    // Pong is sent automatically by axum
218                }
219                _ => {}
220            }
221        }
222    });
223
224    // Wait for either task to finish
225    tokio::select! {
226        _ = &mut send_task => {
227            recv_task.abort();
228        }
229        _ = &mut recv_task => {
230            send_task.abort();
231        }
232    }
233
234    info!("WebSocket connection closed");
235}
236
237/// WebSocket client message.
238#[derive(Debug, Clone, Serialize, Deserialize)]
239#[serde(tag = "type", rename_all = "snake_case")]
240pub enum ClientMessage {
241    /// Subscribe to workflow updates.
242    Subscribe {
243        /// Workflow ID to subscribe to.
244        workflow_id: WorkflowId,
245    },
246
247    /// Unsubscribe from workflow updates.
248    Unsubscribe {
249        /// Workflow ID to unsubscribe from.
250        workflow_id: WorkflowId,
251    },
252
253    /// Subscribe to all workflows.
254    SubscribeAll,
255
256    /// Ping message.
257    Ping,
258}
259
260/// Enhanced WebSocket handler with subscription management.
261pub struct WebSocketManager {
262    state: WebSocketState,
263}
264
265impl WebSocketManager {
266    /// Create a new WebSocket manager.
267    #[must_use]
268    pub fn new(monitoring: Arc<MonitoringService>) -> Self {
269        Self {
270            state: WebSocketState::new(monitoring),
271        }
272    }
273
274    /// Get WebSocket state.
275    #[must_use]
276    pub fn state(&self) -> WebSocketState {
277        self.state.clone()
278    }
279
280    /// Broadcast an event.
281    pub fn broadcast(&self, event: WorkflowEvent) {
282        self.state.broadcast(event);
283    }
284
285    /// Get subscriber count.
286    #[must_use]
287    pub fn subscriber_count(&self) -> usize {
288        self.state.broadcaster.receiver_count()
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn test_websocket_state_creation() {
298        let monitoring = Arc::new(MonitoringService::new());
299        let state = WebSocketState::new(monitoring);
300        assert_eq!(state.broadcaster.receiver_count(), 0);
301    }
302
303    #[tokio::test]
304    async fn test_broadcast_event() {
305        let monitoring = Arc::new(MonitoringService::new());
306        let state = WebSocketState::new(monitoring);
307
308        let mut rx = state.broadcaster.subscribe();
309
310        let workflow_id = WorkflowId::new();
311        state.broadcast_workflow_started(workflow_id, "test-workflow".to_string());
312
313        let event = rx.recv().await.expect("should succeed in test");
314        match event {
315            WorkflowEvent::WorkflowStarted {
316                workflow_id: id,
317                workflow_name,
318            } => {
319                assert_eq!(id, workflow_id);
320                assert_eq!(workflow_name, "test-workflow");
321            }
322            _ => panic!("Wrong event type"),
323        }
324    }
325
326    #[test]
327    fn test_websocket_manager() {
328        let monitoring = Arc::new(MonitoringService::new());
329        let manager = WebSocketManager::new(monitoring);
330
331        assert_eq!(manager.subscriber_count(), 0);
332    }
333
334    #[test]
335    fn test_workflow_event_serialization() {
336        let event = WorkflowEvent::WorkflowStarted {
337            workflow_id: WorkflowId::new(),
338            workflow_name: "test".to_string(),
339        };
340
341        let json = serde_json::to_string(&event).expect("should succeed in test");
342        assert!(json.contains("workflow_started"));
343    }
344
345    #[test]
346    fn test_client_message_serialization() {
347        let msg = ClientMessage::Subscribe {
348            workflow_id: WorkflowId::new(),
349        };
350
351        let json = serde_json::to_string(&msg).expect("should succeed in test");
352        assert!(json.contains("subscribe"));
353    }
354}