Skip to main content

rainier_websocket/
handler.rs

1//! What an application writes — [`WebSocketHandler`] and the route table.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use rainier_http::Request;
7use rainier_support::Result;
8
9use crate::message::Message;
10use crate::socket::Socket;
11
12/// One WebSocket endpoint.
13///
14/// ```ignore
15/// pub struct Chat;
16///
17/// #[async_trait]
18/// impl WebSocketHandler for Chat {
19///     async fn on_message(&self, socket: &Socket, message: Message) -> Result<()> {
20///         socket.send(format!("you said: {}", message.as_text().unwrap_or("")))
21///     }
22/// }
23/// ```
24///
25/// # The three hooks
26///
27/// `on_connect` runs once, after the handshake. `on_message` runs per frame.
28/// `on_close` runs once, **whatever ended the connection** — a clean close, a
29/// dropped TCP connection, or an error from one of the other two. That last
30/// guarantee is the one worth relying on: a room registry that only cleaned up
31/// on a polite goodbye would leak an entry for every client that closed its
32/// laptop.
33///
34/// # Errors
35///
36/// An error from `on_connect` or `on_message` closes the connection and is
37/// logged. It is not sent to the client — the same rule a
38/// [5xx](../rainier_http/index.html) follows, and for the same reason: an
39/// error message is written for you, not for whoever is connected.
40#[async_trait::async_trait]
41pub trait WebSocketHandler: Send + Sync + 'static {
42    /// A new client has finished the handshake.
43    ///
44    /// The place to greet them, register them in a room, or close the
45    /// connection because they should not be here.
46    async fn on_connect(&self, socket: &Socket) -> Result<()> {
47        let _ = socket;
48        Ok(())
49    }
50
51    /// A frame arrived.
52    async fn on_message(&self, socket: &Socket, message: Message) -> Result<()>;
53
54    /// The connection has ended, however it ended.
55    ///
56    /// Runs exactly once per connection. Cannot fail, because there is nothing
57    /// left to fail into — clean up and log.
58    async fn on_close(&self, socket: &Socket) {
59        let _ = socket;
60    }
61
62    /// Whether this request may open a socket at all.
63    ///
64    /// Runs **before the handshake**, with the HTTP request that asked to
65    /// upgrade — so it has the headers, the cookies and whatever the
66    /// middleware put in the extensions. Returning `false` answers `403` and
67    /// no socket is created.
68    ///
69    /// The default allows everyone, which is right for a public feed and
70    /// wrong for anything else. A socket is a route: it needs the same
71    /// thought about who may reach it.
72    fn authorize(&self, request: &Request) -> bool {
73        let _ = request;
74        true
75    }
76}
77
78/// A matched route: the handler, and what its pattern captured.
79pub type Matched = (Arc<dyn WebSocketHandler>, Vec<(String, String)>);
80
81/// A handler under a path pattern.
82struct Route {
83    pattern: String,
84    segments: Vec<Segment>,
85    handler: Arc<dyn WebSocketHandler>,
86}
87
88#[derive(Debug, Clone, PartialEq)]
89enum Segment {
90    Literal(String),
91    Capture(String),
92}
93
94/// The WebSocket routes an application declares — `routes/ws.rs`.
95///
96/// Separate from the HTTP router because the two answer different things: an
97/// HTTP route returns a response and is done, a socket route starts a
98/// conversation. Sharing one table would mean one of them pretending.
99///
100/// ```ignore
101/// pub fn routes() -> WebSocketRoutes {
102///     WebSocketRoutes::new()
103///         .add("/ws/rooms/{room}", Chat::new(rooms))
104///         .add("/ws/notifications", Notifications)
105/// }
106/// ```
107#[derive(Default)]
108pub struct WebSocketRoutes {
109    routes: Vec<Route>,
110}
111
112impl WebSocketRoutes {
113    /// No routes. Every upgrade is a `404` until something is declared.
114    pub fn new() -> Self {
115        Self::default()
116    }
117
118    /// Serve `handler` at `pattern`.
119    ///
120    /// `{name}` captures a segment, readable from
121    /// [`Socket::param`](crate::Socket::param). The first matching pattern
122    /// wins, so declare the specific before the general.
123    pub fn add(mut self, pattern: impl Into<String>, handler: impl WebSocketHandler) -> Self {
124        self.push(pattern, Arc::new(handler));
125        self
126    }
127
128    /// Serve a handler you already share.
129    pub fn add_arc(
130        mut self,
131        pattern: impl Into<String>,
132        handler: Arc<dyn WebSocketHandler>,
133    ) -> Self {
134        self.push(pattern, handler);
135        self
136    }
137
138    fn push(&mut self, pattern: impl Into<String>, handler: Arc<dyn WebSocketHandler>) {
139        let pattern = pattern.into();
140        self.routes.push(Route { segments: parse_pattern(&pattern), pattern, handler });
141    }
142
143    /// The handler for `path`, and whatever its pattern captured.
144    pub fn match_path(&self, path: &str) -> Option<Matched> {
145        self.routes.iter().find_map(|route| {
146            match_segments(&route.segments, path).map(|params| (Arc::clone(&route.handler), params))
147        })
148    }
149
150    /// The declared patterns, for `route:list`.
151    pub fn patterns(&self) -> Vec<&str> {
152        self.routes.iter().map(|route| route.pattern.as_str()).collect()
153    }
154
155    /// How many are declared.
156    pub fn len(&self) -> usize {
157        self.routes.len()
158    }
159
160    /// Whether none are.
161    pub fn is_empty(&self) -> bool {
162        self.routes.is_empty()
163    }
164}
165
166impl std::fmt::Debug for WebSocketRoutes {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        f.debug_struct("WebSocketRoutes").field("patterns", &self.patterns()).finish()
169    }
170}
171
172fn parse_pattern(pattern: &str) -> Vec<Segment> {
173    pattern
174        .trim_matches('/')
175        .split('/')
176        .map(|segment| match segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
177            Some(name) => Segment::Capture(name.to_string()),
178            None => Segment::Literal(segment.to_string()),
179        })
180        .collect()
181}
182
183/// Match a path, exactly — segment for segment.
184///
185/// A pattern does not swallow extra segments: `/ws/rooms/{room}` is not
186/// `/ws/rooms/7/messages`. A route that matched more than it named would
187/// serve paths its author never considered.
188fn match_segments(segments: &[Segment], path: &str) -> Option<Vec<(String, String)>> {
189    let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
190    if parts.len() != segments.len() {
191        return None;
192    }
193
194    let mut params = Vec::new();
195    for (segment, part) in segments.iter().zip(parts) {
196        match segment {
197            Segment::Literal(literal) if literal == part => {}
198            Segment::Literal(_) => return None,
199            Segment::Capture(_) if part.is_empty() => return None,
200            Segment::Capture(name) => params.push((name.clone(), part.to_string())),
201        }
202    }
203    Some(params)
204}
205
206/// Every socket currently connected, grouped by a key you choose.
207///
208/// The thing every non-trivial socket application needs and none of them want
209/// to write twice: a chat needs the room's members, a live dashboard needs
210/// everyone watching one account.
211///
212/// ```ignore
213/// rooms.join("lobby", socket.clone());
214/// rooms.send("lobby", Message::text("someone arrived"));
215/// rooms.leave("lobby", socket.id());
216/// ```
217///
218/// Handles to sockets that have gone away are dropped on the next send rather
219/// than reaped on a timer, so a client that vanished without a close frame
220/// costs one failed send and then nothing.
221#[derive(Debug, Default)]
222pub struct Rooms {
223    members: std::sync::Mutex<HashMap<String, Vec<Socket>>>,
224}
225
226impl Rooms {
227    /// An empty registry.
228    pub fn new() -> Self {
229        Self::default()
230    }
231
232    /// Add `socket` to `room`.
233    pub fn join(&self, room: impl Into<String>, socket: Socket) {
234        let mut members = self.lock();
235        let room = members.entry(room.into()).or_default();
236
237        // Joining twice would deliver twice, and a client that reconnects on
238        // the same id is the ordinary way that happens.
239        if !room.iter().any(|existing| existing.id() == socket.id()) {
240            room.push(socket);
241        }
242    }
243
244    /// Remove one socket from `room`.
245    pub fn leave(&self, room: &str, socket: crate::socket::SocketId) {
246        let mut members = self.lock();
247        if let Some(room_members) = members.get_mut(room) {
248            room_members.retain(|existing| existing.id() != socket);
249        }
250    }
251
252    /// Remove a socket from **every** room.
253    ///
254    /// What `on_close` should call: a handler that tracked which rooms a
255    /// socket was in would be keeping a second copy of this map.
256    pub fn leave_all(&self, socket: crate::socket::SocketId) {
257        let mut members = self.lock();
258        for room in members.values_mut() {
259            room.retain(|existing| existing.id() != socket);
260        }
261        members.retain(|_, room| !room.is_empty());
262    }
263
264    /// Send to everyone in `room`. Returns how many received it.
265    pub fn send(&self, room: &str, message: impl Into<Message>) -> usize {
266        self.deliver(room, message.into(), None)
267    }
268
269    /// Send to everyone in `room` **except** one socket — the one that caused
270    /// the message, which has usually shown it already.
271    pub fn send_except(
272        &self,
273        room: &str,
274        except: crate::socket::SocketId,
275        message: impl Into<Message>,
276    ) -> usize {
277        self.deliver(room, message.into(), Some(except))
278    }
279
280    /// How many sockets are in `room`.
281    pub fn count(&self, room: &str) -> usize {
282        self.lock().get(room).map_or(0, Vec::len)
283    }
284
285    /// The rooms that have anyone in them.
286    pub fn rooms(&self) -> Vec<String> {
287        self.lock().keys().cloned().collect()
288    }
289
290    fn deliver(
291        &self,
292        room: &str,
293        message: Message,
294        except: Option<crate::socket::SocketId>,
295    ) -> usize {
296        let mut members = self.lock();
297        let Some(room_members) = members.get_mut(room) else { return 0 };
298
299        let mut delivered = 0;
300        room_members.retain(|socket| {
301            if Some(socket.id()) == except {
302                return true;
303            }
304            match socket.send(message.clone()) {
305                Ok(()) => {
306                    delivered += 1;
307                    true
308                }
309                // Gone. Drop the handle here rather than sweeping later.
310                Err(_) => false,
311            }
312        });
313        delivered
314    }
315
316    fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, Vec<Socket>>> {
317        self.members.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324    use crate::socket::{Outbound, SocketId};
325    use tokio::sync::mpsc;
326
327    struct Echo;
328
329    #[async_trait::async_trait]
330    impl WebSocketHandler for Echo {
331        async fn on_message(&self, socket: &Socket, message: Message) -> Result<()> {
332            socket.send(message)
333        }
334    }
335
336    fn socket(path: &str) -> (Socket, mpsc::UnboundedReceiver<Outbound>) {
337        let (tx, rx) = mpsc::unbounded_channel();
338        (Socket::new(SocketId::next(), path, Vec::new(), tx), rx)
339    }
340
341    #[test]
342    fn a_pattern_captures_its_segments() {
343        let routes = WebSocketRoutes::new().add("/ws/rooms/{room}", Echo);
344
345        let (_, params) = routes.match_path("/ws/rooms/lobby").expect("matches");
346        assert_eq!(params, vec![("room".to_string(), "lobby".to_string())]);
347    }
348
349    #[test]
350    fn a_pattern_does_not_swallow_extra_segments() {
351        let routes = WebSocketRoutes::new().add("/ws/rooms/{room}", Echo);
352
353        assert!(routes.match_path("/ws/rooms/lobby/messages").is_none());
354        assert!(routes.match_path("/ws/rooms").is_none());
355    }
356
357    #[test]
358    fn an_undeclared_path_matches_nothing() {
359        let routes = WebSocketRoutes::new().add("/ws/chat", Echo);
360        assert!(routes.match_path("/ws/other").is_none());
361    }
362
363    #[test]
364    fn the_first_matching_pattern_wins() {
365        let routes =
366            WebSocketRoutes::new().add("/ws/rooms/lobby", Echo).add("/ws/rooms/{room}", Echo);
367
368        let (_, params) = routes.match_path("/ws/rooms/lobby").expect("matches");
369        assert!(params.is_empty(), "the literal route should win");
370    }
371
372    #[tokio::test]
373    async fn a_room_delivers_to_its_members() {
374        let rooms = Rooms::new();
375        let (first, mut first_rx) = socket("/ws");
376        let (second, mut second_rx) = socket("/ws");
377
378        rooms.join("lobby", first.clone());
379        rooms.join("lobby", second);
380
381        assert_eq!(rooms.send("lobby", "hello"), 2);
382        assert!(first_rx.try_recv().is_ok());
383        assert!(second_rx.try_recv().is_ok());
384    }
385
386    #[tokio::test]
387    async fn send_except_skips_the_sender() {
388        let rooms = Rooms::new();
389        let (first, mut first_rx) = socket("/ws");
390        let (second, mut second_rx) = socket("/ws");
391        let sender = first.id();
392
393        rooms.join("lobby", first);
394        rooms.join("lobby", second);
395
396        assert_eq!(rooms.send_except("lobby", sender, "hello"), 1);
397        assert!(first_rx.try_recv().is_err(), "the sender should not get their own message");
398        assert!(second_rx.try_recv().is_ok());
399    }
400
401    #[tokio::test]
402    async fn joining_twice_delivers_once() {
403        let rooms = Rooms::new();
404        let (socket, _rx) = socket("/ws");
405
406        rooms.join("lobby", socket.clone());
407        rooms.join("lobby", socket);
408
409        assert_eq!(rooms.count("lobby"), 1);
410    }
411
412    #[tokio::test]
413    async fn a_socket_that_went_away_is_dropped_on_the_next_send() {
414        let rooms = Rooms::new();
415        let (alive, _alive_rx) = socket("/ws");
416        let (gone, gone_rx) = socket("/ws");
417
418        rooms.join("lobby", alive);
419        rooms.join("lobby", gone);
420        drop(gone_rx);
421
422        assert_eq!(rooms.send("lobby", "hello"), 1, "only the live one received it");
423        assert_eq!(rooms.count("lobby"), 1, "and the dead handle is gone");
424    }
425
426    #[tokio::test]
427    async fn leaving_every_room_is_one_call() {
428        let rooms = Rooms::new();
429        let (socket, _rx) = socket("/ws");
430        let id = socket.id();
431
432        rooms.join("a", socket.clone());
433        rooms.join("b", socket);
434        rooms.leave_all(id);
435
436        assert_eq!(rooms.count("a"), 0);
437        assert!(rooms.rooms().is_empty(), "empty rooms are forgotten");
438    }
439}