open_sesame/util/
timeout.rs

1//! Timeout tracking for activation delays
2//!
3//! Provides a clean abstraction over timeout logic instead of scattered `Option<Instant>`.
4
5use std::time::{Duration, Instant};
6
7/// Tracks timeout state for delayed actions
8#[derive(Debug, Clone)]
9pub struct TimeoutTracker {
10    /// When the timeout started (None if not active)
11    started_at: Option<Instant>,
12    /// Duration to wait before triggering
13    duration: Duration,
14}
15
16impl TimeoutTracker {
17    /// Creates a new timeout tracker with specified duration in milliseconds.
18    pub fn new(duration_ms: u64) -> Self {
19        Self {
20            started_at: None,
21            duration: Duration::from_millis(duration_ms),
22        }
23    }
24
25    /// Starts or restarts the timeout from current instant.
26    pub fn start(&mut self) {
27        self.started_at = Some(Instant::now());
28    }
29
30    /// Resets the timeout (equivalent to start).
31    pub fn reset(&mut self) {
32        self.start();
33    }
34
35    /// Cancels the timeout.
36    pub fn cancel(&mut self) {
37        self.started_at = None;
38    }
39
40    /// Returns whether timeout is active (started but not elapsed).
41    pub fn is_active(&self) -> bool {
42        self.started_at.is_some() && !self.has_elapsed()
43    }
44
45    /// Returns whether timeout has elapsed.
46    pub fn has_elapsed(&self) -> bool {
47        self.started_at
48            .map(|start| start.elapsed() >= self.duration)
49            .unwrap_or(false)
50    }
51
52    /// Returns remaining time until timeout (None when not active or already elapsed).
53    pub fn remaining(&self) -> Option<Duration> {
54        self.started_at.and_then(|start| {
55            let elapsed = start.elapsed();
56            if elapsed >= self.duration {
57                None
58            } else {
59                Some(self.duration - elapsed)
60            }
61        })
62    }
63
64    /// Returns elapsed time since start (None when not active).
65    pub fn elapsed(&self) -> Option<Duration> {
66        self.started_at.map(|start| start.elapsed())
67    }
68
69    /// Returns the deadline instant when timeout triggers.
70    pub fn deadline(&self) -> Option<Instant> {
71        self.started_at.map(|start| start + self.duration)
72    }
73
74    /// Updates the duration without resetting the timer.
75    pub fn set_duration(&mut self, duration_ms: u64) {
76        self.duration = Duration::from_millis(duration_ms);
77    }
78}
79
80impl Default for TimeoutTracker {
81    fn default() -> Self {
82        Self::new(200) // Default activation delay: 200ms
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use std::thread::sleep;
90
91    #[test]
92    fn test_timeout_not_started() {
93        let tracker = TimeoutTracker::new(100);
94        assert!(!tracker.is_active());
95        assert!(!tracker.has_elapsed());
96        assert!(tracker.remaining().is_none());
97    }
98
99    #[test]
100    fn test_timeout_started() {
101        let mut tracker = TimeoutTracker::new(1000);
102        tracker.start();
103        assert!(tracker.is_active());
104        assert!(!tracker.has_elapsed());
105        assert!(tracker.remaining().is_some());
106    }
107
108    #[test]
109    fn test_timeout_elapsed() {
110        let mut tracker = TimeoutTracker::new(10);
111        tracker.start();
112        sleep(Duration::from_millis(20));
113        assert!(tracker.has_elapsed());
114        assert!(!tracker.is_active());
115    }
116
117    #[test]
118    fn test_timeout_cancel() {
119        let mut tracker = TimeoutTracker::new(1000);
120        tracker.start();
121        assert!(tracker.is_active());
122        tracker.cancel();
123        assert!(!tracker.is_active());
124    }
125}