1use std::collections::{HashMap, HashSet};
17
18#[inline]
24fn fnv1a_64(data: &[u8]) -> u64 {
25 let mut hash: u64 = 14_695_981_039_346_656_037;
26 for &b in data {
27 hash ^= b as u64;
28 hash = hash.wrapping_mul(1_099_511_628_211);
29 }
30 hash
31}
32
33#[allow(dead_code)]
38#[inline]
39fn xorshift64(state: &mut u64) -> u64 {
40 let mut x = *state;
41 x ^= x << 13;
42 x ^= x >> 7;
43 x ^= x << 17;
44 *state = x;
45 x
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Hash)]
54pub struct FloodTopic(pub String);
55
56impl FloodTopic {
57 pub fn new(s: impl Into<String>) -> Self {
59 FloodTopic(s.into())
60 }
61
62 pub fn as_str(&self) -> &str {
64 &self.0
65 }
66}
67
68impl std::fmt::Display for FloodTopic {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 f.write_str(&self.0)
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
78pub struct FloodMessageId(pub [u8; 16]);
79
80impl FloodMessageId {
81 pub fn as_bytes(&self) -> &[u8; 16] {
83 &self.0
84 }
85
86 pub fn to_hex(&self) -> String {
88 self.0.iter().map(|b| format!("{b:02x}")).collect()
89 }
90}
91
92impl std::fmt::Display for FloodMessageId {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 write!(f, "{}", self.to_hex())
95 }
96}
97
98#[derive(Debug, Clone)]
102pub struct FloodMessage {
103 pub id: FloodMessageId,
105 pub topic: FloodTopic,
107 pub payload: Vec<u8>,
109 pub ttl: u8,
112 pub origin_peer: String,
114 pub created_at: u64,
116}
117
118#[derive(Debug, Clone)]
122pub struct SubscriptionRecord {
123 pub peer_id: String,
125 pub topic: FloodTopic,
127 pub subscribed_at: u64,
129 pub message_count: u64,
131}
132
133#[derive(Debug, Clone, PartialEq)]
137pub enum ForwardDecision {
138 Forward {
140 to_peers: Vec<String>,
142 },
143 Drop {
145 reason: String,
147 },
148 Loopback,
151}
152
153#[derive(Debug, Clone)]
157pub struct RouterConfig {
158 pub max_peers_per_topic: usize,
160 pub max_message_cache: usize,
162 pub min_ttl: u8,
164 pub dedup_window_secs: u64,
166 pub max_topics: usize,
168}
169
170impl Default for RouterConfig {
171 fn default() -> Self {
172 RouterConfig {
173 max_peers_per_topic: 100,
174 max_message_cache: 10_000,
175 min_ttl: 1,
176 dedup_window_secs: 60,
177 max_topics: 256,
178 }
179 }
180}
181
182#[derive(Debug, Clone, Default)]
186pub struct FsrRouterStats {
187 pub messages_forwarded: u64,
189 pub messages_dropped: u64,
191 pub messages_looped: u64,
193 pub subscriptions: usize,
195 pub topics: usize,
197 pub cache_size: usize,
199}
200
201pub struct FloodSubRouter {
220 config: RouterConfig,
222 subscriptions: HashMap<String, SubscriptionRecord>,
224 message_cache: HashMap<FloodMessageId, u64>,
226 forwarded_to: HashMap<FloodMessageId, HashSet<String>>,
228 stats: FsrRouterStats,
230}
231
232impl FloodSubRouter {
233 pub fn new(config: RouterConfig) -> Self {
237 FloodSubRouter {
238 config,
239 subscriptions: HashMap::new(),
240 message_cache: HashMap::new(),
241 forwarded_to: HashMap::new(),
242 stats: FsrRouterStats::default(),
243 }
244 }
245
246 pub fn subscribe(&mut self, peer_id: &str, topic: &FloodTopic, now: u64) -> bool {
258 let key = Self::sub_key(peer_id, topic);
259 if self.subscriptions.contains_key(&key) {
260 return false;
261 }
262
263 let current_peers = self.peers_for_topic(topic).len();
265 if current_peers >= self.config.max_peers_per_topic {
266 return false;
267 }
268
269 let topic_exists = self.subscriptions.values().any(|r| &r.topic == topic);
271 if !topic_exists {
272 let distinct_topics: HashSet<&FloodTopic> =
273 self.subscriptions.values().map(|r| &r.topic).collect();
274 if distinct_topics.len() >= self.config.max_topics {
275 return false;
276 }
277 }
278
279 let record = SubscriptionRecord {
280 peer_id: peer_id.to_owned(),
281 topic: topic.clone(),
282 subscribed_at: now,
283 message_count: 0,
284 };
285 self.subscriptions.insert(key, record);
286 self.refresh_stats();
287 true
288 }
289
290 pub fn unsubscribe(&mut self, peer_id: &str, topic: &FloodTopic) -> bool {
294 let key = Self::sub_key(peer_id, topic);
295 let removed = self.subscriptions.remove(&key).is_some();
296 if removed {
297 self.refresh_stats();
298 }
299 removed
300 }
301
302 pub fn peers_for_topic(&self, topic: &FloodTopic) -> Vec<String> {
304 self.subscriptions
305 .values()
306 .filter(|r| &r.topic == topic)
307 .map(|r| r.peer_id.clone())
308 .collect()
309 }
310
311 pub fn topics_for_peer(&self, peer_id: &str) -> Vec<FloodTopic> {
313 self.subscriptions
314 .values()
315 .filter(|r| r.peer_id == peer_id)
316 .map(|r| r.topic.clone())
317 .collect()
318 }
319
320 pub fn route(&mut self, msg: &FloodMessage, now: u64) -> ForwardDecision {
338 if self.message_cache.contains_key(&msg.id) {
340 self.stats.messages_dropped += 1;
341 return ForwardDecision::Drop {
342 reason: "duplicate: already in dedup cache".to_owned(),
343 };
344 }
345
346 if msg.ttl <= self.config.min_ttl {
348 self.stats.messages_dropped += 1;
349 return ForwardDecision::Drop {
350 reason: format!("ttl {} <= min_ttl {}", msg.ttl, self.config.min_ttl),
351 };
352 }
353
354 let subscribers = self.peers_for_topic(&msg.topic);
356 if subscribers.is_empty() {
357 self.stats.messages_dropped += 1;
358 return ForwardDecision::Drop {
359 reason: format!("no subscribers for topic '{}'", msg.topic),
360 };
361 }
362
363 if self.message_cache.len() >= self.config.max_message_cache {
365 if let Some(oldest_key) = self
367 .message_cache
368 .iter()
369 .min_by_key(|(_, &ts)| ts)
370 .map(|(k, _)| *k)
371 {
372 self.message_cache.remove(&oldest_key);
373 self.forwarded_to.remove(&oldest_key);
374 }
375 }
376 self.message_cache.insert(msg.id, now);
377
378 let already_forwarded = self.forwarded_to.get(&msg.id).cloned().unwrap_or_default();
380
381 let to_peers: Vec<String> = subscribers
382 .iter()
383 .filter(|p| *p != &msg.origin_peer && !already_forwarded.contains(*p))
384 .cloned()
385 .collect();
386
387 let entry = self.forwarded_to.entry(msg.id).or_default();
389 for p in &to_peers {
390 entry.insert(p.clone());
391 }
392
393 if to_peers.is_empty() {
395 let non_origin_subs: Vec<_> = subscribers
397 .iter()
398 .filter(|p| *p != &msg.origin_peer)
399 .collect();
400 if non_origin_subs.is_empty() {
401 self.stats.messages_looped += 1;
402 return ForwardDecision::Loopback;
403 }
404 self.stats.messages_dropped += 1;
406 return ForwardDecision::Drop {
407 reason: "all subscribers already received this message".to_owned(),
408 };
409 }
410
411 for peer in &to_peers {
413 let key = Self::sub_key(peer, &msg.topic);
414 if let Some(rec) = self.subscriptions.get_mut(&key) {
415 rec.message_count += 1;
416 }
417 }
418
419 self.stats.messages_forwarded += 1;
420 self.refresh_stats();
421 ForwardDecision::Forward { to_peers }
422 }
423
424 pub fn mark_forwarded(&mut self, id: &FloodMessageId, peer_id: &str) {
429 self.forwarded_to
430 .entry(*id)
431 .or_default()
432 .insert(peer_id.to_owned());
433 }
434
435 pub fn expire_cache(&mut self, now: u64) {
442 let window = self.config.dedup_window_secs;
443 let cutoff = now.saturating_sub(window);
444
445 let expired: Vec<FloodMessageId> = self
447 .message_cache
448 .iter()
449 .filter(|(_, &ts)| ts <= cutoff)
450 .map(|(id, _)| *id)
451 .collect();
452
453 for id in expired {
454 self.message_cache.remove(&id);
455 self.forwarded_to.remove(&id);
456 }
457
458 self.refresh_stats();
459 }
460
461 pub fn stats(&self) -> &FsrRouterStats {
465 &self.stats
466 }
467
468 pub fn compute_message_id(
476 payload: &[u8],
477 topic: &FloodTopic,
478 peer_id: &str,
479 now: u64,
480 ) -> FloodMessageId {
481 let mut buf: Vec<u8> =
484 Vec::with_capacity(payload.len() + topic.0.len() + peer_id.len() + 18);
485 buf.extend_from_slice(payload);
486 buf.push(0x00);
487 buf.extend_from_slice(topic.0.as_bytes());
488 buf.push(0x00);
489 buf.extend_from_slice(peer_id.as_bytes());
490 buf.push(0x00);
491 buf.extend_from_slice(&now.to_le_bytes());
492
493 let h1 = fnv1a_64(&buf);
494 let h2 = fnv1a_64(&h1.to_le_bytes());
496
497 let mut id = [0u8; 16];
498 id[..8].copy_from_slice(&h1.to_le_bytes());
499 id[8..].copy_from_slice(&h2.to_le_bytes());
500 FloodMessageId(id)
501 }
502
503 #[inline]
510 fn sub_key(peer_id: &str, topic: &FloodTopic) -> String {
511 format!("{}\x00{}", peer_id, topic.0)
512 }
513
514 #[inline]
516 fn refresh_stats(&mut self) {
517 let distinct_topics: HashSet<&FloodTopic> =
518 self.subscriptions.values().map(|r| &r.topic).collect();
519 self.stats.subscriptions = self.subscriptions.len();
520 self.stats.topics = distinct_topics.len();
521 self.stats.cache_size = self.message_cache.len();
522 }
523
524 pub fn cache_full(&self) -> bool {
526 self.message_cache.len() >= self.config.max_message_cache
527 }
528
529 pub fn forwarded_count(&self, id: &FloodMessageId) -> usize {
531 self.forwarded_to.get(id).map(|s| s.len()).unwrap_or(0)
532 }
533
534 pub fn is_subscribed(&self, peer_id: &str, topic: &FloodTopic) -> bool {
536 self.subscriptions
537 .contains_key(&Self::sub_key(peer_id, topic))
538 }
539
540 pub fn subscription(&self, peer_id: &str, topic: &FloodTopic) -> Option<&SubscriptionRecord> {
542 self.subscriptions.get(&Self::sub_key(peer_id, topic))
543 }
544
545 pub fn subscription_count(&self) -> usize {
547 self.subscriptions.len()
548 }
549
550 pub fn all_subscriptions(&self) -> impl Iterator<Item = &SubscriptionRecord> {
552 self.subscriptions.values()
553 }
554}
555
556#[cfg(test)]
561mod tests {
562 use super::*;
563
564 fn default_router() -> FloodSubRouter {
567 FloodSubRouter::new(RouterConfig::default())
568 }
569
570 fn topic(s: &str) -> FloodTopic {
571 FloodTopic::new(s)
572 }
573
574 fn make_msg(payload: &[u8], t: &FloodTopic, peer: &str, ttl: u8, now: u64) -> FloodMessage {
576 let id = FloodSubRouter::compute_message_id(payload, t, peer, now);
577 FloodMessage {
578 id,
579 topic: t.clone(),
580 payload: payload.to_vec(),
581 ttl,
582 origin_peer: peer.to_owned(),
583 created_at: now,
584 }
585 }
586
587 #[test]
590 fn test_new_router_is_empty() {
591 let r = default_router();
592 let s = r.stats();
593 assert_eq!(s.subscriptions, 0);
594 assert_eq!(s.topics, 0);
595 assert_eq!(s.cache_size, 0);
596 assert_eq!(s.messages_forwarded, 0);
597 assert_eq!(s.messages_dropped, 0);
598 assert_eq!(s.messages_looped, 0);
599 }
600
601 #[test]
604 fn test_subscribe_new_returns_true() {
605 let mut r = default_router();
606 assert!(r.subscribe("peer1", &topic("news"), 100));
607 }
608
609 #[test]
610 fn test_subscribe_duplicate_returns_false() {
611 let mut r = default_router();
612 r.subscribe("peer1", &topic("news"), 100);
613 assert!(!r.subscribe("peer1", &topic("news"), 200));
614 }
615
616 #[test]
617 fn test_subscribe_updates_stats() {
618 let mut r = default_router();
619 r.subscribe("p1", &topic("t1"), 1);
620 r.subscribe("p2", &topic("t1"), 2);
621 r.subscribe("p1", &topic("t2"), 3);
622 assert_eq!(r.stats().subscriptions, 3);
623 assert_eq!(r.stats().topics, 2);
624 }
625
626 #[test]
627 fn test_subscribe_respects_max_peers_per_topic() {
628 let config = RouterConfig {
629 max_peers_per_topic: 2,
630 ..Default::default()
631 };
632 let mut r = FloodSubRouter::new(config);
633 let t = topic("flood");
634 assert!(r.subscribe("p1", &t, 1));
635 assert!(r.subscribe("p2", &t, 2));
636 assert!(!r.subscribe("p3", &t, 3)); }
638
639 #[test]
640 fn test_subscribe_respects_max_topics() {
641 let config = RouterConfig {
642 max_topics: 2,
643 ..Default::default()
644 };
645 let mut r = FloodSubRouter::new(config);
646 assert!(r.subscribe("p1", &topic("t1"), 1));
647 assert!(r.subscribe("p1", &topic("t2"), 2));
648 assert!(!r.subscribe("p1", &topic("t3"), 3)); }
650
651 #[test]
654 fn test_unsubscribe_existing_returns_true() {
655 let mut r = default_router();
656 r.subscribe("p1", &topic("x"), 1);
657 assert!(r.unsubscribe("p1", &topic("x")));
658 }
659
660 #[test]
661 fn test_unsubscribe_missing_returns_false() {
662 let mut r = default_router();
663 assert!(!r.unsubscribe("p1", &topic("x")));
664 }
665
666 #[test]
667 fn test_unsubscribe_decrements_stats() {
668 let mut r = default_router();
669 r.subscribe("p1", &topic("x"), 1);
670 r.subscribe("p2", &topic("x"), 2);
671 r.unsubscribe("p1", &topic("x"));
672 assert_eq!(r.stats().subscriptions, 1);
673 assert_eq!(r.stats().topics, 1); }
675
676 #[test]
677 fn test_unsubscribe_last_peer_removes_topic_from_stats() {
678 let mut r = default_router();
679 r.subscribe("p1", &topic("x"), 1);
680 r.unsubscribe("p1", &topic("x"));
681 assert_eq!(r.stats().topics, 0);
682 }
683
684 #[test]
687 fn test_peers_for_topic_empty_when_no_subs() {
688 let r = default_router();
689 assert!(r.peers_for_topic(&topic("news")).is_empty());
690 }
691
692 #[test]
693 fn test_peers_for_topic_returns_all_subscribers() {
694 let mut r = default_router();
695 r.subscribe("p1", &topic("news"), 1);
696 r.subscribe("p2", &topic("news"), 2);
697 r.subscribe("p3", &topic("other"), 3);
698 let mut peers = r.peers_for_topic(&topic("news"));
699 peers.sort();
700 assert_eq!(peers, vec!["p1", "p2"]);
701 }
702
703 #[test]
706 fn test_topics_for_peer_empty_when_not_subscribed() {
707 let r = default_router();
708 assert!(r.topics_for_peer("p1").is_empty());
709 }
710
711 #[test]
712 fn test_topics_for_peer_returns_all_topics() {
713 let mut r = default_router();
714 r.subscribe("p1", &topic("t1"), 1);
715 r.subscribe("p1", &topic("t2"), 2);
716 r.subscribe("p2", &topic("t1"), 3);
717 let mut topics = r
718 .topics_for_peer("p1")
719 .into_iter()
720 .map(|t| t.0)
721 .collect::<Vec<_>>();
722 topics.sort();
723 assert_eq!(topics, vec!["t1", "t2"]);
724 }
725
726 #[test]
729 fn test_route_drops_duplicate_message() {
730 let mut r = default_router();
731 r.subscribe("p2", &topic("t"), 1);
732 let msg = make_msg(b"hello", &topic("t"), "p1", 5, 1000);
733 let d1 = r.route(&msg, 1000);
734 assert!(matches!(d1, ForwardDecision::Forward { .. }));
735 let d2 = r.route(&msg, 1001);
736 assert!(matches!(d2, ForwardDecision::Drop { .. }));
737 if let ForwardDecision::Drop { reason } = d2 {
738 assert!(reason.contains("duplicate"));
739 }
740 }
741
742 #[test]
745 fn test_route_drops_on_min_ttl() {
746 let mut r = default_router();
747 r.subscribe("p2", &topic("t"), 1);
748 let msg = make_msg(b"x", &topic("t"), "p1", 1, 1000); let d = r.route(&msg, 1000);
750 assert!(matches!(d, ForwardDecision::Drop { .. }));
751 if let ForwardDecision::Drop { reason } = d {
752 assert!(reason.contains("ttl"));
753 }
754 }
755
756 #[test]
757 fn test_route_drops_on_zero_ttl() {
758 let mut r = default_router();
759 r.subscribe("p2", &topic("t"), 1);
760 let msg = make_msg(b"x", &topic("t"), "p1", 0, 1000);
761 assert!(matches!(r.route(&msg, 1000), ForwardDecision::Drop { .. }));
762 }
763
764 #[test]
765 fn test_route_forwards_with_ttl_above_min() {
766 let mut r = default_router();
767 r.subscribe("p2", &topic("t"), 1);
768 let msg = make_msg(b"x", &topic("t"), "p1", 2, 1000); assert!(matches!(
770 r.route(&msg, 1000),
771 ForwardDecision::Forward { .. }
772 ));
773 }
774
775 #[test]
778 fn test_route_drops_on_no_subscribers() {
779 let mut r = default_router();
780 let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
781 let d = r.route(&msg, 1000);
782 assert!(matches!(d, ForwardDecision::Drop { .. }));
783 if let ForwardDecision::Drop { reason } = d {
784 assert!(reason.contains("no subscribers"));
785 }
786 }
787
788 #[test]
791 fn test_route_loopback_when_origin_only_subscriber() {
792 let mut r = default_router();
793 r.subscribe("p1", &topic("t"), 1); let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000); let d = r.route(&msg, 1000);
796 assert_eq!(d, ForwardDecision::Loopback);
797 assert_eq!(r.stats().messages_looped, 1);
798 }
799
800 #[test]
803 fn test_route_excludes_origin_from_forward_set() {
804 let mut r = default_router();
805 r.subscribe("p1", &topic("t"), 1);
806 r.subscribe("p2", &topic("t"), 2);
807 r.subscribe("p3", &topic("t"), 3);
808 let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
809 if let ForwardDecision::Forward { to_peers } = r.route(&msg, 1000) {
810 assert!(!to_peers.contains(&"p1".to_owned()));
811 assert!(to_peers.contains(&"p2".to_owned()));
812 assert!(to_peers.contains(&"p3".to_owned()));
813 } else {
814 panic!("expected Forward");
815 }
816 }
817
818 #[test]
821 fn test_route_excludes_already_forwarded_peers() {
822 let mut r = default_router();
823 r.subscribe("p2", &topic("t"), 1);
824 r.subscribe("p3", &topic("t"), 2);
825 let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
826 if let ForwardDecision::Forward { to_peers } = r.route(&msg, 1000) {
828 assert_eq!(to_peers.len(), 2);
829 } else {
830 panic!("expected Forward on first call");
831 }
832 let msg2 = make_msg(b"y", &topic("t"), "p1", 5, 1001);
834 if let ForwardDecision::Forward { to_peers } = r.route(&msg2, 1001) {
836 assert!(to_peers.contains(&"p2".to_owned()) || to_peers.contains(&"p3".to_owned()));
837 } else {
838 panic!("expected Forward for second distinct message");
839 }
840 }
841
842 #[test]
845 fn test_mark_forwarded_prevents_re_forward() {
846 let mut r = default_router();
847 r.subscribe("p2", &topic("t"), 1);
848 r.subscribe("p3", &topic("t"), 2);
849 let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
850 r.mark_forwarded(&msg.id, "p2");
852 if let ForwardDecision::Forward { to_peers } = r.route(&msg, 1000) {
855 assert!(!to_peers.contains(&"p2".to_owned()));
856 assert!(to_peers.contains(&"p3".to_owned()));
857 } else {
858 panic!("expected Forward");
859 }
860 }
861
862 #[test]
865 fn test_expire_cache_removes_old_entries() {
866 let mut r = default_router();
867 r.subscribe("p2", &topic("t"), 1);
868 let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
869 r.route(&msg, 1000); assert_eq!(r.stats().cache_size, 1);
871 r.expire_cache(1061);
873 assert_eq!(r.stats().cache_size, 0);
874 }
875
876 #[test]
877 fn test_expire_cache_keeps_recent_entries() {
878 let mut r = default_router();
879 r.subscribe("p2", &topic("t"), 1);
880 let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
881 r.route(&msg, 1000);
882 r.expire_cache(1050);
884 assert_eq!(r.stats().cache_size, 1);
885 }
886
887 #[test]
888 fn test_expire_cache_clears_forwarded_to_map() {
889 let mut r = default_router();
890 r.subscribe("p2", &topic("t"), 1);
891 let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
892 r.route(&msg, 1000);
893 r.expire_cache(1061);
894 assert_eq!(r.forwarded_count(&msg.id), 0);
896 }
897
898 #[test]
901 fn test_stats_messages_forwarded_increments() {
902 let mut r = default_router();
903 r.subscribe("p2", &topic("t"), 1);
904 let msg = make_msg(b"a", &topic("t"), "p1", 5, 1000);
905 r.route(&msg, 1000);
906 assert_eq!(r.stats().messages_forwarded, 1);
907 }
908
909 #[test]
910 fn test_stats_messages_dropped_increments_on_dup() {
911 let mut r = default_router();
912 r.subscribe("p2", &topic("t"), 1);
913 let msg = make_msg(b"a", &topic("t"), "p1", 5, 1000);
914 r.route(&msg, 1000);
915 r.route(&msg, 1001);
916 assert_eq!(r.stats().messages_dropped, 1);
917 }
918
919 #[test]
920 fn test_stats_messages_dropped_increments_on_ttl() {
921 let mut r = default_router();
922 r.subscribe("p2", &topic("t"), 1);
923 let msg = make_msg(b"a", &topic("t"), "p1", 1, 1000);
924 r.route(&msg, 1000);
925 assert_eq!(r.stats().messages_dropped, 1);
926 }
927
928 #[test]
931 fn test_compute_message_id_is_deterministic() {
932 let t = topic("x");
933 let id1 = FloodSubRouter::compute_message_id(b"hello", &t, "peer1", 42);
934 let id2 = FloodSubRouter::compute_message_id(b"hello", &t, "peer1", 42);
935 assert_eq!(id1, id2);
936 }
937
938 #[test]
939 fn test_compute_message_id_differs_on_different_payload() {
940 let t = topic("x");
941 let id1 = FloodSubRouter::compute_message_id(b"hello", &t, "peer1", 42);
942 let id2 = FloodSubRouter::compute_message_id(b"world", &t, "peer1", 42);
943 assert_ne!(id1, id2);
944 }
945
946 #[test]
947 fn test_compute_message_id_differs_on_different_topic() {
948 let id1 = FloodSubRouter::compute_message_id(b"x", &topic("t1"), "peer1", 1);
949 let id2 = FloodSubRouter::compute_message_id(b"x", &topic("t2"), "peer1", 1);
950 assert_ne!(id1, id2);
951 }
952
953 #[test]
954 fn test_compute_message_id_differs_on_different_peer() {
955 let t = topic("t");
956 let id1 = FloodSubRouter::compute_message_id(b"x", &t, "p1", 1);
957 let id2 = FloodSubRouter::compute_message_id(b"x", &t, "p2", 1);
958 assert_ne!(id1, id2);
959 }
960
961 #[test]
962 fn test_compute_message_id_differs_on_different_timestamp() {
963 let t = topic("t");
964 let id1 = FloodSubRouter::compute_message_id(b"x", &t, "p1", 1);
965 let id2 = FloodSubRouter::compute_message_id(b"x", &t, "p1", 2);
966 assert_ne!(id1, id2);
967 }
968
969 #[test]
970 fn test_compute_message_id_is_16_bytes() {
971 let id = FloodSubRouter::compute_message_id(b"test", &topic("t"), "p", 0);
972 assert_eq!(id.as_bytes().len(), 16);
973 }
974
975 #[test]
978 fn test_flood_message_id_to_hex_is_32_chars() {
979 let id = FloodSubRouter::compute_message_id(b"test", &topic("t"), "p", 0);
980 assert_eq!(id.to_hex().len(), 32);
981 }
982
983 #[test]
984 fn test_flood_message_id_display() {
985 let id = FloodMessageId([0u8; 16]);
986 assert_eq!(id.to_string(), "0".repeat(32));
987 }
988
989 #[test]
992 fn test_flood_topic_as_str() {
993 let t = FloodTopic::new("sports");
994 assert_eq!(t.as_str(), "sports");
995 }
996
997 #[test]
998 fn test_flood_topic_display() {
999 let t = FloodTopic::new("sports");
1000 assert_eq!(t.to_string(), "sports");
1001 }
1002
1003 #[test]
1004 fn test_flood_topic_equality() {
1005 assert_eq!(FloodTopic::new("a"), FloodTopic::new("a"));
1006 assert_ne!(FloodTopic::new("a"), FloodTopic::new("b"));
1007 }
1008
1009 #[test]
1012 fn test_route_only_delivers_to_topic_subscribers() {
1013 let mut r = default_router();
1014 r.subscribe("p2", &topic("sports"), 1);
1015 r.subscribe("p3", &topic("news"), 2);
1016 let msg = make_msg(b"goal!", &topic("sports"), "p1", 5, 1000);
1017 if let ForwardDecision::Forward { to_peers } = r.route(&msg, 1000) {
1018 assert!(to_peers.contains(&"p2".to_owned()));
1019 assert!(!to_peers.contains(&"p3".to_owned()));
1020 } else {
1021 panic!("expected Forward");
1022 }
1023 }
1024
1025 #[test]
1028 fn test_subscription_record_message_count_increments() {
1029 let mut r = default_router();
1030 r.subscribe("p2", &topic("t"), 1);
1031 let msg1 = make_msg(b"a", &topic("t"), "p1", 5, 1000);
1032 let msg2 = make_msg(b"b", &topic("t"), "p1", 5, 1001);
1033 r.route(&msg1, 1000);
1034 r.route(&msg2, 1001);
1035 let rec = r.subscription("p2", &topic("t")).expect("must exist");
1036 assert_eq!(rec.message_count, 2);
1037 }
1038
1039 #[test]
1042 fn test_is_subscribed_true_after_subscribe() {
1043 let mut r = default_router();
1044 r.subscribe("p1", &topic("t"), 1);
1045 assert!(r.is_subscribed("p1", &topic("t")));
1046 }
1047
1048 #[test]
1049 fn test_is_subscribed_false_before_subscribe() {
1050 let r = default_router();
1051 assert!(!r.is_subscribed("p1", &topic("t")));
1052 }
1053
1054 #[test]
1055 fn test_is_subscribed_false_after_unsubscribe() {
1056 let mut r = default_router();
1057 r.subscribe("p1", &topic("t"), 1);
1058 r.unsubscribe("p1", &topic("t"));
1059 assert!(!r.is_subscribed("p1", &topic("t")));
1060 }
1061
1062 #[test]
1065 fn test_cache_does_not_exceed_max_message_cache() {
1066 let config = RouterConfig {
1067 max_message_cache: 3,
1068 ..Default::default()
1069 };
1070 let mut r = FloodSubRouter::new(config);
1071 r.subscribe("p2", &topic("t"), 1);
1072 for i in 0u64..5 {
1073 let msg = make_msg(
1074 format!("payload{i}").as_bytes(),
1075 &topic("t"),
1076 "p1",
1077 5,
1078 1000 + i,
1079 );
1080 r.route(&msg, 1000 + i);
1081 }
1082 assert!(r.stats().cache_size <= 3);
1084 }
1085
1086 #[test]
1089 fn test_forwarded_count_zero_before_route() {
1090 let r = default_router();
1091 let id = FloodSubRouter::compute_message_id(b"x", &topic("t"), "p", 1);
1092 assert_eq!(r.forwarded_count(&FloodMessageId(id.0)), 0);
1093 }
1094
1095 #[test]
1096 fn test_forwarded_count_after_route() {
1097 let mut r = default_router();
1098 r.subscribe("p2", &topic("t"), 1);
1099 r.subscribe("p3", &topic("t"), 2);
1100 let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
1101 r.route(&msg, 1000);
1102 assert_eq!(r.forwarded_count(&msg.id), 2);
1103 }
1104
1105 #[test]
1108 fn test_cache_full_returns_false_when_empty() {
1109 let r = default_router();
1110 assert!(!r.cache_full());
1111 }
1112
1113 #[test]
1114 fn test_cache_full_returns_true_at_capacity() {
1115 let config = RouterConfig {
1116 max_message_cache: 1,
1117 ..Default::default()
1118 };
1119 let mut r = FloodSubRouter::new(config);
1120 r.subscribe("p2", &topic("t"), 1);
1121 let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
1122 r.route(&msg, 1000);
1123 assert!(r.cache_full());
1124 }
1125
1126 #[test]
1129 fn test_subscription_count_matches_stats() {
1130 let mut r = default_router();
1131 r.subscribe("p1", &topic("a"), 1);
1132 r.subscribe("p2", &topic("a"), 2);
1133 r.subscribe("p1", &topic("b"), 3);
1134 assert_eq!(r.subscription_count(), r.stats().subscriptions);
1135 assert_eq!(r.subscription_count(), 3);
1136 }
1137
1138 #[test]
1141 fn test_all_subscriptions_iterator_count() {
1142 let mut r = default_router();
1143 r.subscribe("p1", &topic("t1"), 1);
1144 r.subscribe("p2", &topic("t1"), 2);
1145 r.subscribe("p1", &topic("t2"), 3);
1146 assert_eq!(r.all_subscriptions().count(), 3);
1147 }
1148
1149 #[test]
1152 fn test_xorshift64_produces_different_values() {
1153 let mut state: u64 = 0xDEAD_BEEF_CAFE_1234;
1154 let v1 = xorshift64(&mut state);
1155 let v2 = xorshift64(&mut state);
1156 let v3 = xorshift64(&mut state);
1157 assert_ne!(v1, v2);
1159 assert_ne!(v2, v3);
1160 }
1161
1162 #[test]
1163 fn test_xorshift64_is_deterministic() {
1164 let mut s1: u64 = 42;
1165 let mut s2: u64 = 42;
1166 assert_eq!(xorshift64(&mut s1), xorshift64(&mut s2));
1167 }
1168
1169 #[test]
1172 fn test_route_drops_when_all_peers_already_forwarded() {
1173 let mut r = default_router();
1174 r.subscribe("p2", &topic("t"), 1);
1175 let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
1176 r.mark_forwarded(&msg.id, "p2");
1178 let d = r.route(&msg, 1000);
1181 assert!(matches!(d, ForwardDecision::Drop { .. }));
1182 if let ForwardDecision::Drop { reason } = d {
1183 assert!(reason.contains("already received") || reason.contains("already"));
1184 }
1185 }
1186
1187 #[test]
1190 fn test_multiple_expire_rounds_clean_incrementally() {
1191 let mut r = default_router();
1192 r.subscribe("p2", &topic("t"), 1);
1193 let msg1 = make_msg(b"first", &topic("t"), "p1", 5, 1000);
1194 let msg2 = make_msg(b"second", &topic("t"), "p1", 5, 1050);
1195 r.route(&msg1, 1000);
1196 r.route(&msg2, 1050);
1197 assert_eq!(r.stats().cache_size, 2);
1198 r.expire_cache(1061);
1200 assert_eq!(r.stats().cache_size, 1);
1201 r.expire_cache(1111);
1203 assert_eq!(r.stats().cache_size, 0);
1204 }
1205
1206 #[test]
1209 fn test_router_config_defaults() {
1210 let cfg = RouterConfig::default();
1211 assert_eq!(cfg.max_peers_per_topic, 100);
1212 assert_eq!(cfg.max_message_cache, 10_000);
1213 assert_eq!(cfg.min_ttl, 1);
1214 assert_eq!(cfg.dedup_window_secs, 60);
1215 assert_eq!(cfg.max_topics, 256);
1216 }
1217
1218 #[test]
1221 fn test_fsr_router_stats_defaults() {
1222 let s = FsrRouterStats::default();
1223 assert_eq!(s.messages_forwarded, 0);
1224 assert_eq!(s.messages_dropped, 0);
1225 assert_eq!(s.messages_looped, 0);
1226 assert_eq!(s.subscriptions, 0);
1227 assert_eq!(s.topics, 0);
1228 assert_eq!(s.cache_size, 0);
1229 }
1230
1231 #[test]
1234 fn test_subscribe_at_exactly_max_topics() {
1235 let config = RouterConfig {
1236 max_topics: 3,
1237 ..Default::default()
1238 };
1239 let mut r = FloodSubRouter::new(config);
1240 assert!(r.subscribe("p1", &topic("t1"), 1));
1241 assert!(r.subscribe("p1", &topic("t2"), 2));
1242 assert!(r.subscribe("p1", &topic("t3"), 3));
1243 assert!(!r.subscribe("p1", &topic("t4"), 4));
1245 }
1246
1247 #[test]
1250 fn test_resubscribe_after_unsubscribe() {
1251 let mut r = default_router();
1252 r.subscribe("p1", &topic("t"), 1);
1253 r.unsubscribe("p1", &topic("t"));
1254 assert!(r.subscribe("p1", &topic("t"), 2));
1255 }
1256
1257 #[test]
1260 fn test_stats_cache_size_reflects_expire() {
1261 let mut r = default_router();
1262 r.subscribe("p2", &topic("t"), 1);
1263 for i in 0u64..5 {
1264 let msg = make_msg(format!("m{i}").as_bytes(), &topic("t"), "p1", 5, 1000 + i);
1265 r.route(&msg, 1000 + i);
1266 }
1267 assert_eq!(r.stats().cache_size, 5);
1268 r.expire_cache(1100);
1269 assert_eq!(r.stats().cache_size, 0);
1270 }
1271
1272 #[test]
1275 fn test_can_reroute_message_after_cache_expiry() {
1276 let mut r = default_router();
1277 r.subscribe("p2", &topic("t"), 1);
1278 let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
1279 r.route(&msg, 1000);
1280 r.expire_cache(1100); let d = r.route(&msg, 1100);
1283 assert!(matches!(d, ForwardDecision::Forward { .. }));
1284 }
1285
1286 #[test]
1289 fn test_forward_to_multiple_peers_increments_all_counts() {
1290 let mut r = default_router();
1291 for i in 1u8..=5 {
1292 r.subscribe(&format!("p{i}"), &topic("t"), i as u64);
1293 }
1294 let msg = make_msg(b"broadcast", &topic("t"), "p_origin", 5, 1000);
1295 r.route(&msg, 1000);
1296 for i in 1u8..=5 {
1297 let rec = r
1298 .subscription(&format!("p{i}"), &topic("t"))
1299 .expect("must exist");
1300 assert_eq!(rec.message_count, 1, "p{i} should have count 1");
1301 }
1302 }
1303}