1#[cfg(not(feature = "std"))]
54use alloc::vec::Vec;
55
56use crate::document::MergeResult;
57use crate::peer::HivePeer;
58
59pub trait GossipStrategy: Send + Sync {
65 fn select_peers<'a>(&self, peers: &'a [HivePeer]) -> Vec<&'a HivePeer>;
72
73 fn should_forward(&self, result: &MergeResult) -> bool {
78 result.counter_changed || result.emergency_changed || result.chat_changed
80 }
81
82 fn name(&self) -> &'static str;
84}
85
86#[derive(Debug, Clone)]
99pub struct RandomFanout {
100 fanout: usize,
102 #[cfg(feature = "std")]
104 seed: Option<u64>,
105}
106
107impl RandomFanout {
108 pub fn new(fanout: usize) -> Self {
113 Self {
114 fanout: fanout.max(1), #[cfg(feature = "std")]
116 seed: None,
117 }
118 }
119
120 #[cfg(feature = "std")]
122 pub fn with_seed(fanout: usize, seed: u64) -> Self {
123 Self {
124 fanout: fanout.max(1),
125 seed: Some(seed),
126 }
127 }
128
129 #[cfg(feature = "std")]
131 fn random_index(&self, max: usize, iteration: usize) -> usize {
132 use std::time::SystemTime;
133
134 let seed = self.seed.unwrap_or_else(|| {
135 SystemTime::now()
136 .duration_since(SystemTime::UNIX_EPOCH)
137 .map(|d| d.as_nanos() as u64)
138 .unwrap_or(12345)
139 });
140
141 let mixed = seed
143 .wrapping_mul(6364136223846793005)
144 .wrapping_add(iteration as u64);
145 (mixed as usize) % max
146 }
147}
148
149impl Default for RandomFanout {
150 fn default() -> Self {
151 Self::new(2) }
153}
154
155impl GossipStrategy for RandomFanout {
156 fn select_peers<'a>(&self, peers: &'a [HivePeer]) -> Vec<&'a HivePeer> {
157 if peers.is_empty() {
158 return Vec::new();
159 }
160
161 if peers.len() <= self.fanout {
163 return peers.iter().collect();
164 }
165
166 #[cfg(feature = "std")]
168 {
169 let mut selected = Vec::with_capacity(self.fanout);
170 let mut used = std::collections::HashSet::new();
171
172 for i in 0..self.fanout * 3 {
173 if selected.len() >= self.fanout {
175 break;
176 }
177
178 let idx = self.random_index(peers.len(), i);
179 if !used.contains(&idx) {
180 used.insert(idx);
181 selected.push(&peers[idx]);
182 }
183 }
184
185 selected
186 }
187
188 #[cfg(not(feature = "std"))]
189 {
190 peers.iter().take(self.fanout).collect()
192 }
193 }
194
195 fn name(&self) -> &'static str {
196 "random_fanout"
197 }
198}
199
200#[derive(Debug, Clone, Default)]
209pub struct BroadcastAll;
210
211impl BroadcastAll {
212 pub fn new() -> Self {
214 Self
215 }
216}
217
218impl GossipStrategy for BroadcastAll {
219 fn select_peers<'a>(&self, peers: &'a [HivePeer]) -> Vec<&'a HivePeer> {
220 peers.iter().collect()
221 }
222
223 fn name(&self) -> &'static str {
224 "broadcast_all"
225 }
226}
227
228#[derive(Debug, Clone)]
233pub struct SignalBasedFanout {
234 fanout: usize,
236 rssi_threshold: i8,
238}
239
240impl SignalBasedFanout {
241 pub fn new(fanout: usize, rssi_threshold: i8) -> Self {
247 Self {
248 fanout: fanout.max(1),
249 rssi_threshold,
250 }
251 }
252}
253
254impl Default for SignalBasedFanout {
255 fn default() -> Self {
256 Self::new(2, 10) }
258}
259
260impl GossipStrategy for SignalBasedFanout {
261 fn select_peers<'a>(&self, peers: &'a [HivePeer]) -> Vec<&'a HivePeer> {
262 if peers.is_empty() {
263 return Vec::new();
264 }
265
266 if peers.len() <= self.fanout {
267 return peers.iter().collect();
268 }
269
270 let mut sorted: Vec<_> = peers.iter().collect();
272 sorted.sort_by(|a, b| b.rssi.cmp(&a.rssi));
273
274 let mut selected: Vec<&HivePeer> = Vec::with_capacity(self.fanout);
276
277 if let Some(best) = sorted.first() {
279 selected.push(best);
280 }
281
282 for peer in sorted.iter().skip(1) {
284 if selected.len() >= self.fanout {
285 break;
286 }
287
288 let last_rssi = selected.last().map(|p| p.rssi).unwrap_or(-100);
290 let this_rssi = peer.rssi;
291
292 if this_rssi >= last_rssi - self.rssi_threshold || selected.len() < self.fanout / 2 + 1
294 {
295 selected.push(peer);
296 }
297 }
298
299 for peer in sorted.iter() {
301 if selected.len() >= self.fanout {
302 break;
303 }
304 let already_selected = selected.iter().any(|p| p.node_id == peer.node_id);
306 if !already_selected {
307 selected.push(peer);
308 }
309 }
310
311 selected
312 }
313
314 fn name(&self) -> &'static str {
315 "signal_based"
316 }
317}
318
319#[derive(Debug)]
324pub struct EmergencyAware {
325 normal_fanout: usize,
327 emergency_fanout: usize,
329 #[cfg(feature = "std")]
331 emergency_mode: std::sync::atomic::AtomicBool,
332}
333
334impl Clone for EmergencyAware {
335 fn clone(&self) -> Self {
336 Self {
337 normal_fanout: self.normal_fanout,
338 emergency_fanout: self.emergency_fanout,
339 #[cfg(feature = "std")]
340 emergency_mode: std::sync::atomic::AtomicBool::new(self.is_emergency()),
341 }
342 }
343}
344
345impl EmergencyAware {
346 pub fn new(normal_fanout: usize) -> Self {
348 Self {
349 normal_fanout: normal_fanout.max(1),
350 emergency_fanout: usize::MAX, #[cfg(feature = "std")]
352 emergency_mode: std::sync::atomic::AtomicBool::new(false),
353 }
354 }
355
356 #[cfg(feature = "std")]
358 pub fn set_emergency(&self, active: bool) {
359 self.emergency_mode
360 .store(active, std::sync::atomic::Ordering::SeqCst);
361 }
362
363 #[cfg(feature = "std")]
365 pub fn is_emergency(&self) -> bool {
366 self.emergency_mode
367 .load(std::sync::atomic::Ordering::SeqCst)
368 }
369
370 fn effective_fanout(&self) -> usize {
371 #[cfg(feature = "std")]
372 {
373 if self.is_emergency() {
374 self.emergency_fanout
375 } else {
376 self.normal_fanout
377 }
378 }
379 #[cfg(not(feature = "std"))]
380 {
381 self.normal_fanout
382 }
383 }
384}
385
386impl Default for EmergencyAware {
387 fn default() -> Self {
388 Self::new(2)
389 }
390}
391
392impl GossipStrategy for EmergencyAware {
393 fn select_peers<'a>(&self, peers: &'a [HivePeer]) -> Vec<&'a HivePeer> {
394 let fanout = self.effective_fanout();
395
396 if peers.len() <= fanout {
397 return peers.iter().collect();
398 }
399
400 peers.iter().take(fanout).collect()
403 }
404
405 fn should_forward(&self, result: &MergeResult) -> bool {
406 #[cfg(feature = "std")]
408 if self.is_emergency() {
409 return true;
410 }
411
412 #[cfg(feature = "std")]
414 if result.is_emergency() || result.emergency_changed {
415 self.set_emergency(true);
416 }
417
418 result.counter_changed || result.emergency_changed
419 }
420
421 fn name(&self) -> &'static str {
422 "emergency_aware"
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429 use crate::NodeId;
430
431 fn make_peer(id: u32, rssi: i8) -> HivePeer {
432 HivePeer {
433 node_id: NodeId::new(id),
434 identifier: format!("device-{}", id),
435 mesh_id: Some("TEST".to_string()),
436 name: Some(format!("HIVE-{:08X}", id)),
437 rssi,
438 is_connected: true,
439 last_seen_ms: 0,
440 }
441 }
442
443 #[test]
444 fn test_random_fanout_basic() {
445 let strategy = RandomFanout::new(2);
446
447 let peers: Vec<HivePeer> = vec![];
449 assert!(strategy.select_peers(&peers).is_empty());
450
451 let peers = vec![make_peer(1, -50)];
453 let selected = strategy.select_peers(&peers);
454 assert_eq!(selected.len(), 1);
455
456 let peers = vec![
458 make_peer(1, -50),
459 make_peer(2, -60),
460 make_peer(3, -70),
461 make_peer(4, -80),
462 ];
463 let selected = strategy.select_peers(&peers);
464 assert_eq!(selected.len(), 2);
465 }
466
467 #[test]
468 fn test_broadcast_all() {
469 let strategy = BroadcastAll::new();
470
471 let peers = vec![make_peer(1, -50), make_peer(2, -60), make_peer(3, -70)];
472
473 let selected = strategy.select_peers(&peers);
474 assert_eq!(selected.len(), 3);
475 }
476
477 #[test]
478 fn test_signal_based() {
479 let strategy = SignalBasedFanout::new(2, 10);
480
481 let peers = vec![
482 make_peer(1, -80), make_peer(2, -50), make_peer(3, -90), make_peer(4, -55), ];
487
488 let selected = strategy.select_peers(&peers);
489 assert_eq!(selected.len(), 2);
490
491 let node_ids: Vec<_> = selected.iter().map(|p| p.node_id.as_u32()).collect();
493 assert!(node_ids.contains(&2)); }
495
496 #[test]
497 fn test_emergency_aware() {
498 let strategy = EmergencyAware::new(2);
499
500 let peers = vec![
501 make_peer(1, -50),
502 make_peer(2, -60),
503 make_peer(3, -70),
504 make_peer(4, -80),
505 ];
506
507 assert!(!strategy.is_emergency());
509 let selected = strategy.select_peers(&peers);
510 assert_eq!(selected.len(), 2);
511
512 strategy.set_emergency(true);
514 assert!(strategy.is_emergency());
515 let selected = strategy.select_peers(&peers);
516 assert_eq!(selected.len(), 4);
517 }
518
519 #[test]
520 fn test_should_forward() {
521 let strategy = RandomFanout::default();
522
523 let result = MergeResult {
525 source_node: NodeId::new(1),
526 event: None,
527 counter_changed: true,
528 emergency_changed: false,
529 chat_changed: false,
530 total_count: 10,
531 };
532 assert!(strategy.should_forward(&result));
533
534 let result = MergeResult {
536 source_node: NodeId::new(1),
537 event: None,
538 counter_changed: false,
539 emergency_changed: true,
540 chat_changed: false,
541 total_count: 10,
542 };
543 assert!(strategy.should_forward(&result));
544
545 let result = MergeResult {
547 source_node: NodeId::new(1),
548 event: None,
549 counter_changed: false,
550 emergency_changed: false,
551 chat_changed: false,
552 total_count: 10,
553 };
554 assert!(!strategy.should_forward(&result));
555 }
556}