Skip to main content

made_adapters/
clock.rs

1//! Clock adapter.
2//!
3//! The domain never reads the wall clock directly. Aggregates receive
4//! an `OffsetDateTime` through [`ClockPort`] so deliberations stay
5//! reproducible under test.
6
7use std::time::Instant;
8
9use made_core::ports::ClockPort;
10use made_core::value_objects::DurationMs;
11use time::OffsetDateTime;
12
13/// Wall-clock implementation of [`ClockPort`] that returns UTC time
14/// from the host's monotonic source as known to `time`.
15#[derive(Debug, Clone, Copy)]
16pub struct SystemClock {
17    started_at: Instant,
18}
19
20impl SystemClock {
21    #[must_use]
22    pub fn new() -> Self {
23        Self {
24            started_at: Instant::now(),
25        }
26    }
27}
28
29impl Default for SystemClock {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35impl ClockPort for SystemClock {
36    fn now(&self) -> OffsetDateTime {
37        OffsetDateTime::now_utc()
38    }
39
40    fn uptime(&self) -> DurationMs {
41        DurationMs::from_millis(
42            u64::try_from(self.started_at.elapsed().as_millis()).unwrap_or(u64::MAX),
43        )
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use std::thread::sleep;
51    use std::time::Duration;
52
53    #[test]
54    fn now_returns_utc() {
55        let now = SystemClock::new().now();
56        assert_eq!(now.offset(), time::UtcOffset::UTC);
57    }
58
59    #[test]
60    fn subsequent_reads_are_monotonically_non_decreasing() {
61        let clock = SystemClock::new();
62        let a = clock.now();
63        sleep(Duration::from_millis(1));
64        let b = clock.now();
65        assert!(b >= a, "wall clock went backwards: {a:?} -> {b:?}");
66    }
67}