mimir_core/clock.rs
1//! `ClockTime` — UTC milliseconds-since-Unix-epoch newtype used for every
2//! Mimir clock. Implements the contract in `docs/concepts/temporal-model.md`
3//! § 9.1.
4
5use std::fmt;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use thiserror::Error;
9
10/// Sentinel value reserved for "no invalidation" in the canonical form
11/// (see `docs/concepts/ir-canonical-form.md` § 3.1). This value is an
12/// encoding concern — the public API exposes absence via `Option<ClockTime>`.
13pub(crate) const NONE_SENTINEL: u64 = u64::MAX;
14
15/// A point in time, in milliseconds since the Unix epoch, UTC.
16///
17/// Honors `docs/concepts/temporal-model.md` § 9.1:
18///
19/// - Millisecond precision only in v1.
20/// - UTC exclusively — agent-provided times must be in UTC or the grammar
21/// rejects.
22/// - `u64` capacity reaches well past year 584,000,000.
23///
24/// # Examples
25///
26/// ```
27/// # #![allow(clippy::unwrap_used)]
28/// use mimir_core::ClockTime;
29///
30/// let t = ClockTime::try_from_millis(1_713_350_400_000).unwrap();
31/// assert_eq!(t.as_millis(), 1_713_350_400_000);
32/// ```
33#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
34pub struct ClockTime(u64);
35
36/// Errors returned when constructing or manipulating a [`ClockTime`].
37#[derive(Debug, Error, PartialEq, Eq)]
38pub enum ClockTimeError {
39 /// The wall clock appears to predate the Unix epoch. Mimir assumes
40 /// positive epoch times; this error is returned only when the host
41 /// clock is badly misconfigured.
42 #[error("system clock before Unix epoch")]
43 BeforeEpoch,
44
45 /// The requested value collides with the canonical-form `None`
46 /// sentinel (`u64::MAX`). The Mimir API reserves that value for
47 /// "no invalidation" encoding.
48 #[error("reserved sentinel value {0} (u64::MAX)")]
49 ReservedSentinel(u64),
50}
51
52impl ClockTime {
53 /// Construct a [`ClockTime`] from raw milliseconds since the epoch.
54 ///
55 /// # Errors
56 ///
57 /// Returns [`ClockTimeError::ReservedSentinel`] if `millis == u64::MAX`,
58 /// because that value is reserved by the canonical form to encode
59 /// `Option::<ClockTime>::None` (per `ir-canonical-form.md` § 3.1).
60 ///
61 /// # Examples
62 ///
63 /// ```
64 /// use mimir_core::{ClockTime, ClockTimeError};
65 ///
66 /// assert!(ClockTime::try_from_millis(0).is_ok());
67 /// assert_eq!(
68 /// ClockTime::try_from_millis(u64::MAX),
69 /// Err(ClockTimeError::ReservedSentinel(u64::MAX)),
70 /// );
71 /// ```
72 pub const fn try_from_millis(millis: u64) -> Result<Self, ClockTimeError> {
73 if millis == NONE_SENTINEL {
74 Err(ClockTimeError::ReservedSentinel(millis))
75 } else {
76 Ok(Self(millis))
77 }
78 }
79
80 /// Current wall-clock time as milliseconds since the Unix epoch.
81 ///
82 /// # Errors
83 ///
84 /// Returns [`ClockTimeError::BeforeEpoch`] if the host clock reports
85 /// a time before the Unix epoch (should not happen on sane hosts).
86 pub fn now() -> Result<Self, ClockTimeError> {
87 let millis = SystemTime::now()
88 .duration_since(UNIX_EPOCH)
89 .map_err(|_| ClockTimeError::BeforeEpoch)?
90 .as_millis();
91 // `as_millis` returns u128; values up to 2^64-1 ms cover year
92 // 584,000,000, so truncation cannot occur for any real clock.
93 let truncated = u64::try_from(millis).unwrap_or(NONE_SENTINEL - 1);
94 Self::try_from_millis(truncated)
95 }
96
97 /// The underlying millisecond count.
98 #[must_use]
99 pub const fn as_millis(self) -> u64 {
100 self.0
101 }
102}
103
104impl fmt::Display for ClockTime {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 write!(f, "{}ms", self.0)
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn sentinel_is_rejected() {
116 assert!(matches!(
117 ClockTime::try_from_millis(u64::MAX),
118 Err(ClockTimeError::ReservedSentinel(_))
119 ));
120 }
121
122 #[test]
123 fn ordering_is_numeric() {
124 let a = ClockTime::try_from_millis(100).expect("non-sentinel");
125 let b = ClockTime::try_from_millis(200).expect("non-sentinel");
126 assert!(a < b);
127 }
128
129 #[test]
130 fn now_is_close_to_epoch_plus_wallclock() {
131 let t = ClockTime::now().expect("wall clock sane");
132 // Any reasonable wall clock is after 2020-01-01 (1_577_836_800_000 ms).
133 assert!(t.as_millis() > 1_577_836_800_000);
134 }
135}