1use crate::constants::{HOP_COUNT_EMA_ALPHA, LATENCY_EMA_ALPHA, OBSERVED_STATS_COMPACT_THRESHOLD};
8use crate::events::{DorsEscalationPhase, DorsEscalationReasonCode, DorsReasonCode, Event};
9use crate::telemetry::routing::{RoutingDecision, RoutingPhase, RoutingReasonCode};
10use crate::{Error, Result};
11use chrono::Utc;
12use offline_protocol_core::{Message, WireCodec};
13use offline_protocol_router::{
14 display_routing_score, DorsConfig, EscalationTriggerReason, TransportScore, TransportSelector,
15};
16use offline_protocol_transport::{
17 Error as TransportError, Transport, TransportMetrics, TransportStatus, TransportType,
18};
19use std::collections::{HashMap, HashSet};
20use std::sync::Arc;
21use std::time::Instant;
22use tracing::{debug, warn};
23
24pub struct TransportManager {
26 transports: HashMap<TransportType, Arc<dyn Transport>>,
32
33 current_transport: Option<TransportType>,
35
36 selector: TransportSelector,
38
39 observations: HashMap<TransportType, ObservedStats>,
41
42 dors_event_callback: Option<Arc<dyn Fn(Event) + Send + Sync>>,
44
45 routing_decision_callback: Option<Arc<dyn Fn(RoutingDecision) + Send + Sync>>,
50
51 routing_diagnostic: bool,
56
57 last_escalation_trigger_emitted: Option<(DorsEscalationReasonCode, std::time::Instant)>,
59
60 peer_binary_wire: HashSet<String>,
64}
65
66const MESH_TRANSPORTS: &[TransportType] = &[TransportType::WiFiDirect, TransportType::BLE];
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct MeshNeighbor {
79 pub peer_id: String,
81 pub rssi: Option<i16>,
83 pub transport: TransportType,
85}
86
87impl MeshNeighbor {
88 pub fn link_quality(&self) -> u8 {
94 match self.rssi {
95 Some(rssi) => offline_protocol_transport::LinkQuality::from_rssi(rssi).value(),
96 None => offline_protocol_transport::DEFAULT_LINK_QUALITY_WITHOUT_RSSI,
97 }
98 }
99}
100
101const ESCALATION_TRIGGER_DEDUPE_SECS: u64 = 30;
103
104const MAX_PEER_BINARY_WIRE_ENTRIES: usize = 4096;
109
110#[derive(Debug, Default, Clone)]
111struct ObservedStats {
112 success_count: u32,
113 failure_count: u32,
114 latency_ema: Option<f32>,
115 hop_ema: Option<f32>,
116 last_success_at: Option<Instant>,
117}
118
119impl ObservedStats {
120 fn record_success(&mut self, latency_ms: u32, hop_count: u8) {
121 self.success_count = self.success_count.saturating_add(1);
122 self.latency_ema = Some(update_ema(
123 self.latency_ema,
124 latency_ms as f32,
125 LATENCY_EMA_ALPHA,
126 ));
127 self.hop_ema = Some(update_ema(
128 self.hop_ema,
129 hop_count as f32,
130 HOP_COUNT_EMA_ALPHA,
131 ));
132 self.last_success_at = Some(Instant::now());
133 self.compact();
134 }
135
136 fn record_failure(&mut self) {
137 self.failure_count = self.failure_count.saturating_add(1);
138 self.compact();
139 }
140
141 fn compact(&mut self) {
142 let total = self.success_count.saturating_add(self.failure_count);
143 if total > OBSERVED_STATS_COMPACT_THRESHOLD {
144 self.success_count /= 2;
145 self.failure_count /= 2;
146 }
147 }
148
149 fn delivery_ratio(&self) -> Option<f32> {
150 let total = self.success_count + self.failure_count;
151 if total == 0 {
152 None
153 } else {
154 Some(self.success_count as f32 / total as f32)
155 }
156 }
157
158 fn drop_ratio(&self) -> Option<f32> {
159 self.delivery_ratio()
160 .map(|ratio| (1.0 - ratio).clamp(0.0, 1.0))
161 }
162
163 fn apply_to_metrics(&self, metrics: &mut TransportMetrics) {
164 if self.success_count + self.failure_count > 0 {
165 if metrics.success_count + metrics.failure_count == 0 {
176 metrics.success_count = self.success_count;
177 metrics.failure_count = self.failure_count;
178 metrics.delivery_ratio = self.delivery_ratio();
179 metrics.drop_rate = self.drop_ratio();
180 }
181 }
182
183 if let Some(latency) = self.latency_ema {
184 metrics.latency_ms = Some(latency.round().clamp(0.0, u32::MAX as f32) as u32);
185 }
186
187 if let Some(hop) = self.hop_ema {
188 metrics.average_hop_count = Some(hop);
189 }
190 }
191}
192
193fn update_ema(current: Option<f32>, new_value: f32, alpha: f32) -> f32 {
194 let alpha = alpha.clamp(0.0, 1.0);
195 match current {
196 Some(existing) => existing * (1.0 - alpha) + new_value * alpha,
197 None => new_value,
198 }
199}
200
201impl TransportManager {
202 pub fn new(selector: TransportSelector) -> Self {
204 Self {
205 transports: HashMap::new(),
206 current_transport: None,
207 selector,
208 observations: HashMap::new(),
209 dors_event_callback: None,
210 routing_decision_callback: None,
211 routing_diagnostic: false,
212 last_escalation_trigger_emitted: None,
213 peer_binary_wire: HashSet::new(),
214 }
215 }
216
217 pub fn peer_supports_binary_wire(&self, peer_id: &str) -> bool {
220 self.peer_binary_wire.contains(peer_id)
221 }
222
223 pub fn mark_peer_binary_wire(&mut self, peer_id: &str, supported: bool) {
227 if supported {
228 if !self.peer_binary_wire.contains(peer_id)
230 && self.peer_binary_wire.len() >= MAX_PEER_BINARY_WIRE_ENTRIES
231 {
232 self.peer_binary_wire.clear();
233 }
234 self.peer_binary_wire.insert(peer_id.to_string());
235 } else {
236 self.peer_binary_wire.remove(peer_id);
237 }
238 }
239
240 pub fn mark_peer_nostr_pubkey(&mut self, peer_id: &str, pubkey_hex: Option<&str>) {
255 let Some(transport) = self.transports.get(&TransportType::Nostr) else {
256 return;
257 };
258 if let Some(nostr) = transport
259 .as_any()
260 .downcast_ref::<offline_protocol_transport::nostr::NostrTransport>()
261 {
262 match pubkey_hex {
263 Some(pubkey_hex) => nostr.set_peer_nostr_pubkey(peer_id, pubkey_hex),
264 None => nostr.forget_peer_nostr_pubkey(peer_id),
265 }
266 }
267 }
268
269 pub fn clear_peer_nostr_pubkey(&mut self, peer_id: &str) {
277 if let Some(nostr) = self.transports.get(&TransportType::Nostr).and_then(|t| {
278 t.as_any()
279 .downcast_ref::<offline_protocol_transport::nostr::NostrTransport>()
280 }) {
281 nostr.forget_peer_nostr_pubkey(peer_id);
282 }
283 }
284
285 pub fn nostr_public_key(&self) -> Option<String> {
288 self.transports
289 .get(&TransportType::Nostr)?
290 .as_any()
291 .downcast_ref::<offline_protocol_transport::nostr::NostrTransport>()
292 .map(|nostr| nostr.public_key_hex())
293 }
294
295 pub fn set_nostr_sealing_enabled(&mut self, enabled: bool) {
297 if let Some(nostr) = self.transports.get(&TransportType::Nostr).and_then(|t| {
298 t.as_any()
299 .downcast_ref::<offline_protocol_transport::nostr::NostrTransport>()
300 }) {
301 nostr.set_sealing_enabled(enabled);
302 }
303 }
304
305 pub fn set_nostr_cold_contact_enabled(&mut self, enabled: bool) {
307 if let Some(nostr) = self.nostr_transport() {
308 nostr.set_cold_contact_enabled(enabled);
309 }
310 }
311
312 pub fn publish_nostr_key_package(&mut self, slot_id: &str, message: &Message) -> Result<()> {
325 let Some(nostr) = self.nostr_transport() else {
326 return Ok(());
327 };
328 let payload = nostr.serialize_message(message)?;
329 nostr.publish_key_package(slot_id, payload);
330 Ok(())
331 }
332
333 pub fn take_failed_nostr_publications(&self) -> Vec<String> {
338 self.nostr_transport()
339 .map(|nostr| nostr.take_failed_publications())
340 .unwrap_or_default()
341 }
342
343 pub fn nostr_cold_contact_active(&self) -> bool {
345 self.nostr_transport()
346 .map(|nostr| nostr.cold_contact_enabled())
347 .unwrap_or(false)
348 }
349
350 fn nostr_transport(&self) -> Option<&offline_protocol_transport::nostr::NostrTransport> {
351 self.transports.get(&TransportType::Nostr).and_then(|t| {
352 t.as_any()
353 .downcast_ref::<offline_protocol_transport::nostr::NostrTransport>()
354 })
355 }
356
357 pub fn set_dors_event_callback(&mut self, callback: Option<Arc<dyn Fn(Event) + Send + Sync>>) {
359 self.dors_event_callback = callback;
360 }
361
362 pub(crate) fn set_routing_decision_callback(
373 &mut self,
374 callback: Option<Arc<dyn Fn(RoutingDecision) + Send + Sync>>,
375 ) {
376 self.routing_decision_callback = callback;
377 }
378
379 pub(crate) fn set_routing_diagnostic(&mut self, enabled: bool) {
389 self.routing_diagnostic = enabled;
390 }
391
392 fn emit_dors_event(&self, event: Event) {
393 if let Some(ref cb) = self.dors_event_callback {
394 cb(event);
395 }
396 }
397
398 fn emit_routing_decision<F: FnOnce() -> RoutingDecision>(&self, build: F) {
402 if let Some(ref cb) = self.routing_decision_callback {
403 cb(build());
404 }
405 }
406
407 fn escalation_trigger_reason_to_code(r: EscalationTriggerReason) -> DorsEscalationReasonCode {
408 match r {
409 EscalationTriggerReason::RetryThreshold => DorsEscalationReasonCode::RetryThreshold,
410 EscalationTriggerReason::PoorSignal => DorsEscalationReasonCode::PoorSignal,
411 EscalationTriggerReason::Congestion => DorsEscalationReasonCode::Congestion,
412 EscalationTriggerReason::LowTtl => DorsEscalationReasonCode::LowTtl,
413 EscalationTriggerReason::LowSuccessRate => DorsEscalationReasonCode::LowSuccessRate,
414 }
415 }
416
417 fn dors_reason_code_to_routing(code: DorsReasonCode) -> RoutingReasonCode {
418 match code {
419 DorsReasonCode::InitialSelection => RoutingReasonCode::InitialSelection,
420 DorsReasonCode::PrimarySelected => RoutingReasonCode::PrimarySelected,
421 DorsReasonCode::PrimarySuccess => RoutingReasonCode::PrimarySuccess,
422 DorsReasonCode::FallbackSuccess => RoutingReasonCode::FallbackSuccess,
423 DorsReasonCode::EscalationApplied => RoutingReasonCode::EscalationApplied,
424 DorsReasonCode::CurrentUnavailable => RoutingReasonCode::CurrentUnavailable,
425 }
426 }
427
428 fn escalation_reason_code_to_routing(code: DorsEscalationReasonCode) -> RoutingReasonCode {
429 match code {
430 DorsEscalationReasonCode::FallbackSuccess => RoutingReasonCode::FallbackSuccess,
431 DorsEscalationReasonCode::RetryThreshold => RoutingReasonCode::RetryThreshold,
432 DorsEscalationReasonCode::PoorSignal => RoutingReasonCode::PoorSignal,
433 DorsEscalationReasonCode::Congestion => RoutingReasonCode::Congestion,
434 DorsEscalationReasonCode::LowTtl => RoutingReasonCode::LowTtl,
435 DorsEscalationReasonCode::LowSuccessRate => RoutingReasonCode::LowSuccessRate,
436 }
437 }
438
439 #[allow(clippy::too_many_arguments)]
448 fn build_routing_decision(
449 &self,
450 now_ms: i64,
451 phase: RoutingPhase,
452 from: Option<TransportType>,
453 to: Option<TransportType>,
454 winning_score: Option<f32>,
455 reason_code: Option<RoutingReasonCode>,
456 detailed_scores: Option<&[(TransportType, TransportScore)]>,
457 ) -> RoutingDecision {
458 let scores = match (self.routing_diagnostic, detailed_scores) {
465 (true, Some(detailed)) => detailed
466 .iter()
467 .map(|(t, s)| {
468 let mut s = s.clone();
469 s.total = display_routing_score(s.total);
470 (*t, s)
471 })
472 .collect(),
473 _ => Vec::new(),
474 };
475 RoutingDecision {
476 timestamp_ms: now_ms,
477 phase,
478 from,
479 to,
480 winning_score: winning_score.map(display_routing_score),
481 reason_code,
482 scores,
483 }
484 }
485
486 fn emit_escalation_trigger_if_deduped(
488 &mut self,
489 now_ms: i64,
490 reason_code: DorsEscalationReasonCode,
491 from: TransportType,
492 to: TransportType,
493 reason_detail: Option<String>,
494 detailed_scores: Option<&[(TransportType, TransportScore)]>,
495 ) {
496 let now = std::time::Instant::now();
497 let emit = match &self.last_escalation_trigger_emitted {
498 Some((last_code, last_at)) => {
499 *last_code != reason_code
500 || last_at.elapsed().as_secs() >= ESCALATION_TRIGGER_DEDUPE_SECS
501 }
502 None => true,
503 };
504 if emit {
505 self.last_escalation_trigger_emitted = Some((reason_code, now));
506 self.emit_dors_event(Event::dors_escalation_triggered(
507 DorsEscalationPhase::Triggered,
508 from.to_string(),
509 to.to_string(),
510 reason_code,
511 reason_detail,
512 ));
513 self.emit_routing_decision(|| {
514 self.build_routing_decision(
515 now_ms,
516 RoutingPhase::Escalated,
517 Some(from),
518 Some(to),
519 None,
520 Some(Self::escalation_reason_code_to_routing(reason_code)),
521 detailed_scores,
522 )
523 });
524 }
525 }
526
527 pub fn add_transport(&mut self, transport_type: TransportType, transport: Box<dyn Transport>) {
534 self.transports.insert(transport_type, Arc::from(transport));
535 }
536
537 pub fn send(&mut self, message: &Message) -> Result<()> {
559 let stamped;
579 let message = if message.wire_codec() != WireCodec::BinaryV1
580 && self.peer_binary_wire.contains(message.recipient.as_str())
581 {
582 let mut m = message.clone();
583 m.set_wire_codec(WireCodec::BinaryV1);
584 stamped = m;
585 &stamped
586 } else {
587 message
588 };
589
590 let available = self.get_available_transports();
591 if available.is_empty() {
592 return Err(Error::Transport(TransportError::TransportNotAvailable(
593 "No available transport".to_string(),
594 )));
595 }
596
597 let now_ms = Utc::now().timestamp_millis();
601
602 let previous = self.current_transport;
603
604 let primary = self
606 .selector
607 .select_transport(message, &available)
608 .ok_or_else(|| {
609 Error::Transport(TransportError::TransportNotAvailable(
610 "No suitable transport selected from available transports".to_string(),
611 ))
612 })?;
613
614 let detailed_scores: Option<Vec<(TransportType, TransportScore)>> =
619 if self.routing_diagnostic && self.routing_decision_callback.is_some() {
620 Some(self.selector.score_and_rank_detailed(message, &available))
621 } else {
622 None
623 };
624 let detailed_slice = detailed_scores.as_deref();
625 let scored: Vec<(TransportType, f32)> = match detailed_slice {
626 Some(d) => d.iter().map(|(t, s)| (*t, s.total)).collect(),
627 None => self.selector.score_and_rank(message, &available),
628 };
629 let scores: Vec<(String, f32)> = scored
633 .iter()
634 .map(|(t, s)| (t.to_string(), display_routing_score(*s)))
635 .collect();
636 self.emit_dors_event(Event::dors_score_updated(scores));
637
638 self.emit_routing_decision(|| {
639 self.build_routing_decision(
640 now_ms,
641 RoutingPhase::ScoreUpdated,
642 previous,
643 None,
644 None,
645 None,
646 detailed_slice,
647 )
648 });
649 let primary_score = scored
652 .iter()
653 .find(|(t, _)| *t == primary)
654 .map(|(_, s)| display_routing_score(*s))
655 .unwrap_or(0.0);
656 let selection_reason = if previous.is_none() {
657 DorsReasonCode::InitialSelection
658 } else {
659 DorsReasonCode::PrimarySelected
660 };
661 self.emit_dors_event(Event::dors_transport_selected(
662 previous.map(|t| t.to_string()),
663 primary.to_string(),
664 selection_reason,
665 primary_score,
666 ));
667 self.emit_routing_decision(|| {
668 self.build_routing_decision(
669 now_ms,
670 RoutingPhase::Selected,
671 previous,
672 Some(primary),
673 Some(primary_score),
674 Some(Self::dors_reason_code_to_routing(selection_reason)),
675 detailed_slice,
676 )
677 });
678
679 let primary_result = {
681 let transport = self
682 .transports
683 .get(&primary)
684 .ok_or_else(|| Error::Other(format!("Transport {:?} not found", primary)))?;
685 transport.send(message)
686 };
687
688 match primary_result {
689 Ok(()) => {
690 self.current_transport = Some(primary);
691 if previous != Some(primary) {
692 let reason_code = if previous.is_some_and(|p| !available.contains_key(&p)) {
693 DorsReasonCode::CurrentUnavailable
694 } else {
695 DorsReasonCode::PrimarySuccess
696 };
697 self.emit_dors_event(Event::dors_transport_switched(
698 previous.map(|t| t.to_string()),
699 primary.to_string(),
700 reason_code,
701 None,
702 ));
703 self.emit_routing_decision(|| {
704 self.build_routing_decision(
705 now_ms,
706 RoutingPhase::Switched,
707 previous,
708 Some(primary),
709 Some(primary_score),
710 Some(Self::dors_reason_code_to_routing(reason_code)),
711 detailed_slice,
712 )
713 });
714 }
715 return Ok(());
716 }
717 Err(e) => {
718 let is_peer_routing = matches!(e, TransportError::PeerNotReachable(_));
719 if is_peer_routing {
720 debug!(
721 transport = ?primary,
722 error = %e,
723 "Peer not reachable via primary transport, trying fallback"
724 );
725 } else {
726 warn!(
727 transport = ?primary,
728 error = %e,
729 "Primary transport send failed, trying fallback"
730 );
731 self.selector.record_retry_failure(primary);
732 }
733 }
734 }
735
736 let mut last_error = None;
740 let mut attempted: Vec<TransportType> = vec![primary];
741
742 for (transport_type, _score) in &scored {
743 if *transport_type == primary {
744 continue;
745 }
746
747 if primary == TransportType::BLE && *transport_type == TransportType::WiFiDirect {
749 if let Some(trigger_reason) = self.selector.escalation_trigger_reason() {
750 let reason_code = Self::escalation_trigger_reason_to_code(trigger_reason);
751 self.emit_escalation_trigger_if_deduped(
752 now_ms,
753 reason_code,
754 primary,
755 *transport_type,
756 None,
757 detailed_slice,
758 );
759 }
760 }
761
762 let transport = match self.transports.get(transport_type) {
763 Some(t) => t,
764 None => continue,
765 };
766
767 attempted.push(*transport_type);
768
769 match transport.send(message) {
770 Ok(()) => {
771 self.current_transport = Some(*transport_type);
772 self.selector.set_current_transport(*transport_type);
773 if previous != Some(*transport_type) {
775 let reason_code = if primary == TransportType::BLE
776 && *transport_type == TransportType::WiFiDirect
777 {
778 DorsReasonCode::EscalationApplied
779 } else {
780 DorsReasonCode::FallbackSuccess
781 };
782 self.emit_dors_event(Event::dors_transport_switched(
783 previous.map(|t| t.to_string()),
784 transport_type.to_string(),
785 reason_code,
786 Some("primary send failed, fallback succeeded".to_string()),
787 ));
788 let fallback = *transport_type;
789 self.emit_routing_decision(|| {
790 self.build_routing_decision(
791 now_ms,
792 RoutingPhase::Switched,
793 previous,
794 Some(fallback),
795 None,
796 Some(Self::dors_reason_code_to_routing(reason_code)),
797 detailed_slice,
798 )
799 });
800 }
801 if primary == TransportType::BLE && *transport_type == TransportType::WiFiDirect
803 {
804 self.emit_dors_event(Event::dors_escalation_triggered(
805 DorsEscalationPhase::Applied,
806 primary.to_string(),
807 transport_type.to_string(),
808 DorsEscalationReasonCode::FallbackSuccess,
809 Some("primary BLE send failed, fallback to WiFi succeeded".to_string()),
810 ));
811 let escalated = *transport_type;
812 self.emit_routing_decision(|| {
813 self.build_routing_decision(
814 now_ms,
815 RoutingPhase::Escalated,
816 Some(primary),
817 Some(escalated),
818 None,
819 Some(Self::escalation_reason_code_to_routing(
820 DorsEscalationReasonCode::FallbackSuccess,
821 )),
822 detailed_slice,
823 )
824 });
825 }
826 return Ok(());
827 }
828 Err(e) => {
829 if !matches!(e, TransportError::PeerNotReachable(_)) {
830 warn!(
831 transport = ?transport_type,
832 error = %e,
833 "Fallback transport send failed, trying next"
834 );
835 self.selector.record_retry_failure(*transport_type);
836 }
837 last_error = Some(e);
838 }
839 }
840 }
841
842 let terminal_error = last_error.unwrap_or_else(|| {
843 TransportError::SendFailed(format!("All transports failed (tried {:?})", attempted))
844 });
845 Err(Error::Transport(terminal_error))
846 }
847
848 pub fn send_via_transport(
859 &mut self,
860 message: &Message,
861 transport_type: TransportType,
862 ) -> Result<()> {
863 let transport = self
864 .transports
865 .get(&transport_type)
866 .ok_or_else(|| Error::Other(format!("Transport {:?} not found", transport_type)))?;
867
868 if transport.status() != TransportStatus::Available {
869 return Err(Error::Other(format!(
870 "Transport {:?} is not available",
871 transport_type
872 )));
873 }
874
875 transport.send(message).map_err(Error::Transport)?;
882
883 self.current_transport = Some(transport_type);
885
886 Ok(())
887 }
888
889 pub fn mesh_neighbors(&self) -> Vec<MeshNeighbor> {
903 let mut neighbors: Vec<MeshNeighbor> = Vec::new();
904
905 for transport_type in MESH_TRANSPORTS {
906 let Some(transport) = self.transports.get(transport_type) else {
907 continue;
908 };
909 for link in transport.connected_peers() {
910 if neighbors.iter().any(|n| n.peer_id == link.peer_id) {
911 continue;
912 }
913 neighbors.push(MeshNeighbor {
914 peer_id: link.peer_id,
915 rssi: link.rssi,
916 transport: *transport_type,
917 });
918 }
919 }
920
921 neighbors
922 }
923
924 pub fn can_reach_without_carrying(&self, peer_id: &str) -> bool {
950 let has_infrastructure = self.transports.iter().any(|(transport_type, transport)| {
951 !MESH_TRANSPORTS.contains(transport_type)
952 && transport.status() == TransportStatus::Available
953 });
954 if has_infrastructure {
955 return true;
956 }
957
958 MESH_TRANSPORTS.iter().any(|transport_type| {
959 self.transports
960 .get(transport_type)
961 .filter(|transport| transport.status() == TransportStatus::Available)
962 .is_some_and(|transport| {
963 transport
964 .connected_peers()
965 .iter()
966 .any(|link| link.peer_id == peer_id)
967 })
968 })
969 }
970
971 pub fn can_address_via(&self, transport_type: TransportType, peer_id: &str) -> bool {
986 let Some(transport) = self.transports.get(&transport_type) else {
987 return false;
988 };
989 if transport.status() != TransportStatus::Available {
990 return false;
991 }
992 if !MESH_TRANSPORTS.contains(&transport_type) {
993 return true;
994 }
995 transport
996 .connected_peers()
997 .iter()
998 .any(|link| link.peer_id == peer_id)
999 }
1000
1001 pub fn send_to_neighbor(&self, peer_id: &str, message: &Message) -> Result<TransportType> {
1013 let stamped;
1014 let message = if message.wire_codec() != WireCodec::BinaryV1
1015 && self.peer_binary_wire.contains(peer_id)
1016 {
1017 let mut m = message.clone();
1018 m.set_wire_codec(WireCodec::BinaryV1);
1019 stamped = m;
1020 &stamped
1021 } else if message.wire_codec() == WireCodec::BinaryV1
1022 && !self.peer_binary_wire.contains(peer_id)
1023 {
1024 let mut m = message.clone();
1028 m.set_wire_codec(WireCodec::Json);
1029 stamped = m;
1030 &stamped
1031 } else {
1032 message
1033 };
1034
1035 let mut last_error: Option<String> = None;
1036
1037 for transport_type in MESH_TRANSPORTS {
1038 let Some(transport) = self.transports.get(transport_type) else {
1039 continue;
1040 };
1041 if transport.status() != TransportStatus::Available {
1042 continue;
1043 }
1044 match transport.send_to_peer(peer_id, message) {
1045 Ok(()) => return Ok(*transport_type),
1046 Err(e) => last_error = Some(e.to_string()),
1047 }
1048 }
1049
1050 Err(Error::Transport(TransportError::PeerNotReachable(format!(
1051 "no mesh transport holds a link to {}{}",
1052 peer_id,
1053 last_error.map(|e| format!(" ({})", e)).unwrap_or_default()
1054 ))))
1055 }
1056
1057 pub fn receive(&self) -> Result<Option<(TransportType, Message)>> {
1063 for (transport_type, transport) in &self.transports {
1065 match transport.receive() {
1066 Ok(Some(message)) => {
1067 debug!(
1068 transport = ?transport_type,
1069 sender = %message.sender,
1070 recipient = %message.recipient,
1071 "TransportManager found message"
1072 );
1073 return Ok(Some((*transport_type, message)));
1074 }
1075 Ok(None) => {
1076 }
1078 Err(e) => {
1079 warn!(
1080 transport = ?transport_type,
1081 error = %e,
1082 "Transport receive error"
1083 );
1084 }
1085 }
1086 }
1087 Ok(None)
1088 }
1089
1090 pub fn get_available_transports(&self) -> HashMap<TransportType, TransportMetrics> {
1096 let mut available = HashMap::new();
1097
1098 for (transport_type, transport) in &self.transports {
1099 let maybe_metrics = {
1100 let status = transport.status();
1101 if status == TransportStatus::Available {
1102 Some(transport.metrics())
1103 } else {
1104 debug!(
1105 transport = ?transport_type,
1106 status = ?status,
1107 "Transport excluded from available set"
1108 );
1109 None
1110 }
1111 };
1112
1113 if let Some(mut metrics) = maybe_metrics {
1114 if let Some(stats) = self.observations.get(transport_type) {
1115 stats.apply_to_metrics(&mut metrics);
1116 }
1117 available.insert(*transport_type, metrics);
1118 }
1119 }
1120
1121 available
1122 }
1123
1124 pub fn current_transport(&self) -> Option<TransportType> {
1126 self.current_transport
1127 }
1128
1129 pub fn get_all_transport_statuses(&self) -> HashMap<TransportType, TransportStatus> {
1136 let mut out = HashMap::with_capacity(self.transports.len());
1137 for (transport_type, transport) in &self.transports {
1138 out.insert(*transport_type, transport.status());
1139 }
1140 out
1141 }
1142
1143 pub fn snapshot_status_and_available(
1151 &self,
1152 ) -> (
1153 HashMap<TransportType, TransportStatus>,
1154 HashMap<TransportType, TransportMetrics>,
1155 ) {
1156 let mut statuses = HashMap::with_capacity(self.transports.len());
1157 let mut available = HashMap::new();
1158
1159 for (transport_type, transport) in &self.transports {
1160 let status = transport.status();
1161 let metrics = if status == TransportStatus::Available {
1162 Some(transport.metrics())
1163 } else {
1164 None
1165 };
1166 statuses.insert(*transport_type, status);
1167 if let Some(mut metrics) = metrics {
1168 if let Some(stats) = self.observations.get(transport_type) {
1169 stats.apply_to_metrics(&mut metrics);
1170 }
1171 available.insert(*transport_type, metrics);
1172 }
1173 }
1174
1175 (statuses, available)
1176 }
1177
1178 pub fn start(&mut self) -> Result<()> {
1180 for (transport_type, transport) in &self.transports {
1181 transport.start().map_err(|e| {
1182 Error::Other(format!(
1183 "Failed to start transport {:?}: {}",
1184 transport_type, e
1185 ))
1186 })?;
1187 }
1188 Ok(())
1189 }
1190
1191 pub fn stop(&mut self) -> Result<()> {
1193 for (transport_type, transport) in &self.transports {
1194 transport.stop().map_err(|e| {
1195 Error::Other(format!(
1196 "Failed to stop transport {:?}: {}",
1197 transport_type, e
1198 ))
1199 })?;
1200 }
1201 Ok(())
1202 }
1203
1204 pub fn get_transport(&self, transport_type: TransportType) -> Option<Arc<dyn Transport>> {
1206 self.transports.get(&transport_type).cloned()
1207 }
1208
1209 pub fn remove_transport(&mut self, transport_type: TransportType) {
1215 self.transports.remove(&transport_type);
1216
1217 if self.current_transport == Some(transport_type) {
1219 self.current_transport = None;
1220 }
1221
1222 self.observations.remove(&transport_type);
1223 }
1224
1225 pub fn get_active_transports(&self) -> Vec<TransportType> {
1231 self.transports.keys().copied().collect()
1232 }
1233
1234 pub fn should_escalate_to_wifi(&self) -> bool {
1236 self.selector.should_escalate_to_wifi()
1237 }
1238
1239 pub fn record_retry_failure(&mut self, transport_type: TransportType) {
1241 self.selector.record_retry_failure(transport_type);
1242 }
1243
1244 pub fn reset_retry_count(&mut self, transport_type: TransportType) {
1246 self.selector.reset_retry_count(transport_type);
1247 }
1248
1249 pub fn record_delivery_success(
1251 &mut self,
1252 transport_type: TransportType,
1253 latency_ms: u32,
1254 hop_count: u8,
1255 ) {
1256 let stats = self.observations.entry(transport_type).or_default();
1257 stats.record_success(latency_ms, hop_count);
1258 }
1259
1260 pub fn record_delivery_failure(&mut self, transport_type: TransportType) {
1262 let stats = self.observations.entry(transport_type).or_default();
1263 stats.record_failure();
1264 }
1265
1266 pub fn update_selector_config(&mut self, config: DorsConfig) {
1269 self.selector.update_config(config);
1270 }
1271}
1272
1273#[cfg(test)]
1274mod tests {
1275 use super::*;
1276 use offline_protocol_core::{AppId, UserId};
1277 use offline_protocol_router::DorsConfig;
1278 use offline_protocol_transport::{mock::MockTransport, TransportType};
1279 use std::sync::Mutex;
1280
1281 fn create_test_message() -> Message {
1282 Message::new(
1283 UserId::new("alice").unwrap(),
1284 UserId::new("bob").unwrap(),
1285 AppId::new("test").unwrap(),
1286 "Test message",
1287 )
1288 }
1289
1290 #[test]
1291 fn test_transport_manager_creation() {
1292 let selector = TransportSelector::with_config(DorsConfig::default());
1293 let manager = TransportManager::new(selector);
1294 assert_eq!(manager.transports.len(), 0);
1295 assert!(manager.current_transport().is_none());
1296 }
1297
1298 #[test]
1299 fn test_add_transport() {
1300 let selector = TransportSelector::with_config(DorsConfig::default());
1301 let mut manager = TransportManager::new(selector);
1302
1303 let transport = Box::new(MockTransport::new(TransportType::BLE));
1304 manager.add_transport(TransportType::BLE, transport);
1305
1306 assert_eq!(manager.transports.len(), 1);
1307 }
1308
1309 #[test]
1310 fn test_start_stop_through_shared_transport_handle() {
1311 let selector = TransportSelector::with_config(DorsConfig::default());
1312 let mut manager = TransportManager::new(selector);
1313 manager.add_transport(
1314 TransportType::BLE,
1315 Box::new(MockTransport::new(TransportType::BLE)),
1316 );
1317
1318 let handle = manager
1321 .get_transport(TransportType::BLE)
1322 .expect("transport registered");
1323 assert_eq!(handle.status(), TransportStatus::Unavailable);
1324
1325 manager.start().unwrap();
1326 assert_eq!(handle.status(), TransportStatus::Available);
1327
1328 manager.stop().unwrap();
1329 assert_eq!(handle.status(), TransportStatus::Disconnected);
1330 }
1331
1332 #[test]
1333 fn test_send_message() {
1334 let selector = TransportSelector::with_config(DorsConfig::default());
1335 let mut manager = TransportManager::new(selector);
1336
1337 let transport = MockTransport::new(TransportType::BLE);
1338 transport.start().unwrap();
1339 manager.add_transport(TransportType::BLE, Box::new(transport));
1340
1341 let message = create_test_message();
1342 let result = manager.send(&message);
1343 assert!(result.is_ok());
1344 }
1345
1346 #[test]
1347 fn send_stamps_binary_only_for_capable_peer() {
1348 let selector = TransportSelector::with_config(DorsConfig::default());
1349 let mut manager = TransportManager::new(selector);
1350 let transport = MockTransport::new(TransportType::BLE);
1351 transport.start().unwrap();
1352 manager.add_transport(TransportType::BLE, Box::new(transport));
1353
1354 manager.send(&create_test_message()).unwrap();
1356 manager.mark_peer_binary_wire("bob", true);
1358 manager.send(&create_test_message()).unwrap();
1359
1360 let mock = manager
1361 .transports
1362 .get(&TransportType::BLE)
1363 .unwrap()
1364 .as_any()
1365 .downcast_ref::<MockTransport>()
1366 .unwrap();
1367 let sent = mock.sent_messages();
1368 assert_eq!(sent.len(), 2);
1369 assert_eq!(sent[0].wire_codec(), WireCodec::Json);
1370 assert_eq!(sent[1].wire_codec(), WireCodec::BinaryV1);
1371 }
1372
1373 #[test]
1374 fn send_reverts_to_json_after_peer_downgrades() {
1375 let selector = TransportSelector::with_config(DorsConfig::default());
1376 let mut manager = TransportManager::new(selector);
1377 let transport = MockTransport::new(TransportType::BLE);
1378 transport.start().unwrap();
1379 manager.add_transport(TransportType::BLE, Box::new(transport));
1380
1381 manager.mark_peer_binary_wire("bob", true);
1383 assert!(manager.peer_supports_binary_wire("bob"));
1384 manager.send(&create_test_message()).unwrap();
1385
1386 manager.mark_peer_binary_wire("bob", false);
1391 assert!(!manager.peer_supports_binary_wire("bob"));
1392 manager.send(&create_test_message()).unwrap();
1393
1394 let mock = manager
1395 .transports
1396 .get(&TransportType::BLE)
1397 .unwrap()
1398 .as_any()
1399 .downcast_ref::<MockTransport>()
1400 .unwrap();
1401 let sent = mock.sent_messages();
1402 assert_eq!(sent.len(), 2);
1403 assert_eq!(sent[0].wire_codec(), WireCodec::BinaryV1);
1404 assert_eq!(sent[1].wire_codec(), WireCodec::Json);
1405 }
1406
1407 #[test]
1408 fn test_dors_events_emitted_when_callback_set() {
1409 let selector = TransportSelector::with_config(DorsConfig::default());
1410 let mut manager = TransportManager::new(selector);
1411
1412 let transport = MockTransport::new(TransportType::BLE);
1413 transport.start().unwrap();
1414 manager.add_transport(TransportType::BLE, Box::new(transport));
1415
1416 let events: std::sync::Arc<Mutex<Vec<Event>>> = std::sync::Arc::new(Mutex::new(Vec::new()));
1417 let events_clone = events.clone();
1418 manager.set_dors_event_callback(Some(Arc::new(move |e| {
1419 events_clone.lock().unwrap().push(e);
1420 })));
1421
1422 let message = create_test_message();
1423 let _ = manager.send(&message);
1424
1425 let captured = events.lock().unwrap();
1426 assert!(
1427 captured
1428 .iter()
1429 .any(|e| matches!(e, crate::events::Event::DorsScoreUpdated { .. })),
1430 "expected dors_score_updated"
1431 );
1432 assert!(
1433 captured
1434 .iter()
1435 .any(|e| { matches!(e, crate::events::Event::DorsTransportSelected { .. }) }),
1436 "expected dors_transport_selected"
1437 );
1438 }
1439
1440 #[test]
1441 fn test_receive_message() {
1442 let selector = TransportSelector::with_config(DorsConfig::default());
1443 let mut manager = TransportManager::new(selector);
1444
1445 let transport = MockTransport::new(TransportType::BLE);
1446 transport.start().unwrap();
1447 let message = create_test_message();
1448 transport.queue_message(message.clone());
1449
1450 manager.add_transport(TransportType::BLE, Box::new(transport));
1451
1452 let received = manager.receive().unwrap();
1453 assert!(matches!(received, Some((TransportType::BLE, _))));
1454 }
1455
1456 #[test]
1457 fn test_record_delivery_success_enriches_metrics() {
1458 let selector = TransportSelector::with_config(DorsConfig::default());
1459 let mut manager = TransportManager::new(selector);
1460
1461 let transport = MockTransport::new(TransportType::BLE);
1462 transport.start().unwrap();
1463 manager.add_transport(TransportType::BLE, Box::new(transport));
1464
1465 manager.record_delivery_success(TransportType::BLE, 150, 2);
1466
1467 let metrics = manager.get_available_transports();
1468 let ble_metrics = metrics.get(&TransportType::BLE).expect("metrics");
1469
1470 assert_eq!(ble_metrics.success_count, 1);
1471 assert_eq!(ble_metrics.failure_count, 0);
1472 assert!(ble_metrics.delivery_ratio.expect("delivery ratio") > 0.99);
1473 assert_eq!(ble_metrics.average_hop_count, Some(2.0));
1474 assert_eq!(ble_metrics.latency_ms, Some(150));
1475 }
1476
1477 #[test]
1478 fn test_record_delivery_failure_enriches_metrics() {
1479 let selector = TransportSelector::with_config(DorsConfig::default());
1480 let mut manager = TransportManager::new(selector);
1481
1482 let transport = MockTransport::new(TransportType::BLE);
1483 transport.start().unwrap();
1484 manager.add_transport(TransportType::BLE, Box::new(transport));
1485
1486 manager.record_delivery_failure(TransportType::BLE);
1487 let metrics = manager.get_available_transports();
1488 let ble_metrics = metrics.get(&TransportType::BLE).expect("metrics");
1489
1490 assert_eq!(ble_metrics.success_count, 0);
1491 assert_eq!(ble_metrics.failure_count, 1);
1492 assert!(ble_metrics.drop_rate.expect("drop ratio") > 0.99);
1493 }
1494}