intent_engine/
notifications.rs

1/// Unified notification infrastructure for Dashboard and MCP WebSocket communication
2///
3/// This module provides a centralized NotificationSender that handles the common
4/// pattern of sending database operation notifications via both WebSocket channels.
5use crate::dashboard::websocket::{DatabaseOperationPayload, ProtocolMessage, WebSocketState};
6use std::sync::Arc;
7use tokio::sync::mpsc::UnboundedSender;
8
9/// Centralized notification sender for database operations
10///
11/// Handles the common 3-step pattern:
12/// 1. Create ProtocolMessage from payload
13/// 2. Serialize to JSON
14/// 3. Send via both WebSocket (Dashboard UI) and MCP channels
15pub struct NotificationSender {
16    ws_state: Option<Arc<WebSocketState>>,
17    mcp_notifier: Option<UnboundedSender<String>>,
18}
19
20impl NotificationSender {
21    /// Create a new NotificationSender
22    ///
23    /// # Arguments
24    /// * `ws_state` - Optional WebSocket state for Dashboard UI notifications
25    /// * `mcp_notifier` - Optional MCP channel for MCP client notifications
26    pub fn new(
27        ws_state: Option<Arc<WebSocketState>>,
28        mcp_notifier: Option<UnboundedSender<String>>,
29    ) -> Self {
30        Self {
31            ws_state,
32            mcp_notifier,
33        }
34    }
35
36    /// Send a database operation notification via all available channels
37    ///
38    /// This method handles:
39    /// - Creating a ProtocolMessage wrapper
40    /// - Serializing to JSON (with error handling)
41    /// - Broadcasting to Dashboard WebSocket (if connected)
42    /// - Sending to MCP channel (if connected)
43    ///
44    /// # Arguments
45    /// * `payload` - The database operation payload to send
46    pub async fn send(&self, payload: DatabaseOperationPayload) {
47        use ProtocolMessage as PM;
48
49        // Step 1: Wrap payload in protocol message
50        let msg = PM::new("db_operation", payload);
51
52        // Step 2: Serialize to JSON
53        let json = match msg.to_json() {
54            Ok(j) => j,
55            Err(e) => {
56                tracing::warn!("Failed to serialize notification message: {}", e);
57                return;
58            },
59        };
60
61        // Step 3a: Send via Dashboard WebSocket (if available)
62        if let Some(ws) = &self.ws_state {
63            ws.broadcast_to_ui(&json).await;
64        }
65
66        // Step 3b: Send via MCP channel (if available) - non-blocking
67        if let Some(notifier) = &self.mcp_notifier {
68            if let Err(e) = notifier.send(json) {
69                tracing::debug!("Failed to send MCP notification (channel closed): {}", e);
70            }
71        }
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn test_notification_sender_new() {
81        let sender = NotificationSender::new(None, None);
82        assert!(sender.ws_state.is_none());
83        assert!(sender.mcp_notifier.is_none());
84    }
85
86    #[tokio::test]
87    async fn test_send_with_no_channels() {
88        // Should not panic when no channels are configured
89        let sender = NotificationSender::new(None, None);
90        let payload = DatabaseOperationPayload {
91            operation: "create".to_string(),
92            entity: "task".to_string(),
93            affected_ids: vec![1],
94            data: None,
95            project_path: "/test".to_string(),
96        };
97
98        sender.send(payload).await; // Should complete without error
99    }
100}