Skip to main content

arc_web/websocket/
server.rs

1use actix::prelude::*;
2use std::collections::{HashMap, HashSet};
3use tracing::info;
4use uuid::Uuid;
5
6/// Message to connect a new client
7#[derive(Message)]
8#[rtype(result = "()")]
9pub struct Connect {
10    pub id: Uuid,
11    pub user_id: Option<String>,
12    pub addr: Recipient<WsMessage>,
13}
14
15/// Message to disconnect a client
16#[derive(Message)]
17#[rtype(result = "()")]
18pub struct Disconnect {
19    pub id: Uuid,
20}
21
22/// Message to subscribe to a room/channel
23#[derive(Message)]
24#[rtype(result = "()")]
25pub struct Subscribe {
26    pub id: Uuid,
27    pub room: String,
28}
29
30/// Message to unsubscribe from a room/channel
31#[derive(Message)]
32#[rtype(result = "()")]
33pub struct Unsubscribe {
34    pub id: Uuid,
35    pub room: String,
36}
37
38/// Message to broadcast to a specific room
39#[derive(Message)]
40#[rtype(result = "()")]
41pub struct BroadcastToRoom {
42    pub room: String,
43    pub message: String,
44    pub skip_id: Option<Uuid>,
45}
46
47/// Message to broadcast to a specific user
48#[derive(Message)]
49#[rtype(result = "()")]
50pub struct BroadcastToUser {
51    pub user_id: String,
52    pub message: String,
53}
54
55/// Message to broadcast to all connected clients
56#[derive(Message)]
57#[rtype(result = "()")]
58pub struct BroadcastAll {
59    pub message: String,
60    pub skip_id: Option<Uuid>,
61}
62
63/// WebSocket message to send to a client
64#[derive(Message, Clone)]
65#[rtype(result = "()")]
66pub struct WsMessage(pub String);
67
68/// Connection info stored in the server
69struct ConnectionInfo {
70    addr: Recipient<WsMessage>,
71    user_id: Option<String>,
72    rooms: HashSet<String>,
73}
74
75/// WebSocket broadcast server
76/// Maintains registry of all active connections and handles message broadcasting
77pub struct WsServer {
78    /// Map of connection ID to connection info
79    connections: HashMap<Uuid, ConnectionInfo>,
80    /// Map of room name to set of connection IDs
81    rooms: HashMap<String, HashSet<Uuid>>,
82    /// Map of user ID to set of connection IDs (for user-specific broadcasts)
83    user_connections: HashMap<String, HashSet<Uuid>>,
84}
85
86impl WsServer {
87    pub fn new() -> Self {
88        WsServer {
89            connections: HashMap::new(),
90            rooms: HashMap::new(),
91            user_connections: HashMap::new(),
92        }
93    }
94
95    /// Send a message to a specific connection
96    fn send_message(&self, id: &Uuid, message: &str) {
97        if let Some(conn) = self.connections.get(id) {
98            conn.addr.do_send(WsMessage(message.to_string()));
99        }
100    }
101
102    /// Get the number of active connections
103    pub fn connection_count(&self) -> usize {
104        self.connections.len()
105    }
106}
107
108impl Default for WsServer {
109    fn default() -> Self {
110        Self::new()
111    }
112}
113
114impl Actor for WsServer {
115    type Context = Context<Self>;
116}
117
118impl Handler<Connect> for WsServer {
119    type Result = ();
120
121    fn handle(&mut self, msg: Connect, _: &mut Context<Self>) {
122        let user_id_for_log = msg.user_id.clone();
123        let user_id_for_track = msg.user_id.clone();
124        // Add connection to registry
125        self.connections.insert(
126            msg.id,
127            ConnectionInfo {
128                addr: msg.addr,
129                user_id: msg.user_id,
130                rooms: HashSet::new(),
131            },
132        );
133
134        // Track user connections if authenticated
135        if let Some(user_id) = user_id_for_track {
136            self.user_connections
137                .entry(user_id)
138                .or_default()
139                .insert(msg.id);
140        }
141
142        info!(connection_id = %msg.id, user_id = ?user_id_for_log, "WebSocket connection established");
143    }
144}
145
146impl Handler<Disconnect> for WsServer {
147    type Result = ();
148
149    fn handle(&mut self, msg: Disconnect, _: &mut Context<Self>) {
150        if let Some(conn) = self.connections.remove(&msg.id) {
151            // Remove from all rooms
152            for room in &conn.rooms {
153                if let Some(room_members) = self.rooms.get_mut(room) {
154                    room_members.remove(&msg.id);
155                    if room_members.is_empty() {
156                        self.rooms.remove(room);
157                    }
158                }
159            }
160
161            // Remove from user connections
162            if let Some(user_id) = conn.user_id {
163                if let Some(user_conns) = self.user_connections.get_mut(&user_id) {
164                    user_conns.remove(&msg.id);
165                    if user_conns.is_empty() {
166                        self.user_connections.remove(&user_id);
167                    }
168                }
169            }
170
171            info!(connection_id = %msg.id, "WebSocket connection closed");
172        }
173    }
174}
175
176impl Handler<Subscribe> for WsServer {
177    type Result = ();
178
179    fn handle(&mut self, msg: Subscribe, _: &mut Context<Self>) {
180        // Add connection to room
181        self.rooms
182            .entry(msg.room.clone())
183            .or_default()
184            .insert(msg.id);
185
186        // Track room in connection
187        if let Some(conn) = self.connections.get_mut(&msg.id) {
188            conn.rooms.insert(msg.room.clone());
189        }
190
191        // Debug: Connection subscribed to room
192    }
193}
194
195impl Handler<Unsubscribe> for WsServer {
196    type Result = ();
197
198    fn handle(&mut self, msg: Unsubscribe, _: &mut Context<Self>) {
199        // Remove connection from room
200        if let Some(room_members) = self.rooms.get_mut(&msg.room) {
201            room_members.remove(&msg.id);
202            if room_members.is_empty() {
203                self.rooms.remove(&msg.room);
204            }
205        }
206
207        // Remove room from connection tracking
208        if let Some(conn) = self.connections.get_mut(&msg.id) {
209            conn.rooms.remove(&msg.room);
210        }
211
212        // Debug: Connection unsubscribed from room
213    }
214}
215
216impl Handler<BroadcastToRoom> for WsServer {
217    type Result = ();
218
219    fn handle(&mut self, msg: BroadcastToRoom, _: &mut Context<Self>) {
220        if let Some(members) = self.rooms.get(&msg.room) {
221            for id in members {
222                // Skip sender if specified
223                if let Some(skip_id) = msg.skip_id {
224                    if *id == skip_id {
225                        continue;
226                    }
227                }
228                self.send_message(id, &msg.message);
229            }
230        }
231    }
232}
233
234impl Handler<BroadcastToUser> for WsServer {
235    type Result = ();
236
237    fn handle(&mut self, msg: BroadcastToUser, _: &mut Context<Self>) {
238        if let Some(user_conns) = self.user_connections.get(&msg.user_id) {
239            for id in user_conns {
240                self.send_message(id, &msg.message);
241            }
242        }
243    }
244}
245
246impl Handler<BroadcastAll> for WsServer {
247    type Result = ();
248
249    fn handle(&mut self, msg: BroadcastAll, _: &mut Context<Self>) {
250        for id in self.connections.keys() {
251            // Skip sender if specified
252            if let Some(skip_id) = msg.skip_id {
253                if *id == skip_id {
254                    continue;
255                }
256            }
257            self.send_message(id, &msg.message);
258        }
259    }
260}