Skip to main content

mockforge_collab/
events.rs

1//! Event system for real-time updates
2
3use crate::error::Result;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::sync::Arc;
7use tokio::sync::broadcast;
8use uuid::Uuid;
9
10/// Type of change event
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12#[serde(rename_all = "snake_case")]
13pub enum ChangeType {
14    /// Mock/route created
15    MockCreated,
16    /// Mock/route updated
17    MockUpdated,
18    /// Mock/route deleted
19    MockDeleted,
20    /// Workspace settings updated
21    WorkspaceUpdated,
22    /// Member added
23    MemberAdded,
24    /// Member removed
25    MemberRemoved,
26    /// Member role changed
27    RoleChanged,
28    /// Snapshot created
29    SnapshotCreated,
30    /// User cursor moved (presence)
31    CursorMoved,
32    /// User joined workspace
33    UserJoined,
34    /// User left workspace
35    UserLeft,
36}
37
38/// A change event
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ChangeEvent {
41    /// Event ID
42    pub id: Uuid,
43    /// Workspace ID
44    pub workspace_id: Uuid,
45    /// Type of change
46    pub change_type: ChangeType,
47    /// User who triggered the change
48    pub user_id: Uuid,
49    /// Resource ID (mock ID, member ID, etc.)
50    pub resource_id: Option<Uuid>,
51    /// Event payload
52    pub payload: serde_json::Value,
53    /// Timestamp
54    pub timestamp: DateTime<Utc>,
55}
56
57impl ChangeEvent {
58    /// Create a new change event
59    #[must_use]
60    pub fn new(
61        workspace_id: Uuid,
62        change_type: ChangeType,
63        user_id: Uuid,
64        resource_id: Option<Uuid>,
65        payload: serde_json::Value,
66    ) -> Self {
67        Self {
68            id: Uuid::new_v4(),
69            workspace_id,
70            change_type,
71            user_id,
72            resource_id,
73            payload,
74            timestamp: Utc::now(),
75        }
76    }
77}
78
79/// Event listener trait
80#[async_trait::async_trait]
81pub trait EventListener: Send + Sync {
82    /// Handle an event
83    async fn on_event(&self, event: ChangeEvent) -> Result<()>;
84}
85
86/// Event bus for broadcasting changes
87pub struct EventBus {
88    /// Broadcast channel for events
89    sender: broadcast::Sender<ChangeEvent>,
90}
91
92impl EventBus {
93    /// Create a new event bus
94    #[must_use]
95    pub fn new(capacity: usize) -> Self {
96        let (sender, _) = broadcast::channel(capacity);
97        Self { sender }
98    }
99
100    /// Publish an event
101    ///
102    /// # Errors
103    ///
104    /// Returns an error if publishing fails.
105    pub fn publish(&self, event: ChangeEvent) -> Result<()> {
106        // Ignore error if no receivers (it's ok)
107        let _ = self.sender.send(event);
108        Ok(())
109    }
110
111    /// Subscribe to events
112    #[must_use]
113    pub fn subscribe(&self) -> broadcast::Receiver<ChangeEvent> {
114        self.sender.subscribe()
115    }
116
117    /// Get number of active subscribers
118    #[must_use]
119    pub fn subscriber_count(&self) -> usize {
120        self.sender.receiver_count()
121    }
122}
123
124/// Workspace-specific event bus
125pub struct WorkspaceEventBus {
126    /// Main event bus
127    event_bus: Arc<EventBus>,
128    /// Workspace ID
129    workspace_id: Uuid,
130}
131
132impl WorkspaceEventBus {
133    /// Create a new workspace event bus
134    #[must_use]
135    pub const fn new(event_bus: Arc<EventBus>, workspace_id: Uuid) -> Self {
136        Self {
137            event_bus,
138            workspace_id,
139        }
140    }
141
142    /// Publish an event for this workspace
143    ///
144    /// # Errors
145    ///
146    /// Returns an error if publishing fails.
147    pub fn publish(
148        &self,
149        change_type: ChangeType,
150        user_id: Uuid,
151        resource_id: Option<Uuid>,
152        payload: serde_json::Value,
153    ) -> Result<()> {
154        let event = ChangeEvent::new(self.workspace_id, change_type, user_id, resource_id, payload);
155        self.event_bus.publish(event)
156    }
157
158    /// Subscribe to events (need to filter by `workspace_id`)
159    #[must_use]
160    pub fn subscribe(&self) -> broadcast::Receiver<ChangeEvent> {
161        self.event_bus.subscribe()
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_change_event_creation() {
171        let workspace_id = Uuid::new_v4();
172        let user_id = Uuid::new_v4();
173        let event = ChangeEvent::new(
174            workspace_id,
175            ChangeType::MockCreated,
176            user_id,
177            None,
178            serde_json::json!({"mock_id": "123"}),
179        );
180
181        assert_eq!(event.workspace_id, workspace_id);
182        assert_eq!(event.change_type, ChangeType::MockCreated);
183        assert_eq!(event.user_id, user_id);
184    }
185
186    #[test]
187    fn test_event_bus() {
188        let bus = EventBus::new(100);
189        assert_eq!(bus.subscriber_count(), 0);
190
191        let _rx1 = bus.subscribe();
192        assert_eq!(bus.subscriber_count(), 1);
193
194        let _rx2 = bus.subscribe();
195        assert_eq!(bus.subscriber_count(), 2);
196    }
197
198    #[tokio::test]
199    async fn test_event_publishing() {
200        let bus = EventBus::new(100);
201        let mut rx = bus.subscribe();
202
203        let workspace_id = Uuid::new_v4();
204        let user_id = Uuid::new_v4();
205        let event = ChangeEvent::new(
206            workspace_id,
207            ChangeType::MockCreated,
208            user_id,
209            None,
210            serde_json::json!({}),
211        );
212
213        bus.publish(event.clone()).unwrap();
214
215        let received = rx.recv().await.unwrap();
216        assert_eq!(received.workspace_id, workspace_id);
217        assert_eq!(received.change_type, ChangeType::MockCreated);
218    }
219}