//! 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));
}
}