use std::collections::HashMap;
use std::fmt;
use std::sync::{Arc, Mutex, PoisonError};
use crate::realtime::{Broadcast, ChannelPayload, Subscription};
use super::channel::NotificationError;
use super::notification::BroadcastContent;
pub trait BroadcastChannels: Send + Sync + fmt::Debug {
fn channel_for(&self, notifiable_key: &str) -> Option<Broadcast>;
}
impl<T: BroadcastChannels + ?Sized> BroadcastChannels for Arc<T> {
fn channel_for(&self, notifiable_key: &str) -> Option<Broadcast> {
(**self).channel_for(notifiable_key)
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct PerRecipientChannels {
capacity: usize,
channels: Arc<Mutex<HashMap<String, Broadcast>>>,
}
impl PerRecipientChannels {
#[must_use]
pub fn new(capacity: usize) -> Option<Self> {
if capacity == 0 {
return None;
}
Some(Self {
capacity,
channels: Arc::new(Mutex::new(HashMap::new())),
})
}
#[must_use]
pub fn capacity(&self) -> usize {
self.capacity
}
#[must_use]
pub fn subscribe(&self, notifiable_key: &str) -> Subscription {
let mut channels = self.lock();
channels.retain(|key, channel| channel.subscriber_count() > 0 || key == notifiable_key);
let channel = channels
.entry(notifiable_key.to_owned())
.or_insert_with(|| {
Broadcast::new(self.capacity)
.expect("capacity is non-zero, checked in PerRecipientChannels::new")
})
.clone();
channel.subscribe()
}
#[must_use]
pub fn connections(&self, notifiable_key: &str) -> usize {
self.lock()
.get(notifiable_key)
.map_or(0, Broadcast::subscriber_count)
}
#[must_use]
pub fn len(&self) -> usize {
self.lock().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.lock().is_empty()
}
fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, Broadcast>> {
self.channels.lock().unwrap_or_else(PoisonError::into_inner)
}
}
impl fmt::Debug for PerRecipientChannels {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PerRecipientChannels")
.field("capacity", &self.capacity)
.field("recipients", &self.len())
.finish()
}
}
impl BroadcastChannels for PerRecipientChannels {
fn channel_for(&self, notifiable_key: &str) -> Option<Broadcast> {
self.lock().get(notifiable_key).cloned()
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct BroadcastNotifications {
channels: Arc<dyn BroadcastChannels>,
}
impl BroadcastNotifications {
#[must_use]
pub fn new(channels: impl BroadcastChannels + 'static) -> Self {
Self {
channels: Arc::new(channels),
}
}
pub fn push(
&self,
notifiable_key: &str,
content: &BroadcastContent,
) -> Result<usize, NotificationError> {
let Some(channel) = self.channels.channel_for(notifiable_key) else {
return Ok(0);
};
let envelope = serde_json::json!({
"kind": content.kind(),
"data": content.data(),
});
let bytes = serde_json::to_vec(&envelope)
.map_err(|error| NotificationError::Encode(error.to_string()))?;
Ok(channel
.publish(ChannelPayload::from_bytes(bytes))
.unwrap_or(0))
}
}
impl fmt::Debug for BroadcastNotifications {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BroadcastNotifications")
.field("channels", &self.channels)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn content() -> BroadcastContent {
BroadcastContent::new("mention", serde_json::json!({ "by": "ada" }))
}
#[test]
fn a_capacity_of_zero_is_refused() {
assert!(PerRecipientChannels::new(0).is_none());
assert!(PerRecipientChannels::new(1).is_some());
}
#[test]
fn a_recipient_with_no_connection_has_no_channel() {
let channels = PerRecipientChannels::new(8).unwrap();
assert!(channels.channel_for("user:1").is_none());
assert_eq!(channels.connections("user:1"), 0);
assert!(channels.is_empty());
}
#[test]
fn subscribing_creates_the_channel_and_dropping_releases_it() {
let channels = PerRecipientChannels::new(8).unwrap();
let first = channels.subscribe("user:1");
assert_eq!(channels.connections("user:1"), 1);
assert_eq!(channels.len(), 1);
let second = channels.subscribe("user:1");
assert_eq!(channels.connections("user:1"), 2);
assert_eq!(channels.len(), 1, "one entry, two connections");
drop(first);
assert_eq!(channels.connections("user:1"), 1);
drop(second);
assert_eq!(channels.connections("user:1"), 0);
}
#[test]
fn a_disconnected_recipients_entry_is_swept() {
let channels = PerRecipientChannels::new(8).unwrap();
drop(channels.subscribe("user:1"));
assert_eq!(channels.len(), 1, "the entry outlives the connection");
let _ada = channels.subscribe("user:2");
assert_eq!(channels.len(), 1);
assert!(channels.channel_for("user:1").is_none());
}
#[test]
fn the_sweep_never_takes_the_entry_being_subscribed_to() {
let channels = PerRecipientChannels::new(8).unwrap();
drop(channels.subscribe("user:1"));
let held = channels.subscribe("user:1");
assert_eq!(channels.connections("user:1"), 1);
drop(held);
}
#[test]
fn a_push_to_nobody_reaches_nobody_and_is_not_an_error() {
let channels = PerRecipientChannels::new(8).unwrap();
let broadcast = BroadcastNotifications::new(channels);
assert_eq!(broadcast.push("user:1", &content()).unwrap(), 0);
}
#[test]
fn a_push_reaches_every_connection_of_that_recipient() {
let channels = PerRecipientChannels::new(8).unwrap();
let broadcast = BroadcastNotifications::new(channels.clone());
let _one = channels.subscribe("user:1");
let _two = channels.subscribe("user:1");
assert_eq!(broadcast.push("user:1", &content()).unwrap(), 2);
}
#[tokio::test]
async fn a_subscriber_receives_the_kind_and_the_data() {
let channels = PerRecipientChannels::new(8).unwrap();
let broadcast = BroadcastNotifications::new(channels.clone());
let mut ada = channels.subscribe("user:1");
broadcast.push("user:1", &content()).unwrap();
let payload = ada.recv().await.unwrap();
let decoded: serde_json::Value = serde_json::from_slice(payload.as_bytes()).unwrap();
assert_eq!(decoded["kind"], "mention");
assert_eq!(decoded["data"]["by"], "ada");
}
#[tokio::test]
async fn one_recipients_push_does_not_reach_another() {
let channels = PerRecipientChannels::new(8).unwrap();
let broadcast = BroadcastNotifications::new(channels.clone());
let _ada = channels.subscribe("user:1");
let mut grace = channels.subscribe("user:2");
assert_eq!(broadcast.push("user:1", &content()).unwrap(), 1);
assert!(
tokio::time::timeout(std::time::Duration::from_millis(50), grace.recv())
.await
.is_err(),
"a push addressed to user:1 reached user:2"
);
}
#[test]
fn debug_does_not_print_who_is_online() {
let channels = PerRecipientChannels::new(8).unwrap();
let _ada = channels.subscribe("user:secret-identifier");
let rendered = format!("{channels:?}");
assert!(!rendered.contains("secret-identifier"), "{rendered}");
assert!(rendered.contains("capacity: 8"), "{rendered}");
let broadcast = format!("{:?}", BroadcastNotifications::new(channels));
assert!(!broadcast.contains("secret-identifier"), "{broadcast}");
}
#[test]
fn a_custom_resolver_can_group_connections_however_it_likes() {
#[derive(Debug)]
struct TeamChannels {
team: Broadcast,
}
impl BroadcastChannels for TeamChannels {
fn channel_for(&self, notifiable_key: &str) -> Option<Broadcast> {
notifiable_key
.starts_with("team:acme:")
.then(|| self.team.clone())
}
}
let team = Broadcast::new(8).unwrap();
let subscription = team.subscribe();
let broadcast = BroadcastNotifications::new(TeamChannels { team });
assert_eq!(broadcast.push("team:acme:ada", &content()).unwrap(), 1);
assert_eq!(broadcast.push("team:other:grace", &content()).unwrap(), 0);
drop(subscription);
}
}