moirai_transport/
router.rs1use crate::{lock_mutex, transport::Address, Transport, TransportResult};
2use std::{
3 collections::HashMap,
4 fmt,
5 sync::{Arc, Mutex},
6};
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub struct RemoteAddress {
11 pub host: String,
13 pub port: u16,
15 pub service: String,
17}
18
19impl fmt::Display for RemoteAddress {
20 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 write!(f, "{}://{}:{}", self.service, self.host, self.port)
22 }
23}
24
25pub struct MessageRouter<T: Transport> {
35 transport: Arc<T>,
36 subscriptions: Mutex<HashMap<String, Vec<Address>>>,
37}
38
39impl<T: Transport> MessageRouter<T> {
40 pub fn new(transport: Arc<T>) -> Self {
42 Self {
43 transport,
44 subscriptions: Mutex::new(HashMap::new()),
45 }
46 }
47
48 pub fn subscribe(&self, topic: &str, address: Address) {
51 let mut subs = lock_mutex(&self.subscriptions);
52 let entry = subs.entry(topic.to_string()).or_default();
53 if !entry.contains(&address) {
54 entry.push(address);
55 }
56 }
57
58 pub fn unsubscribe(&self, topic: &str, address: &Address) -> bool {
61 let mut subs = lock_mutex(&self.subscriptions);
62 if let Some(entry) = subs.get_mut(topic) {
63 let before = entry.len();
64 entry.retain(|a| a != address);
65 let removed = entry.len() != before;
66 if entry.is_empty() {
67 subs.remove(topic);
68 }
69 return removed;
70 }
71 false
72 }
73
74 pub fn publish(&self, topic: &str, data: Vec<u8>) -> TransportResult<usize> {
83 let targets: Vec<Address> = {
86 let subs = lock_mutex(&self.subscriptions);
87 match subs.get(topic) {
88 Some(addresses) => addresses.clone(),
89 None => return Ok(0),
90 }
91 };
92
93 let mut delivered = 0;
98 let Some(last) = targets.len().checked_sub(1) else {
99 return Ok(0);
100 };
101 let mut data = Some(data);
102 for (index, addr) in targets.iter().enumerate() {
103 let payload = if index == last {
104 data.take()
105 .expect("invariant: original buffer moved exactly once, at the last subscriber")
106 } else {
107 data.as_ref()
108 .expect("invariant: original buffer present until the last subscriber")
109 .clone()
110 };
111 self.transport.send(addr, payload)?;
112 delivered += 1;
113 }
114 Ok(delivered)
115 }
116
117 pub fn subscriber_count(&self, topic: &str) -> usize {
119 lock_mutex(&self.subscriptions)
120 .get(topic)
121 .map_or(0, Vec::len)
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use crate::{InMemoryTransport, Transport};
129
130 #[test]
131 fn message_router_delivers_to_each_subscriber_once() {
132 let transport = Arc::new(InMemoryTransport::new());
133 let router = MessageRouter::new(Arc::clone(&transport));
134 let sub_a = Address::Local("sub_a".to_string());
135 let sub_b = Address::Local("sub_b".to_string());
136
137 router.subscribe("topic", sub_a.clone());
138 router.subscribe("topic", sub_b.clone());
139 router.subscribe("topic", sub_a.clone()); assert_eq!(router.subscriber_count("topic"), 2);
141
142 let delivered = router.publish("topic", vec![1, 2, 3]).unwrap();
143 assert_eq!(delivered, 2);
144
145 assert_eq!(transport.recv(&sub_a).unwrap(), vec![1, 2, 3]);
146 assert_eq!(transport.recv(&sub_b).unwrap(), vec![1, 2, 3]);
147 }
148
149 #[test]
150 fn message_router_single_subscriber_receives_moved_buffer() {
151 let transport = Arc::new(InMemoryTransport::new());
152 let router = MessageRouter::new(Arc::clone(&transport));
153 let sub = Address::Local("solo".to_string());
154 router.subscribe("t", sub.clone());
155
156 let delivered = router.publish("t", vec![7, 8, 9]).unwrap();
157 assert_eq!(delivered, 1);
158 assert_eq!(transport.recv(&sub).unwrap(), vec![7, 8, 9]);
159 }
160
161 #[test]
162 fn message_router_unknown_topic_delivers_nothing() {
163 let transport = Arc::new(InMemoryTransport::new());
164 let router = MessageRouter::new(transport);
165 assert_eq!(router.publish("absent", vec![0]).unwrap(), 0);
166 }
167
168 #[test]
169 fn message_router_unsubscribe_stops_delivery() {
170 let transport = Arc::new(InMemoryTransport::new());
171 let router = MessageRouter::new(Arc::clone(&transport));
172 let sub = Address::Local("s".to_string());
173
174 router.subscribe("t", sub.clone());
175 assert!(router.unsubscribe("t", &sub));
176 assert!(
177 !router.unsubscribe("t", &sub),
178 "second unsubscribe is a no-op"
179 );
180 assert_eq!(router.subscriber_count("t"), 0);
181 assert_eq!(router.publish("t", vec![9]).unwrap(), 0);
182 }
183}