use crate::WsMessage;
use std::collections::HashMap;
use tokio::sync::mpsc;
#[derive(Default)]
pub struct RoutingTable {
routes: HashMap<String, HashMap<String, mpsc::Sender<WsMessage>>>,
}
impl RoutingTable {
pub fn new() -> Self {
Self {
routes: HashMap::new(),
}
}
pub fn subscribe(
&mut self,
channel: &str,
session_id: &str,
sender: mpsc::Sender<WsMessage>,
) -> bool {
let is_new = !self.routes.contains_key(channel);
self.routes
.entry(channel.to_string())
.or_default()
.insert(session_id.to_string(), sender);
is_new
}
pub fn unsubscribe(&mut self, channel: &str, session_id: &str) -> bool {
if let Some(sessions) = self.routes.get_mut(channel) {
sessions.remove(session_id);
if sessions.is_empty() {
self.routes.remove(channel);
return true;
}
}
false
}
pub fn route(&mut self, channel: &str, event: WsMessage) -> (usize, usize) {
let Some(sessions) = self.routes.get_mut(channel) else {
return (0, 0);
};
let mut delivered = 0usize;
let mut dropped = 0usize;
sessions.retain(|_, sender| match sender.try_send(event.clone()) {
Ok(()) => {
delivered += 1;
true
}
Err(mpsc::error::TrySendError::Full(_)) => {
dropped += 1;
true
}
Err(mpsc::error::TrySendError::Closed(_)) => false,
});
if sessions.is_empty() {
self.routes.remove(channel);
}
(delivered, dropped)
}
pub fn channels(&self) -> Vec<String> {
self.routes.keys().cloned().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{MessageKind, MessageScope};
fn channel_pair(capacity: usize) -> (mpsc::Sender<WsMessage>, mpsc::Receiver<WsMessage>) {
mpsc::channel(capacity)
}
fn sample_event() -> WsMessage {
WsMessage {
scope: MessageScope::Broadcast,
kind: MessageKind::Event,
node_id: None,
payload: serde_json::json!({ "test": true }),
}
}
#[test]
fn first_subscriber_triggers_real_subscribe() {
let mut table = RoutingTable::new();
let (tx, _rx) = channel_pair(4);
assert!(table.subscribe("chan", "session-1", tx));
}
#[test]
fn second_subscriber_does_not_resubscribe() {
let mut table = RoutingTable::new();
let (tx1, _rx1) = channel_pair(4);
let (tx2, _rx2) = channel_pair(4);
assert!(table.subscribe("chan", "session-1", tx1));
assert!(!table.subscribe("chan", "session-2", tx2));
}
#[test]
fn last_unsubscriber_triggers_real_unsubscribe() {
let mut table = RoutingTable::new();
let (tx1, _rx1) = channel_pair(4);
let (tx2, _rx2) = channel_pair(4);
table.subscribe("chan", "session-1", tx1);
table.subscribe("chan", "session-2", tx2);
assert!(!table.unsubscribe("chan", "session-1"));
assert!(table.unsubscribe("chan", "session-2"));
}
#[test]
fn route_delivers_to_all_subscribers() {
let mut table = RoutingTable::new();
let (tx1, mut rx1) = channel_pair(4);
let (tx2, mut rx2) = channel_pair(4);
table.subscribe("chan", "session-1", tx1);
table.subscribe("chan", "session-2", tx2);
let (delivered, dropped) = table.route("chan", sample_event());
assert_eq!(delivered, 2);
assert_eq!(dropped, 0);
assert!(rx1.try_recv().is_ok());
assert!(rx2.try_recv().is_ok());
}
#[test]
fn route_prunes_dropped_receiver() {
let mut table = RoutingTable::new();
let (tx, rx) = channel_pair(4);
table.subscribe("chan", "session-1", tx);
drop(rx);
let (delivered, dropped) = table.route("chan", sample_event());
assert_eq!(delivered, 0);
assert_eq!(dropped, 0);
assert!(table.channels().is_empty());
}
#[test]
fn route_drops_but_keeps_slow_session() {
let mut table = RoutingTable::new();
let (tx, _rx) = channel_pair(1); table.subscribe("chan", "session-1", tx);
let (d1, dropped1) = table.route("chan", sample_event()); let (d2, dropped2) = table.route("chan", sample_event());
assert_eq!((d1, dropped1), (1, 0));
assert_eq!((d2, dropped2), (0, 1));
assert_eq!(table.channels(), vec!["chan".to_string()]);
}
}