Skip to main content

rws_server/channels/
mod.rs

1//! Channels (18.35.0, #94): named topics that pages and other clients
2//! subscribe to over SSE or WebSocket, and that server code publishes to —
3//! "a comment was added", "the import finished". In-process: replicas
4//! don't see each other's messages (publish from where the change happens,
5//! or bridge a queue into [`Hub::publish`]).
6//!
7//! ```rust
8//! # extern crate rws_server as rust_web_server;
9//! use rust_web_server::app::App;
10//! use rust_web_server::channels::Hub;
11//!
12//! let hub = Hub::new();
13//! let app = App::with_state(hub.clone())
14//!     // EventSource("/rooms/lobby/events") receives every message.
15//!     .get("/rooms/:room/events", |_, params, _, hub| hub.sse(params.get("room").unwrap_or("lobby")))
16//!     // A WebSocket client receives them too; what it sends is published.
17//!     .websocket("/rooms/:room/ws", |ws, params, hub| {
18//!         let room = params.get("room").unwrap_or("lobby").to_string();
19//!         hub.serve_websocket(ws, &room, |hub, room, text| { hub.publish(room, text); });
20//!     });
21//!
22//! let delivered = hub.publish("lobby", "hello");
23//! assert_eq!(0, delivered); // nobody is listening yet
24//! ```
25//!
26//! Delivery is best effort and never blocks on a slow SSE client: a
27//! message that doesn't fit its buffer is dropped for that client.
28//! WebSocket messages are written by the publishing thread. Subscribers
29//! that have gone away are removed on the next publish to their topic.
30
31#[cfg(test)]
32mod tests;
33
34use std::collections::HashMap;
35use std::sync::mpsc::{channel, Receiver, Sender};
36use std::sync::{Arc, Mutex};
37
38use crate::response::Response;
39use crate::sse::{SseEvent, SseSender, SseStream};
40use crate::websocket::{Message, WebSocketConnection, WebSocketSender};
41
42enum Subscriber {
43    Sse(SseSender),
44    WebSocket(WebSocketSender),
45    Channel(Sender<String>),
46}
47
48impl Subscriber {
49    /// Deliver `message`: `Some(true)` delivered, `Some(false)` dropped
50    /// (full), `None` the subscriber is gone.
51    fn deliver(&self, topic: &str, message: &str) -> Option<bool> {
52        match self {
53            Subscriber::Sse(tx) => tx.try_send(SseEvent::data(message).event_type(topic)).ok(),
54            Subscriber::WebSocket(ws) => (!ws.is_closed() && ws.send_text(message).is_ok()).then_some(true),
55            Subscriber::Channel(tx) => tx.send(message.to_string()).ok().map(|_| true),
56        }
57    }
58}
59
60/// Topics and their subscribers; cheap to clone (all clones share them).
61#[derive(Clone, Default)]
62pub struct Hub {
63    topics: Arc<Mutex<HashMap<String, Vec<Subscriber>>>>,
64}
65
66impl Hub {
67    pub fn new() -> Self {
68        Hub::default()
69    }
70
71    fn add(&self, topic: &str, subscriber: Subscriber) {
72        let mut topics = self.topics.lock().unwrap_or_else(|e| e.into_inner());
73        topics.entry(topic.to_string()).or_default().push(subscriber);
74    }
75
76    /// Send `message` to every subscriber of `topic`; the number it was
77    /// delivered to. SSE clients get it as an event named after the topic
78    /// (`EventSource.addEventListener(topic, …)`), WebSocket clients as a
79    /// text message.
80    pub fn publish(&self, topic: &str, message: &str) -> usize {
81        // Take the subscribers out so a slow WebSocket write doesn't hold
82        // the lock; put the live ones back after.
83        let subscribers = {
84            let mut topics = self.topics.lock().unwrap_or_else(|e| e.into_inner());
85            match topics.get_mut(topic) {
86                Some(list) => std::mem::take(list),
87                None => return 0,
88            }
89        };
90        let mut delivered = 0;
91        let mut live = Vec::with_capacity(subscribers.len());
92        for subscriber in subscribers {
93            if let Some(sent) = subscriber.deliver(topic, message) {
94                delivered += sent as usize;
95                live.push(subscriber);
96            }
97        }
98        let mut topics = self.topics.lock().unwrap_or_else(|e| e.into_inner());
99        let list = topics.entry(topic.to_string()).or_default();
100        // Subscribers that joined while this publish ran come after.
101        live.append(list);
102        if live.is_empty() {
103            topics.remove(topic);
104        } else {
105            *list = live;
106        }
107        delivered
108    }
109
110    /// A streaming SSE response subscribed to `topic`: return it from the
111    /// handler.
112    pub fn sse(&self, topic: &str) -> Response {
113        let (tx, response) = SseStream::new().start();
114        self.add(topic, Subscriber::Sse(tx));
115        response
116    }
117
118    /// Subscribe a WebSocket client (its sender) to `topic`.
119    pub fn subscribe_websocket(&self, topic: &str, sender: WebSocketSender) {
120        self.add(topic, Subscriber::WebSocket(sender));
121    }
122
123    /// Subscribe `ws` to `topic` and read its messages until it closes,
124    /// handing each text message to `on_message(hub, topic, text)`. Run it
125    /// in a `.websocket(…)` handler.
126    pub fn serve_websocket<F>(&self, mut ws: WebSocketConnection, topic: &str, on_message: F)
127    where
128        F: Fn(&Hub, &str, &str),
129    {
130        self.subscribe_websocket(topic, ws.sender());
131        while let Ok(Some(message)) = ws.recv() {
132            if let Message::Text(text) = message {
133                on_message(self, topic, &text);
134            }
135        }
136        let _ = ws.close(1000, "");
137    }
138
139    /// Subscribe in-process: every message to `topic` arrives on the
140    /// receiver until it's dropped.
141    pub fn subscribe(&self, topic: &str) -> Receiver<String> {
142        let (tx, rx) = channel();
143        self.add(topic, Subscriber::Channel(tx));
144        rx
145    }
146
147    /// How many subscribers `topic` has (some may have gone away since the
148    /// last publish).
149    pub fn subscribers(&self, topic: &str) -> usize {
150        self.topics.lock().unwrap_or_else(|e| e.into_inner()).get(topic).map_or(0, Vec::len)
151    }
152
153    /// The topics that have subscribers, sorted.
154    pub fn topics(&self) -> Vec<String> {
155        let mut topics: Vec<String> = self.topics.lock().unwrap_or_else(|e| e.into_inner()).keys().cloned().collect();
156        topics.sort();
157        topics
158    }
159}