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