Skip to main content

actr_hyper/lifecycle/
connection_supervisor.rs

1use super::network_event::{
2    AppLifecycleState, CleanupReason, LONG_BACKGROUND_RECONNECT_THRESHOLD_MS, NetworkEvent,
3    NetworkRecoveryAction, NetworkSnapshot, ReconnectReason,
4};
5
6/// Stable fact model used to converge mobile/network lifecycle events before execution.
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8pub enum ConnectionFact {
9    NetworkSnapshotChanged(NetworkSnapshot),
10    AppEnteredBackground,
11    AppEnteredForeground { background_duration_ms: u64 },
12    CleanupRequested(CleanupReason),
13    ForceReconnectRequested(ReconnectReason),
14}
15
16impl ConnectionFact {
17    pub fn from_network_event(event: &NetworkEvent) -> Self {
18        match event {
19            NetworkEvent::NetworkPathChanged { snapshot } => {
20                Self::NetworkSnapshotChanged(snapshot.clone())
21            }
22            NetworkEvent::AppLifecycleChanged { state } => match state {
23                AppLifecycleState::Background => Self::AppEnteredBackground,
24                AppLifecycleState::Foreground {
25                    background_duration_ms,
26                } => Self::AppEnteredForeground {
27                    background_duration_ms: *background_duration_ms,
28                },
29            },
30            NetworkEvent::CleanupConnections { reason } => Self::CleanupRequested(*reason),
31            NetworkEvent::ForceReconnect { reason } => Self::ForceReconnectRequested(*reason),
32        }
33    }
34}
35
36/// Pure decision state for a settled connection event batch.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct ConnectionSupervisor {
39    cleanup_requested: Option<CleanupReason>,
40    force_reconnect_requested: Option<ReconnectReason>,
41    latest_state_action: NetworkRecoveryAction,
42    latest_snapshot_sequence: Option<u64>,
43}
44
45impl Default for ConnectionSupervisor {
46    fn default() -> Self {
47        Self {
48            cleanup_requested: None,
49            force_reconnect_requested: None,
50            latest_state_action: NetworkRecoveryAction::Noop,
51            latest_snapshot_sequence: None,
52        }
53    }
54}
55
56impl ConnectionSupervisor {
57    pub fn new() -> Self {
58        Self::default()
59    }
60
61    pub fn from_events(events: &[NetworkEvent]) -> Self {
62        let mut supervisor = Self::new();
63        for event in events {
64            supervisor.submit_event(event);
65        }
66        supervisor
67    }
68
69    pub fn select_action(events: &[NetworkEvent]) -> NetworkRecoveryAction {
70        Self::from_events(events).reconcile()
71    }
72
73    pub fn submit_event(&mut self, event: &NetworkEvent) {
74        self.submit_fact(ConnectionFact::from_network_event(event));
75    }
76
77    pub fn submit_fact(&mut self, fact: ConnectionFact) {
78        match fact {
79            ConnectionFact::CleanupRequested(reason) => {
80                self.cleanup_requested = Some(reason);
81            }
82            ConnectionFact::ForceReconnectRequested(reason) => {
83                self.force_reconnect_requested = Some(reason);
84            }
85            ConnectionFact::NetworkSnapshotChanged(snapshot) => {
86                let is_latest = self
87                    .latest_snapshot_sequence
88                    .map(|sequence| snapshot.sequence >= sequence)
89                    .unwrap_or(true);
90                if is_latest {
91                    self.latest_snapshot_sequence = Some(snapshot.sequence);
92                    self.latest_state_action = if snapshot.is_offline() {
93                        NetworkRecoveryAction::Offline
94                    } else if snapshot.should_restore() {
95                        NetworkRecoveryAction::Restore
96                    } else {
97                        NetworkRecoveryAction::Probe
98                    };
99                }
100            }
101            ConnectionFact::AppEnteredForeground {
102                background_duration_ms,
103            } => {
104                if background_duration_ms >= LONG_BACKGROUND_RECONNECT_THRESHOLD_MS {
105                    self.force_reconnect_requested = Some(ReconnectReason::LongBackground);
106                } else if self.latest_state_action == NetworkRecoveryAction::Noop {
107                    self.latest_state_action = NetworkRecoveryAction::Probe;
108                }
109            }
110            ConnectionFact::AppEnteredBackground => {}
111        }
112    }
113
114    pub fn reconcile(&self) -> NetworkRecoveryAction {
115        if self.cleanup_requested.is_some() {
116            NetworkRecoveryAction::CleanupOnly
117        } else if self.latest_state_action == NetworkRecoveryAction::Offline {
118            NetworkRecoveryAction::Offline
119        } else if self.force_reconnect_requested.is_some() {
120            NetworkRecoveryAction::ForceReconnect
121        } else {
122            self.latest_state_action
123        }
124    }
125}