use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientTimeout {
After(Duration),
Never,
}
impl Default for ClientTimeout {
fn default() -> Self {
Self::After(Duration::from_secs(10))
}
}
impl ClientTimeout {
#[must_use]
pub const fn from_seconds(seconds: u64) -> Self {
if seconds == 0 {
Self::Never
} else {
Self::After(Duration::from_secs(seconds))
}
}
#[must_use]
pub const fn duration(self) -> Option<Duration> {
match self {
Self::After(limit) => Some(limit),
Self::Never => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WatchState {
Connected,
Waiting {
remaining: Duration,
},
Expired,
}
impl WatchState {
#[must_use]
pub const fn is_expired(self) -> bool {
matches!(self, Self::Expired)
}
}
#[derive(Debug)]
pub struct ClientWatch {
timeout: ClientTimeout,
alone_since: Option<Instant>,
ever_connected: bool,
}
impl ClientWatch {
#[must_use]
pub const fn new(timeout: ClientTimeout, now: Instant) -> Self {
Self {
timeout,
alone_since: Some(now),
ever_connected: false,
}
}
#[must_use]
pub const fn ever_connected(&self) -> bool {
self.ever_connected
}
pub fn observe(&mut self, clients: usize, now: Instant) -> WatchState {
if clients > 0 {
self.alone_since = None;
self.ever_connected = true;
return WatchState::Connected;
}
let Some(limit) = self.timeout.duration() else {
self.alone_since.get_or_insert(now);
return WatchState::Waiting {
remaining: Duration::MAX,
};
};
let since = *self.alone_since.get_or_insert(now);
let waited = now.saturating_duration_since(since);
limit
.checked_sub(waited)
.filter(|remaining| !remaining.is_zero())
.map_or(WatchState::Expired, |remaining| WatchState::Waiting {
remaining,
})
}
}
#[cfg(test)]
mod tests {
use std::time::{Duration, Instant};
use super::{ClientTimeout, ClientWatch, WatchState};
#[test]
fn ten_seconds_is_the_default() {
assert_eq!(
ClientTimeout::default(),
ClientTimeout::After(Duration::from_secs(10))
);
}
#[test]
fn zero_seconds_means_never_rather_than_immediately() {
assert_eq!(ClientTimeout::from_seconds(0), ClientTimeout::Never);
assert_eq!(ClientTimeout::Never.duration(), None);
assert_eq!(
ClientTimeout::from_seconds(30),
ClientTimeout::After(Duration::from_secs(30))
);
}
#[test]
fn a_connected_client_stops_the_clock() {
let start = Instant::now();
let mut watch = ClientWatch::new(ClientTimeout::from_seconds(10), start);
assert_eq!(watch.observe(1, start), WatchState::Connected);
let later = start + Duration::from_secs(600);
assert_eq!(watch.observe(1, later), WatchState::Connected);
assert!(watch.ever_connected());
}
#[test]
fn the_clock_runs_from_startup_when_nobody_ever_connects() {
let start = Instant::now();
let mut watch = ClientWatch::new(ClientTimeout::from_seconds(10), start);
assert!(matches!(
watch.observe(0, start + Duration::from_secs(9)),
WatchState::Waiting { .. }
));
assert_eq!(
watch.observe(0, start + Duration::from_secs(10)),
WatchState::Expired
);
assert!(!watch.ever_connected(), "nobody ever connected");
}
#[test]
fn losing_the_last_client_starts_the_clock_again() {
let start = Instant::now();
let mut watch = ClientWatch::new(ClientTimeout::from_seconds(10), start);
watch.observe(1, start);
let left = start + Duration::from_secs(100);
assert!(matches!(watch.observe(0, left), WatchState::Waiting { .. }));
assert!(matches!(
watch.observe(0, left + Duration::from_secs(9)),
WatchState::Waiting { .. }
));
assert_eq!(
watch.observe(0, left + Duration::from_secs(10)),
WatchState::Expired
);
}
#[test]
fn reconnecting_before_the_limit_cancels_it() {
let start = Instant::now();
let mut watch = ClientWatch::new(ClientTimeout::from_seconds(10), start);
watch.observe(1, start);
let left = start + Duration::from_secs(50);
watch.observe(0, left);
assert_eq!(
watch.observe(1, left + Duration::from_secs(5)),
WatchState::Connected
);
assert_eq!(
watch.observe(1, left + Duration::from_secs(500)),
WatchState::Connected
);
}
#[test]
fn never_does_not_expire_however_long_it_waits() {
let start = Instant::now();
let mut watch = ClientWatch::new(ClientTimeout::Never, start);
for days in [1, 7, 365] {
let state = watch.observe(0, start + Duration::from_secs(days * 86_400));
assert!(
!state.is_expired(),
"an unlimited host must never expire, but did after {days} day(s)"
);
}
}
#[test]
fn remaining_counts_down() {
let start = Instant::now();
let mut watch = ClientWatch::new(ClientTimeout::from_seconds(10), start);
assert_eq!(
watch.observe(0, start + Duration::from_secs(3)),
WatchState::Waiting {
remaining: Duration::from_secs(7)
}
);
assert_eq!(
watch.observe(0, start + Duration::from_secs(8)),
WatchState::Waiting {
remaining: Duration::from_secs(2)
}
);
}
}