use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ConnectivityState {
pub is_offline: bool,
pub check_pending: bool,
pub poll_pending: bool,
pub consecutive_failures: u32,
pub consecutive_successes: u32,
pub required_failures_to_go_offline: u32,
pub required_successes_to_go_online: u32,
pub offline_poll_interval_ms: u64,
}
impl Default for ConnectivityState {
fn default() -> Self {
Self {
is_offline: false,
check_pending: false,
poll_pending: false,
consecutive_failures: 0,
consecutive_successes: 0,
required_failures_to_go_offline: 2,
required_successes_to_go_online: 1,
offline_poll_interval_ms: 5000,
}
}
}
impl ConnectivityState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn trigger_check(self) -> Self {
Self {
check_pending: true,
..self
}
}
#[must_use]
pub fn clear_check_pending(self) -> Self {
Self {
check_pending: false,
..self
}
}
#[must_use]
pub fn on_probe_failed(self) -> Self {
let consecutive_failures = self.consecutive_failures.saturating_add(1);
let consecutive_successes = 0;
let is_offline = consecutive_failures >= self.required_failures_to_go_offline;
let poll_pending = is_offline;
let check_pending = !is_offline;
Self {
is_offline,
poll_pending,
check_pending,
consecutive_failures,
consecutive_successes,
..self
}
}
#[must_use]
pub fn on_probe_succeeded(self) -> Self {
let consecutive_successes = self.consecutive_successes.saturating_add(1);
let consecutive_failures = 0;
let back_online = consecutive_successes >= self.required_successes_to_go_online;
let is_offline = if back_online { false } else { self.is_offline };
let poll_pending = if back_online {
false
} else {
self.poll_pending
};
let check_pending = false;
Self {
is_offline,
poll_pending,
check_pending,
consecutive_failures,
consecutive_successes,
..self
}
}
#[must_use]
pub fn reset_debounce(self) -> Self {
Self {
consecutive_failures: 0,
consecutive_successes: 0,
..self
}
}
#[must_use]
pub const fn is_offline_mode(&self) -> bool {
self.is_offline
}
#[must_use]
pub const fn is_check_pending(&self) -> bool {
self.check_pending
}
#[must_use]
pub const fn is_poll_pending(&self) -> bool {
self.poll_pending
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_state_is_fully_online() {
let state = ConnectivityState::default();
assert!(!state.is_offline);
assert!(!state.check_pending);
assert!(!state.poll_pending);
assert_eq!(state.consecutive_failures, 0);
assert_eq!(state.consecutive_successes, 0);
}
#[test]
fn test_trigger_check_sets_check_pending() {
let state = ConnectivityState::default().trigger_check();
assert!(state.check_pending);
assert!(!state.is_offline);
assert!(!state.poll_pending);
}
#[test]
fn test_single_probe_failure_below_threshold() {
let state = ConnectivityState::default().on_probe_failed();
assert!(
!state.is_offline,
"Should not be offline after only 1 failure"
);
assert!(
state.check_pending,
"Should still be checking after 1 failure"
);
assert!(!state.poll_pending);
assert_eq!(state.consecutive_failures, 1);
}
#[test]
fn test_threshold_probe_failures_trigger_offline() {
let state = ConnectivityState::default()
.on_probe_failed()
.on_probe_failed();
assert!(
state.is_offline,
"Should be offline after 2 consecutive failures"
);
assert!(!state.check_pending, "Should not be checking when offline");
assert!(state.poll_pending);
assert_eq!(state.consecutive_failures, 2);
}
#[test]
fn test_probe_success_while_online_clears_check() {
let state = ConnectivityState::default()
.trigger_check()
.on_probe_succeeded();
assert!(!state.check_pending);
assert!(!state.is_offline);
assert_eq!(state.consecutive_failures, 0);
}
#[test]
fn test_probe_success_while_offline_triggers_back_online() {
let state = ConnectivityState {
is_offline: true,
poll_pending: true,
check_pending: false,
consecutive_failures: 2,
consecutive_successes: 0,
..Default::default()
}
.on_probe_succeeded();
assert!(
!state.is_offline,
"Should be back online after 1 successful probe"
);
assert!(!state.poll_pending);
assert!(!state.check_pending);
assert_eq!(state.consecutive_failures, 0);
assert_eq!(state.consecutive_successes, 1);
}
#[test]
fn test_debounce_resets_on_success() {
let state = ConnectivityState::default()
.on_probe_failed()
.on_probe_succeeded();
assert_eq!(
state.consecutive_failures, 0,
"Failures should reset to 0 on success"
);
assert_eq!(state.consecutive_successes, 1);
}
#[test]
fn test_clear_check_pending() {
let state = ConnectivityState::default()
.trigger_check()
.clear_check_pending();
assert!(!state.check_pending);
}
#[test]
fn test_reset_debounce() {
let state = ConnectivityState {
consecutive_failures: 3,
consecutive_successes: 2,
..Default::default()
}
.reset_debounce();
assert_eq!(state.consecutive_failures, 0);
assert_eq!(state.consecutive_successes, 0);
assert!(!state.is_offline);
}
}