Skip to main content

audit_trail/
clock.rs

1//! Time source and [`Timestamp`] type.
2
3/// A timestamp expressed as nanoseconds since the Unix epoch.
4///
5/// Stored as a `u64`, so the representable range extends well beyond the
6/// 22nd century. Operations are saturating to avoid panics on overflow.
7///
8/// # Example
9///
10/// ```
11/// use audit_trail::Timestamp;
12///
13/// let t = Timestamp::from_nanos(1_700_000_000_000_000_000);
14/// assert_eq!(t.as_nanos(), 1_700_000_000_000_000_000);
15/// ```
16#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
17pub struct Timestamp(u64);
18
19impl Timestamp {
20    /// The Unix epoch (1970-01-01T00:00:00Z), expressed as nanoseconds.
21    pub const EPOCH: Self = Self(0);
22
23    /// Construct a timestamp from nanoseconds since the Unix epoch.
24    #[inline]
25    pub const fn from_nanos(nanos: u64) -> Self {
26        Self(nanos)
27    }
28
29    /// Nanoseconds since the Unix epoch.
30    #[inline]
31    pub const fn as_nanos(self) -> u64 {
32        self.0
33    }
34}
35
36/// Pluggable time source for the audit chain.
37///
38/// Implementations are expected to be monotonic with respect to successive
39/// calls. The chain enforces monotonicity at append time and returns
40/// [`crate::Error::NonMonotonicClock`] if a regression is observed.
41///
42/// # Example
43///
44/// ```
45/// use audit_trail::{Clock, Timestamp};
46///
47/// /// A fixed clock useful for testing.
48/// struct FixedClock(Timestamp);
49///
50/// impl Clock for FixedClock {
51///     fn now(&self) -> Timestamp { self.0 }
52/// }
53///
54/// let clock = FixedClock(Timestamp::from_nanos(42));
55/// assert_eq!(clock.now().as_nanos(), 42);
56/// ```
57pub trait Clock {
58    /// Return the current timestamp.
59    fn now(&self) -> Timestamp;
60}
61
62/// Wall-clock time source backed by [`std::time::SystemTime`]. Requires
63/// the `std` feature.
64///
65/// Most production deployments want this; it returns nanoseconds since
66/// the Unix epoch using the host's system clock. The host clock is **not**
67/// strictly monotonic — if the operator adjusts time backwards, the next
68/// [`crate::Chain::append`] will return [`crate::Error::NonMonotonicClock`].
69/// Deployments that need a strictly-monotonic source should wrap a
70/// monotonic instant in a custom [`Clock`] instead.
71///
72/// On the unusual case that `SystemTime::now()` is before the Unix epoch,
73/// this returns [`Timestamp::EPOCH`] (0). On the equally-unusual case
74/// that the system clock exceeds `u64::MAX` nanoseconds past the epoch
75/// (year ~2554 and later), the value saturates at `u64::MAX`.
76///
77/// # Example
78///
79/// ```
80/// use audit_trail::{Clock, SystemClock};
81///
82/// let clock = SystemClock::new();
83/// let t = clock.now();
84/// assert!(t.as_nanos() > 0);
85/// ```
86#[cfg(feature = "std")]
87#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
88#[derive(Copy, Clone, Debug, Default)]
89pub struct SystemClock;
90
91#[cfg(feature = "std")]
92impl SystemClock {
93    /// Construct a fresh `SystemClock`.
94    #[inline]
95    pub const fn new() -> Self {
96        Self
97    }
98}
99
100#[cfg(feature = "std")]
101impl Clock for SystemClock {
102    fn now(&self) -> Timestamp {
103        let nanos = std::time::SystemTime::now()
104            .duration_since(std::time::UNIX_EPOCH)
105            .map(|d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX))
106            .unwrap_or(0);
107        Timestamp::from_nanos(nanos)
108    }
109}