1use async_trait::async_trait;
18use serde::{Deserialize, Serialize};
19use tokio::sync::broadcast;
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct DiscoveredPeer {
24 pub node_id: String,
26 pub address: String,
28 pub platform: String,
30 pub source: DiscoverySource,
32}
33
34#[non_exhaustive]
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37pub enum DiscoverySource {
38 SeedPeer,
40 Mdns,
42 Kademlia,
44 PeerExchange,
46 Manual,
48}
49
50#[non_exhaustive]
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub enum MeshPeerEvent {
59 Joined {
61 node_id: String,
63 address: Option<String>,
65 platform: Option<String>,
67 #[serde(default)]
75 verified: bool,
76 },
77 Left {
79 node_id: String,
81 },
82 Alive {
84 node_id: String,
86 },
87 Suspect {
89 node_id: String,
91 },
92 Unreachable {
94 node_id: String,
96 },
97 Recovered {
99 node_id: String,
101 address: Option<String>,
103 #[serde(default)]
105 verified: bool,
106 },
107}
108
109impl MeshPeerEvent {
110 pub fn node_id(&self) -> &str {
112 match self {
113 MeshPeerEvent::Joined { node_id, .. }
114 | MeshPeerEvent::Left { node_id }
115 | MeshPeerEvent::Alive { node_id }
116 | MeshPeerEvent::Suspect { node_id }
117 | MeshPeerEvent::Unreachable { node_id }
118 | MeshPeerEvent::Recovered { node_id, .. } => node_id.as_str(),
119 }
120 }
121}
122
123const DEFAULT_PEER_EVENT_CAPACITY: usize = 256;
125
126#[derive(Clone, Debug)]
132pub struct MeshPeerEventBus {
133 tx: broadcast::Sender<MeshPeerEvent>,
134}
135
136impl MeshPeerEventBus {
137 pub fn new() -> Self {
139 Self::with_capacity(DEFAULT_PEER_EVENT_CAPACITY)
140 }
141
142 pub fn with_capacity(capacity: usize) -> Self {
144 let (tx, _) = broadcast::channel(capacity.max(1));
145 Self { tx }
146 }
147
148 pub fn emit(&self, event: MeshPeerEvent) {
150 let _ = self.tx.send(event);
151 }
152
153 pub fn subscribe(&self) -> broadcast::Receiver<MeshPeerEvent> {
155 self.tx.subscribe()
156 }
157
158 pub fn receiver_count(&self) -> usize {
160 self.tx.receiver_count()
161 }
162
163 pub fn sender(&self) -> broadcast::Sender<MeshPeerEvent> {
165 self.tx.clone()
166 }
167}
168
169impl Default for MeshPeerEventBus {
170 fn default() -> Self {
171 Self::new()
172 }
173}
174
175#[async_trait]
177pub trait DiscoveryBackend: Send + Sync + 'static {
178 fn name(&self) -> &str;
180 async fn start(&mut self) -> Result<(), DiscoveryError>;
182 async fn poll(&mut self) -> Vec<DiscoveredPeer>;
184 async fn stop(&mut self) -> Result<(), DiscoveryError>;
186}
187
188#[non_exhaustive]
190#[derive(Debug, thiserror::Error)]
191pub enum DiscoveryError {
192 #[error("discovery backend error: {0}")]
193 Backend(String),
194 #[error("invalid seed address: {0}")]
195 InvalidAddress(String),
196 #[error("discovery not started")]
197 NotStarted,
198}
199
200pub struct DiscoveryCoordinator {
202 backends: Vec<Box<dyn DiscoveryBackend>>,
203 known_peers: std::collections::HashSet<String>,
204}
205
206impl DiscoveryCoordinator {
207 pub fn new() -> Self {
208 Self {
209 backends: Vec::new(),
210 known_peers: std::collections::HashSet::new(),
211 }
212 }
213
214 pub fn add_backend(&mut self, backend: Box<dyn DiscoveryBackend>) {
216 self.backends.push(backend);
217 }
218
219 pub async fn start_all(&mut self) -> Result<(), DiscoveryError> {
221 for backend in &mut self.backends {
222 backend.start().await?;
223 }
224 Ok(())
225 }
226
227 pub async fn poll_all(&mut self) -> Vec<DiscoveredPeer> {
229 let mut new_peers = Vec::new();
230 for backend in &mut self.backends {
231 let peers = backend.poll().await;
232 for peer in peers {
233 if self.known_peers.insert(peer.node_id.clone()) {
234 new_peers.push(peer);
235 }
236 }
237 }
238 new_peers
239 }
240
241 pub async fn stop_all(&mut self) -> Result<(), DiscoveryError> {
243 for backend in &mut self.backends {
244 backend.stop().await?;
245 }
246 Ok(())
247 }
248
249 pub fn known_peer_count(&self) -> usize {
251 self.known_peers.len()
252 }
253
254 pub fn is_known(&self, node_id: &str) -> bool {
256 self.known_peers.contains(node_id)
257 }
258}
259
260impl Default for DiscoveryCoordinator {
261 fn default() -> Self {
262 Self::new()
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 struct MockBackend {
272 name: &'static str,
273 peers: Vec<DiscoveredPeer>,
274 started: bool,
275 }
276
277 impl MockBackend {
278 fn new(name: &'static str, peers: Vec<DiscoveredPeer>) -> Self {
279 Self {
280 name,
281 peers,
282 started: false,
283 }
284 }
285 }
286
287 #[async_trait]
288 impl DiscoveryBackend for MockBackend {
289 fn name(&self) -> &str {
290 self.name
291 }
292 async fn start(&mut self) -> Result<(), DiscoveryError> {
293 self.started = true;
294 Ok(())
295 }
296 async fn poll(&mut self) -> Vec<DiscoveredPeer> {
297 std::mem::take(&mut self.peers)
298 }
299 async fn stop(&mut self) -> Result<(), DiscoveryError> {
300 self.started = false;
301 Ok(())
302 }
303 }
304
305 fn make_peer(id: &str, source: DiscoverySource) -> DiscoveredPeer {
306 DiscoveredPeer {
307 node_id: id.to_string(),
308 address: format!("127.0.0.1:900{}", id.len()),
309 platform: "linux".into(),
310 source,
311 }
312 }
313
314 #[tokio::test]
315 async fn coordinator_add_and_start() {
316 let mut coord = DiscoveryCoordinator::new();
317 assert_eq!(coord.known_peer_count(), 0);
318
319 let backend = MockBackend::new("mock", vec![make_peer("a", DiscoverySource::Manual)]);
320 coord.add_backend(Box::new(backend));
321 coord.start_all().await.unwrap();
322 }
323
324 #[tokio::test]
325 async fn coordinator_poll_deduplicates() {
326 let mut coord = DiscoveryCoordinator::new();
327
328 let b1 = MockBackend::new(
329 "b1",
330 vec![
331 make_peer("node-1", DiscoverySource::SeedPeer),
332 make_peer("node-2", DiscoverySource::SeedPeer),
333 ],
334 );
335 let b2 = MockBackend::new(
336 "b2",
337 vec![
338 make_peer("node-2", DiscoverySource::Mdns), make_peer("node-3", DiscoverySource::Mdns),
340 ],
341 );
342 coord.add_backend(Box::new(b1));
343 coord.add_backend(Box::new(b2));
344 coord.start_all().await.unwrap();
345
346 let peers = coord.poll_all().await;
347 assert_eq!(peers.len(), 3);
349 assert_eq!(coord.known_peer_count(), 3);
350 assert!(coord.is_known("node-1"));
351 assert!(coord.is_known("node-2"));
352 assert!(coord.is_known("node-3"));
353 }
354
355 #[tokio::test]
356 async fn coordinator_second_poll_empty() {
357 let mut coord = DiscoveryCoordinator::new();
358 let b = MockBackend::new("b", vec![make_peer("x", DiscoverySource::Manual)]);
359 coord.add_backend(Box::new(b));
360 coord.start_all().await.unwrap();
361
362 let first = coord.poll_all().await;
363 assert_eq!(first.len(), 1);
364
365 let second = coord.poll_all().await;
366 assert!(second.is_empty());
367 }
368
369 #[tokio::test]
370 async fn coordinator_stop_all() {
371 let mut coord = DiscoveryCoordinator::new();
372 let b = MockBackend::new("b", vec![]);
373 coord.add_backend(Box::new(b));
374 coord.start_all().await.unwrap();
375 coord.stop_all().await.unwrap();
376 }
377
378 #[test]
379 fn discovered_peer_serde_roundtrip() {
380 let peer = make_peer("serde-test", DiscoverySource::Kademlia);
381 let json = serde_json::to_string(&peer).unwrap();
382 let back: DiscoveredPeer = serde_json::from_str(&json).unwrap();
383 assert_eq!(back.node_id, "serde-test");
384 assert_eq!(back.source, DiscoverySource::Kademlia);
385 }
386
387 #[test]
388 fn mesh_peer_event_node_id() {
389 let e = MeshPeerEvent::Joined {
390 node_id: "n1".into(),
391 address: Some("127.0.0.1:9".into()),
392 platform: None,
393 verified: false,
394 };
395 assert_eq!(e.node_id(), "n1");
396 assert_eq!(
397 MeshPeerEvent::Left {
398 node_id: "n2".into()
399 }
400 .node_id(),
401 "n2"
402 );
403 }
404
405 #[tokio::test]
406 async fn peer_event_bus_delivers_to_subscriber() {
407 let bus = MeshPeerEventBus::new();
408 let mut rx = bus.subscribe();
409 bus.emit(MeshPeerEvent::Alive {
410 node_id: "p".into(),
411 });
412 let got = rx.recv().await.unwrap();
413 assert_eq!(
414 got,
415 MeshPeerEvent::Alive {
416 node_id: "p".into()
417 }
418 );
419 }
420
421 #[test]
422 fn mesh_peer_event_serde_roundtrip() {
423 let e = MeshPeerEvent::Recovered {
424 node_id: "r1".into(),
425 address: Some("10.0.0.2:9489".into()),
426 verified: true,
427 };
428 let json = serde_json::to_string(&e).unwrap();
429 let back: MeshPeerEvent = serde_json::from_str(&json).unwrap();
430 assert_eq!(back, e);
431 }
432
433 #[test]
434 fn discovery_source_serde_roundtrip() {
435 let sources = [
436 DiscoverySource::SeedPeer,
437 DiscoverySource::Mdns,
438 DiscoverySource::Kademlia,
439 DiscoverySource::PeerExchange,
440 DiscoverySource::Manual,
441 ];
442 for src in &sources {
443 let json = serde_json::to_string(src).unwrap();
444 let back: DiscoverySource = serde_json::from_str(&json).unwrap();
445 assert_eq!(&back, src);
446 }
447 }
448
449 #[test]
450 fn discovery_error_display() {
451 let e = DiscoveryError::Backend("timeout".into());
452 assert!(e.to_string().contains("timeout"));
453 let e = DiscoveryError::InvalidAddress("bad:addr".into());
454 assert!(e.to_string().contains("bad:addr"));
455 let e = DiscoveryError::NotStarted;
456 assert!(e.to_string().contains("not started"));
457 }
458}