jacquardSnapshot

← snapshot

3374 bytes
//! Time, injected as a port.
//!
//! Clocks are always injected, never read ambiently, so every time-dependent
//! path is exhaustively testable without sleeping. `std::time::SystemTime` is
//! a `disallowed-type` in `clippy.toml` for exactly this reason.

use core::fmt;

use serde::{Deserialize, Serialize};

use crate::error::CoreError;

/// Milliseconds since the Unix epoch.
///
/// `i64` rather than `u64` so that differences are representable without
/// wrapping, and so pre-epoch values round-trip rather than saturating.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
#[repr(transparent)]
pub struct Timestamp(i64);

impl Timestamp {
    /// The Unix epoch.
    pub const EPOCH: Self = Self(0);

    /// Constructs a timestamp from milliseconds since the epoch.
    #[must_use]
    pub const fn from_millis(ms: i64) -> Self {
        Self(ms)
    }

    /// Milliseconds since the epoch.
    #[must_use]
    pub const fn as_millis(self) -> i64 {
        self.0
    }

    /// Returns `self` advanced by `ms` milliseconds.
    ///
    /// # Errors
    ///
    /// Returns [`CoreError::TimestampRange`] on overflow. Deadline arithmetic
    /// must not wrap: a wrapped deadline is a deadline in the past.
    pub const fn checked_add_millis(self, ms: i64) -> Result<Self, CoreError> {
        match self.0.checked_add(ms) {
            Some(v) => Ok(Self(v)),
            None => Err(CoreError::TimestampRange),
        }
    }
}

impl fmt::Display for Timestamp {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}ms", self.0)
    }
}

/// Source of the current time.
pub trait Clock: fmt::Debug {
    /// The current instant.
    fn now(&self) -> Timestamp;
}

/// Wall-clock time from the operating system.
#[derive(Debug, Clone, Copy, Default)]
pub struct SystemClock;

impl Clock for SystemClock {
    #[expect(
        clippy::disallowed_types,
        reason = "the single permitted `SystemTime` use: this is the adapter the port exists for"
    )]
    fn now(&self) -> Timestamp {
        use std::time::{SystemTime, UNIX_EPOCH};
        let ms = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map_or(0i64, |d| i64::try_from(d.as_millis()).unwrap_or(i64::MAX));
        Timestamp::from_millis(ms)
    }
}

/// A clock that returns a fixed instant and can be stepped by hand, for tests
/// and for the narrated demo.
#[derive(Debug, Clone, Copy)]
pub struct FixedClock(Timestamp);

impl FixedClock {
    /// Constructs a clock pinned to `at`.
    #[must_use]
    pub const fn new(at: Timestamp) -> Self {
        Self(at)
    }

    /// Advances the clock by `ms`, saturating rather than wrapping.
    pub const fn advance(&mut self, ms: i64) {
        self.0 = Timestamp(self.0.0.saturating_add(ms));
    }
}

impl Clock for FixedClock {
    fn now(&self) -> Timestamp {
        self.0
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn arithmetic_does_not_wrap() {
        assert_eq!(
            Timestamp::from_millis(i64::MAX).checked_add_millis(1),
            Err(CoreError::TimestampRange)
        );
    }

    #[test]
    fn fixed_clock_advances() {
        let mut c = FixedClock::new(Timestamp::from_millis(10));
        c.advance(5);
        assert_eq!(c.now(), Timestamp::from_millis(15));
    }
}