#![allow(clippy::missing_docs_in_private_items)]
use std::collections::{HashMap, HashSet};
use std::fmt::Display;
use arcstr::ArcStr;
use thiserror::Error;
use crate::qos::QoS;
use crate::topic_match::TopicPath;
use crate::topic_matcher::{TopicMatcherError, TopicMatcherNode};
use crate::topic_pattern_item::TopicPatternError;
use crate::topic_pattern_path::TopicPatternPath;
#[derive(Error, Debug, Clone, PartialEq, Eq)]
pub enum TopicRouterError {
#[error("Invalid topic pattern: {0}")]
InvalidPattern(#[from] TopicPatternError),
#[error("Topic matching failed: {0}")]
MatchingFailed(#[from] TopicMatcherError),
#[error("Subscription {id:?} not found")]
SubscriptionNotFound {
id: SubscriptionId,
},
#[error("Topic '{topic}' is invalid for routing: {reason}")]
InvalidRoutingTopic {
topic: String,
reason: String,
},
#[error("Internal routing state corrupted: {details}")]
InternalStateCorrupted {
details: String,
},
}
impl TopicRouterError {
pub fn subscription_not_found(id: SubscriptionId) -> Self {
Self::SubscriptionNotFound { id }
}
pub fn invalid_routing_topic(
topic: impl Into<String>,
reason: impl Into<String>,
) -> Self {
Self::InvalidRoutingTopic {
topic: topic.into(),
reason: reason.into(),
}
}
pub fn internal_state_corrupted(details: impl Into<String>) -> Self {
Self::InternalStateCorrupted {
details: details.into(),
}
}
}
#[derive(Debug, Eq, PartialEq, Hash, Copy, Clone)]
pub struct SubscriptionId(usize);
impl Display for SubscriptionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "SubscriptionId({})", self.0)
}
}
type SubscriptionTable<T> = HashMap<SubscriptionId, T>;
pub struct TopicRouter<T> {
topic_matcher: TopicMatcherNode<SubscriptionTable<T>>,
subscriptions: SubscriptionTable<(TopicPatternPath, QoS)>,
next_id: usize,
}
impl<T> Default for TopicRouter<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> TopicRouter<T> {
pub fn new() -> Self {
Self {
topic_matcher: TopicMatcherNode::new(),
subscriptions: SubscriptionTable::new(),
next_id: 0,
}
}
pub fn add_subscription(
&mut self,
topic: TopicPatternPath,
qos: impl Into<QoS>,
subscription: T,
) -> (bool, SubscriptionId) {
let qos = qos.into();
let subscription_table =
self.topic_matcher.get_or_create_subscription_table(&topic);
let needs_subscribe = subscription_table
.keys()
.map(|id| {
self.subscriptions
.get(id)
.unwrap_or_else(|| {
panic!(
"BUG: Subscription ID {id:?} exists in topic \
matcher but missing from subscriptions. Topic: \
{topic}"
)
})
.1
})
.max_by_key(|qos| *qos as u8)
.is_none_or(|max| qos > max);
let id = SubscriptionId(self.next_id);
self.next_id = self.next_id.wrapping_add(1);
subscription_table.insert(id, subscription);
self.subscriptions.insert(id, (topic, qos));
(needs_subscribe, id)
}
pub fn unsubscribe(
&mut self,
id: &SubscriptionId,
) -> Result<(bool, TopicPatternPath), TopicRouterError> {
let topic = self.subscriptions.remove(id);
match topic {
| Some((topic_pattern, _qos)) => {
let resolved_segments = topic_pattern.resolve_bound_segments();
let topic_now_empty = self.topic_matcher.update_node(
&resolved_segments,
|table| {
table.remove(id);
},
)?;
Ok((topic_now_empty, topic_pattern))
}
| None => Err(TopicRouterError::subscription_not_found(*id)),
}
}
pub fn get_subscribers<'a>(
&'a self,
topic: &TopicPath,
) -> Vec<(&'a SubscriptionId, &'a (TopicPatternPath, QoS), &'a T)> {
let subscribers = self.topic_matcher.find_by_path(topic);
subscribers
.into_iter()
.flat_map(|hash_map| hash_map.iter())
.map(|(id, subscription)| {
let topic_pattern = self
.subscriptions
.get(id)
.expect("Subscription ID should exist in subscriptions");
(id, topic_pattern, subscription)
})
.collect()
}
pub fn get_active_subscriptions(
&self,
) -> impl Iterator<Item = &(TopicPatternPath, QoS)> {
self.subscriptions.values()
}
#[allow(dead_code)] fn get_max_qos_for_topic(
&self,
topic: &TopicPatternPath,
topic_subscriptions: &HashMap<SubscriptionId, T>,
) -> QoS {
debug_assert!(
!topic_subscriptions.is_empty(),
"topic_subscriptions should never be empty - this is guaranteed \
by collect_active_subscriptions()"
);
let max_qos = topic_subscriptions
.keys()
.map(|id| {
self.subscriptions
.get(id)
.unwrap_or_else(|| {
panic!(
"BUG: Subscription ID {id:?} exists in topic \
matcher but missing from subscriptions. Topic: \
{topic}"
)
})
.1
})
.max_by_key(|qos| *qos as u8)
.unwrap();
max_qos
}
pub fn get_topics_for_unsubscribe(&self) -> HashSet<ArcStr> {
self.subscriptions
.values()
.map(|(topic, _)| topic.mqtt_pattern())
.collect()
}
pub fn get_topics_for_resubscribe(&self) -> HashMap<ArcStr, QoS> {
let mut result: HashMap<ArcStr, QoS> = HashMap::new();
for (topic, qos) in self.subscriptions.values() {
let mqtt_pattern = topic.mqtt_pattern();
result
.entry(mqtt_pattern)
.and_modify(|existing_qos| {
if *qos > *existing_qos {
*existing_qos = *qos;
}
})
.or_insert(*qos);
}
result
}
pub fn cleanup(&mut self) {
self.topic_matcher = TopicMatcherNode::new();
self.subscriptions.clear();
self.next_id = 0;
}
pub fn get_topic_by_id(
&self,
id: &SubscriptionId,
) -> Result<&(TopicPatternPath, QoS), TopicRouterError> {
self.subscriptions
.get(id)
.ok_or(TopicRouterError::subscription_not_found(*id))
}
}