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}