Skip to main content

waggle_core/
time.rs

1//! Time as a value. The core never asks a clock (design doc `03 §1`) —
2//! callers pass `now` explicitly, which is also what makes replay (doc `04`)
3//! trivially deterministic.
4
5use serde::{Deserialize, Serialize};
6
7/// Milliseconds since the Unix epoch, as a value.
8///
9/// There is deliberately no `Timestamp::now()`: the host supplies time
10/// (natively from `SystemTime`, in Workers from `Date.now()`, in tests as a
11/// constant). Ordering and arithmetic are the only operations the domain
12/// needs.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
14#[serde(transparent)]
15pub struct Timestamp(u64);
16
17impl Timestamp {
18    /// Wrap a unix-epoch-milliseconds value.
19    #[must_use]
20    pub const fn from_unix_ms(ms: u64) -> Self {
21        Self(ms)
22    }
23
24    /// The raw unix-epoch-milliseconds value.
25    #[must_use]
26    pub const fn as_unix_ms(self) -> u64 {
27        self.0
28    }
29
30    /// This timestamp advanced by `ms` milliseconds (saturating).
31    #[must_use]
32    pub const fn plus_ms(self, ms: u64) -> Self {
33        Self(self.0.saturating_add(ms))
34    }
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40
41    #[test]
42    fn ordering_is_chronological() {
43        let earlier = Timestamp::from_unix_ms(1_000);
44        let later = Timestamp::from_unix_ms(2_000);
45        assert!(earlier < later);
46        assert_eq!(earlier.plus_ms(1_000), later);
47    }
48
49    #[test]
50    fn plus_saturates_instead_of_wrapping() {
51        let max = Timestamp::from_unix_ms(u64::MAX);
52        assert_eq!(max.plus_ms(5), max);
53    }
54
55    #[test]
56    fn serde_is_transparent() {
57        let ts = Timestamp::from_unix_ms(42);
58        let json = serde_json::to_string(&ts).unwrap();
59        assert_eq!(json, "42");
60        let back: Timestamp = serde_json::from_str(&json).unwrap();
61        assert_eq!(back, ts);
62    }
63}