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}