use std::collections::HashMap;
use std::sync::{Arc, Mutex, MutexGuard};
use tokio::sync::broadcast;
struct Room<T> {
tx: broadcast::Sender<T>,
subscribers: usize,
}
struct Shared<T> {
rooms: Mutex<HashMap<String, Room<T>>>,
capacity: usize,
}
pub struct Hub<T> {
shared: Arc<Shared<T>>,
}
impl<T> Clone for Hub<T> {
fn clone(&self) -> Self {
Self {
shared: self.shared.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Delivery<T> {
Message(T),
Missed(u64),
}
pub struct Subscription<T> {
rx: broadcast::Receiver<T>,
room: String,
hub: Hub<T>,
}
impl<T: Clone + Send + 'static> Hub<T> {
pub fn new(capacity: usize) -> Self {
Self {
shared: Arc::new(Shared {
rooms: Mutex::new(HashMap::new()),
capacity: capacity.max(1),
}),
}
}
fn lock(&self) -> MutexGuard<'_, HashMap<String, Room<T>>> {
self.shared.rooms.lock().unwrap_or_else(|e| e.into_inner())
}
pub fn subscribe(&self, room: &str) -> Subscription<T> {
let mut rooms = self.lock();
let entry = rooms.entry(room.to_string()).or_insert_with(|| Room {
tx: broadcast::channel(self.shared.capacity).0,
subscribers: 0,
});
entry.subscribers += 1;
Subscription {
rx: entry.tx.subscribe(),
room: room.to_string(),
hub: self.clone(),
}
}
pub fn publish(&self, room: &str, message: T) -> usize {
let rooms = self.lock();
rooms
.get(room)
.map(|r| r.tx.send(message).unwrap_or(0))
.unwrap_or(0)
}
pub fn members(&self, room: &str) -> usize {
self.lock().get(room).map_or(0, |r| r.subscribers)
}
pub fn rooms(&self) -> Vec<(String, usize)> {
let mut rooms: Vec<_> = self
.lock()
.iter()
.map(|(name, r)| (name.clone(), r.subscribers))
.collect();
rooms.sort();
rooms
}
}
impl<T: Clone + Send + 'static> Subscription<T> {
pub async fn recv(&mut self) -> Option<Delivery<T>> {
match self.rx.recv().await {
Ok(m) => Some(Delivery::Message(m)),
Err(broadcast::error::RecvError::Lagged(n)) => Some(Delivery::Missed(n)),
Err(broadcast::error::RecvError::Closed) => None,
}
}
pub fn room(&self) -> &str {
&self.room
}
}
impl<T> Drop for Subscription<T> {
fn drop(&mut self) {
let mut rooms = self
.hub
.shared
.rooms
.lock()
.unwrap_or_else(|e| e.into_inner());
if let Some(r) = rooms.get_mut(&self.room) {
r.subscribers = r.subscribers.saturating_sub(1);
if r.subscribers == 0 {
rooms.remove(&self.room);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn rooms_are_isolated_and_counted() {
let hub: Hub<u32> = Hub::new(8);
let mut a1 = hub.subscribe("a");
let mut a2 = hub.subscribe("a");
let mut b = hub.subscribe("b");
assert_eq!(hub.publish("a", 1), 2);
assert_eq!(hub.publish("b", 2), 1);
assert_eq!(hub.publish("nobody", 3), 0);
assert_eq!(a1.recv().await, Some(Delivery::Message(1)));
assert_eq!(a2.recv().await, Some(Delivery::Message(1)));
assert_eq!(b.recv().await, Some(Delivery::Message(2)));
assert_eq!(hub.rooms(), [("a".to_string(), 2), ("b".to_string(), 1)]);
assert_eq!(a1.room(), "a");
}
#[tokio::test]
async fn late_joiners_only_see_new_messages_and_empty_rooms_vanish() {
let hub: Hub<&'static str> = Hub::new(8);
let early = hub.subscribe("r");
hub.publish("r", "before");
let mut late = hub.subscribe("r");
hub.publish("r", "after");
assert_eq!(late.recv().await, Some(Delivery::Message("after")));
drop(early);
assert_eq!(hub.members("r"), 1);
drop(late);
assert_eq!(hub.members("r"), 0);
assert!(
hub.rooms().is_empty(),
"the last member takes the room with it"
);
assert_eq!(hub.publish("r", "void"), 0);
}
#[tokio::test]
async fn slow_subscribers_learn_what_they_missed_and_do_not_block_others() {
let hub: Hub<u32> = Hub::new(2);
let mut slow = hub.subscribe("r");
let mut fast = hub.subscribe("r");
for i in 1..=5 {
hub.publish("r", i);
assert_eq!(fast.recv().await, Some(Delivery::Message(i)));
}
assert_eq!(slow.recv().await, Some(Delivery::Missed(3)));
assert_eq!(slow.recv().await, Some(Delivery::Message(4)));
assert_eq!(slow.recv().await, Some(Delivery::Message(5)));
}
#[tokio::test]
async fn clones_share_rooms_and_zero_capacity_is_bumped() {
let hub: Hub<u8> = Hub::new(0);
let other = hub.clone();
let mut sub = other.subscribe("x");
assert_eq!(hub.publish("x", 9), 1);
assert_eq!(sub.recv().await, Some(Delivery::Message(9)));
}
}