Skip to main content

cranpose_services/
network_status.rs

1//! Network status (connectivity + whether the connection is metered). Apps use
2//! it to defer or confirm large transfers on cellular/metered links.
3//!
4//! The default reports online + unmetered; platform backends can install a real
5//! monitor via [`set_platform_network_monitor`] (iOS `NWPathMonitor`, Android
6//! `ConnectivityManager`, web `navigator.connection`).
7
8use std::sync::{Arc, OnceLock};
9
10use crate::registry::{RecoveryGate, ServiceRegistry};
11
12/// A snapshot of the current network state.
13#[derive(Clone, Copy, PartialEq, Eq, Debug)]
14pub struct NetworkStatus {
15    /// Whether the device currently has a usable network path.
16    pub online: bool,
17    /// Whether the active path is metered/expensive (cellular, hotspot, …).
18    pub metered: bool,
19}
20
21impl Default for NetworkStatus {
22    fn default() -> Self {
23        Self {
24            online: true,
25            metered: false,
26        }
27    }
28}
29
30/// Reports the current network status.
31pub trait NetworkMonitor: Send + Sync {
32    fn status(&self) -> NetworkStatus;
33    fn is_alive(&self) -> bool;
34    fn reconnect(&self);
35}
36
37pub type NetworkMonitorRef = Arc<dyn NetworkMonitor>;
38
39struct DefaultNetworkMonitor;
40
41impl NetworkMonitor for DefaultNetworkMonitor {
42    fn status(&self) -> NetworkStatus {
43        NetworkStatus::default()
44    }
45
46    fn is_alive(&self) -> bool {
47        true
48    }
49
50    fn reconnect(&self) {}
51}
52
53static PLATFORM_NETWORK_MONITOR: ServiceRegistry<dyn NetworkMonitor> = ServiceRegistry::new();
54static DEFAULT_NETWORK_MONITOR: OnceLock<NetworkMonitorRef> = OnceLock::new();
55static NETWORK_MONITOR_HANDLE: OnceLock<NetworkMonitorRef> = OnceLock::new();
56static NETWORK_RECOVERY: RecoveryGate = RecoveryGate::new();
57
58struct PlatformNetworkMonitor;
59
60fn registered_network_monitor() -> NetworkMonitorRef {
61    PLATFORM_NETWORK_MONITOR
62        .get_or_warn("network monitor")
63        .unwrap_or_else(|| {
64            DEFAULT_NETWORK_MONITOR
65                .get_or_init(|| Arc::new(DefaultNetworkMonitor))
66                .clone()
67        })
68}
69
70fn active_network_monitor() -> NetworkMonitorRef {
71    let monitor = registered_network_monitor();
72    if monitor.is_alive() {
73        NETWORK_RECOVERY.succeeded();
74    } else if NETWORK_RECOVERY.try_start() {
75        monitor.reconnect();
76    }
77    monitor
78}
79
80impl NetworkMonitor for PlatformNetworkMonitor {
81    fn status(&self) -> NetworkStatus {
82        active_network_monitor().status()
83    }
84
85    fn is_alive(&self) -> bool {
86        registered_network_monitor().is_alive()
87    }
88
89    fn reconnect(&self) {
90        registered_network_monitor().reconnect();
91    }
92}
93
94/// Installs a platform network monitor, replacing any previous one.
95pub fn set_platform_network_monitor(monitor: NetworkMonitorRef) {
96    PLATFORM_NETWORK_MONITOR.set(monitor);
97    NETWORK_RECOVERY.succeeded();
98}
99
100/// Removes any registered platform network monitor (tests and teardown).
101pub fn clear_platform_network_monitor() {
102    PLATFORM_NETWORK_MONITOR.clear();
103}
104
105/// The active network monitor: the platform one if installed, else the default
106/// (online, unmetered).
107pub fn network_monitor() -> NetworkMonitorRef {
108    NETWORK_MONITOR_HANDLE
109        .get_or_init(|| Arc::new(PlatformNetworkMonitor))
110        .clone()
111}
112
113/// Convenience: the current network status.
114pub fn network_status() -> NetworkStatus {
115    let monitor = network_monitor();
116    if monitor.is_alive() {
117        NETWORK_RECOVERY.succeeded();
118    } else if NETWORK_RECOVERY.try_start() {
119        monitor.reconnect();
120    }
121    monitor.status()
122}
123
124#[cfg(test)]
125mod tests {
126    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
127
128    use super::*;
129
130    #[test]
131    fn default_is_online_unmetered_and_overridable() {
132        let _guard = crate::registry::test_service_guard();
133        clear_platform_network_monitor();
134        assert_eq!(
135            network_status(),
136            NetworkStatus {
137                online: true,
138                metered: false
139            }
140        );
141        struct Metered;
142        impl NetworkMonitor for Metered {
143            fn status(&self) -> NetworkStatus {
144                NetworkStatus {
145                    online: true,
146                    metered: true,
147                }
148            }
149
150            fn is_alive(&self) -> bool {
151                true
152            }
153
154            fn reconnect(&self) {}
155        }
156        set_platform_network_monitor(Arc::new(Metered));
157        assert!(network_status().metered);
158        clear_platform_network_monitor();
159    }
160
161    #[test]
162    fn dead_monitor_reconnects_before_status_is_read() {
163        let _guard = crate::registry::test_service_guard();
164        struct Reconnecting {
165            alive: AtomicBool,
166            reconnects: AtomicUsize,
167        }
168        impl NetworkMonitor for Reconnecting {
169            fn status(&self) -> NetworkStatus {
170                NetworkStatus {
171                    online: self.alive.load(Ordering::Acquire),
172                    metered: false,
173                }
174            }
175            fn is_alive(&self) -> bool {
176                self.alive.load(Ordering::Acquire)
177            }
178            fn reconnect(&self) {
179                self.reconnects.fetch_add(1, Ordering::AcqRel);
180                self.alive.store(true, Ordering::Release);
181            }
182        }
183        clear_platform_network_monitor();
184        let monitor = Arc::new(Reconnecting {
185            alive: AtomicBool::new(false),
186            reconnects: AtomicUsize::new(0),
187        });
188        set_platform_network_monitor(monitor.clone());
189        assert!(network_status().online);
190        assert_eq!(monitor.reconnects.load(Ordering::Acquire), 1);
191        clear_platform_network_monitor();
192    }
193}