Skip to main content

hardless_core/
time.rs

1//! Time: [`Millis`] timestamps and the injectable [`Clock`] (FR-05).
2
3use core::fmt;
4use core::ops::Add;
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::sync::Arc;
7
8use serde::{Deserialize, Serialize};
9
10/// A timestamp or duration in milliseconds.
11///
12/// The engine never reads a clock: whoever drives a device passes `now` in. The epoch is the
13/// caller's choice (the server uses time since start-up; tests usually start at zero).
14///
15/// ```
16/// use hardless_core::Millis;
17/// assert_eq!(Millis(1_000) + 300, Millis(1_300));
18/// assert_eq!(Millis(1_300).since(Millis(1_000)), 300);
19/// ```
20#[derive(
21    Clone,
22    Copy,
23    Debug,
24    Default,
25    PartialEq,
26    Eq,
27    PartialOrd,
28    Ord,
29    Hash,
30    Serialize,
31    Deserialize,
32    schemars::JsonSchema,
33)]
34#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
35#[serde(transparent)]
36pub struct Millis(pub u64);
37
38impl Millis {
39    /// The zero timestamp.
40    pub const ZERO: Millis = Millis(0);
41
42    /// Milliseconds elapsed from `earlier` to `self`, or zero if `earlier` is later.
43    ///
44    /// ```
45    /// use hardless_core::Millis;
46    /// assert_eq!(Millis(5).since(Millis(9)), 0);
47    /// ```
48    pub const fn since(self, earlier: Millis) -> u64 {
49        self.0.saturating_sub(earlier.0)
50    }
51}
52
53impl Add<u64> for Millis {
54    type Output = Millis;
55
56    /// Saturating addition, so a deadline far in the future never wraps around.
57    fn add(self, ms: u64) -> Millis {
58        Millis(self.0.saturating_add(ms))
59    }
60}
61
62impl fmt::Display for Millis {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        write!(f, "{}ms", self.0)
65    }
66}
67
68/// A source of time, injected into the [`Registry`](crate::Registry) (FR-05).
69///
70/// The server passes a system clock; tests pass a [`FakeClock`]. Any
71/// `Fn() -> Millis + Send + Sync` closure is also a clock.
72///
73/// ```
74/// use hardless_core::{Clock, Millis};
75/// let fixed = || Millis(42);
76/// assert_eq!(fixed.now(), Millis(42));
77/// ```
78pub trait Clock: Send + Sync {
79    /// The current time.
80    fn now(&self) -> Millis;
81}
82
83impl<F> Clock for F
84where
85    F: Fn() -> Millis + Send + Sync,
86{
87    fn now(&self) -> Millis {
88        self()
89    }
90}
91
92/// A manually driven clock for tests. Clones share the same time.
93///
94/// ```
95/// use hardless_core::{Clock, FakeClock, Millis};
96/// let clock = FakeClock::new();
97/// let handle = clock.clone(); // e.g. give `clock` to a Registry and keep `handle`
98/// handle.advance(300);
99/// assert_eq!(clock.now(), Millis(300));
100/// ```
101#[derive(Clone, Debug, Default)]
102pub struct FakeClock(Arc<AtomicU64>);
103
104impl FakeClock {
105    /// A fake clock starting at [`Millis::ZERO`].
106    pub fn new() -> Self {
107        Self::default()
108    }
109
110    /// A fake clock starting at `start`.
111    pub fn at(start: Millis) -> Self {
112        Self(Arc::new(AtomicU64::new(start.0)))
113    }
114
115    /// Sets the time. Setting it backwards is allowed; the registry keeps its own timestamps
116    /// monotonic.
117    pub fn set(&self, now: Millis) {
118        self.0.store(now.0, Ordering::SeqCst);
119    }
120
121    /// Moves the time forward by `ms` milliseconds (saturating).
122    pub fn advance(&self, ms: u64) {
123        let mut current = self.0.load(Ordering::SeqCst);
124        loop {
125            let next = current.saturating_add(ms);
126            match self
127                .0
128                .compare_exchange(current, next, Ordering::SeqCst, Ordering::SeqCst)
129            {
130                Ok(_) => return,
131                Err(actual) => current = actual,
132            }
133        }
134    }
135}
136
137impl Clock for FakeClock {
138    fn now(&self) -> Millis {
139        Millis(self.0.load(Ordering::SeqCst))
140    }
141}