whycodes_core/
swarm_hub.rs1use std::collections::HashMap;
7use std::sync::{Arc, Mutex};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct SwarmMessage {
12 pub from: String,
13 pub to: String,
14 pub text: String,
15}
16
17pub type SwarmMessageListener = Arc<dyn Fn(SwarmMessage) + Send + Sync>;
19
20#[derive(Clone, Default)]
22pub struct SwarmHub {
23 inner: Arc<SwarmHubInner>,
24}
25
26struct SwarmHubInner {
27 inboxes: Mutex<HashMap<String, Vec<SwarmMessage>>>,
29 listener: Mutex<Option<SwarmMessageListener>>,
30}
31
32impl Default for SwarmHubInner {
33 fn default() -> Self {
34 Self {
35 inboxes: Mutex::new(HashMap::new()),
36 listener: Mutex::new(None),
37 }
38 }
39}
40
41impl std::fmt::Debug for SwarmHub {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 let n = recover_lock(self.inner.inboxes.lock())
44 .values()
45 .map(|v| v.len())
46 .sum::<usize>();
47 f.debug_struct("SwarmHub").field("queued", &n).finish()
48 }
49}
50
51impl SwarmHub {
52 pub fn new() -> Self {
53 Self::default()
54 }
55
56 pub fn ensure(&self, id: &str) {
58 recover_lock(self.inner.inboxes.lock())
59 .entry(id.to_string())
60 .or_default();
61 }
62
63 pub fn set_listener(&self, listener: Option<SwarmMessageListener>) {
64 *recover_lock(self.inner.listener.lock()) = listener;
65 }
66
67 pub fn send(&self, from: &str, to: &str, text: &str) -> Result<(), String> {
71 let text = text.trim();
72 if text.is_empty() {
73 return Ok(());
74 }
75 let mut boxes = recover_lock(self.inner.inboxes.lock());
76 let targets: Vec<String> = if to.eq_ignore_ascii_case("all") {
77 let mut ids: Vec<String> = boxes.keys().cloned().collect();
78 if !ids.iter().any(|k| k == "parent") {
79 ids.push("parent".into());
80 }
81 ids.into_iter().filter(|id| id != from).collect()
82 } else if boxes.contains_key(to) {
83 vec![to.to_string()]
84 } else {
85 let mut known: Vec<&str> = boxes.keys().map(String::as_str).collect();
86 known.sort_unstable();
87 return Err(format!(
88 "unknown recipient `{to}`; known: all, {}",
89 known.join(", ")
90 ));
91 };
92 for dest in targets {
93 let msg = SwarmMessage {
94 from: from.to_string(),
95 to: dest.clone(),
96 text: text.to_string(),
97 };
98 boxes.entry(dest).or_default().push(msg.clone());
99 self.emit(&msg);
100 }
101 Ok(())
102 }
103
104 fn emit(&self, msg: &SwarmMessage) {
105 if let Some(ref f) = *recover_lock(self.inner.listener.lock()) {
106 f(msg.clone());
107 }
108 }
109
110 pub fn drain(&self, id: &str) -> Vec<SwarmMessage> {
113 recover_lock(self.inner.inboxes.lock())
114 .get_mut(id)
115 .map(std::mem::take)
116 .unwrap_or_default()
117 }
118}
119
120pub(crate) fn recover_lock<T>(res: std::sync::LockResult<T>) -> T {
121 match res {
122 Ok(g) => g,
123 Err(p) => p.into_inner(),
124 }
125}
126
127#[cfg(test)]
128impl SwarmHub {
129 pub(crate) fn poison_inboxes(&self) {
130 poison_mutex(&self.inner.inboxes);
131 }
132 pub(crate) fn poison_listener(&self) {
133 poison_mutex(&self.inner.listener);
134 }
135}
136
137#[cfg(test)]
138fn poison_mutex<T>(m: &Mutex<T>) {
139 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
140 let _g = m.lock().unwrap();
141 panic!("poison");
142 }));
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use std::sync::{Arc, Mutex};
149
150 #[test]
151 fn send_broadcast_drain_and_listener() {
152 let hub = SwarmHub::new();
153 hub.ensure("parent");
154 hub.ensure("w0");
155 hub.send("w0", "parent", " ").unwrap();
156 hub.send("w0", "parent", "hello").unwrap();
157 let got = hub.drain("parent");
158 assert_eq!(got.len(), 1);
159 assert_eq!(got[0].text, "hello");
160 assert!(hub.drain("parent").is_empty());
161
162 let seen = Arc::new(Mutex::new(Vec::new()));
163 let seen2 = Arc::clone(&seen);
164 hub.set_listener(Some(Arc::new(move |msg: SwarmMessage| {
165 seen2.lock().unwrap().push(msg.text);
166 })));
167 hub.send("parent", "all", "broadcast").unwrap();
168 assert!(!hub.drain("w0").is_empty());
169 assert!(!seen.lock().unwrap().is_empty());
170 hub.set_listener(None);
171 assert!(format!("{hub:?}").contains("SwarmHub"));
172 }
173
174 #[test]
175 fn broadcast_still_reaches_an_inbox_after_it_was_drained() {
176 let hub = SwarmHub::new();
177 hub.ensure("parent");
178 hub.ensure("worker-0");
179 hub.ensure("worker-1");
180 hub.send("worker-0", "worker-1", "first").unwrap();
181 assert_eq!(hub.drain("worker-1").len(), 1);
182 hub.send("worker-0", "all", "second").unwrap();
183 let got = hub.drain("worker-1");
184 assert_eq!(got.len(), 1);
185 assert_eq!(got[0].text, "second");
186 assert!(hub.drain("worker-0").is_empty(), "sender skips itself");
187 assert_eq!(hub.drain("parent").len(), 1);
188 }
189}