ubiquisync_core/hlc/timestamp.rs
1//! The [`Timestamp`] value type: the packed HLC instant every log entry
2//! carries. This is just the representation and its ordering — the clock that
3//! generates timestamps lives in [`super::clock`].
4
5/// HLC timestamp — packs `(wall_ms: 48, counter: 16)` into a single `u64`.
6/// This is the canonical in-memory and on-wire representation of a
7/// `LogEntry.timestamp` value. Plain numeric comparison on the raw u64
8/// preserves causal order (wall dominates, counter tie-breaks).
9#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
10pub struct Timestamp {
11 data: u64,
12}
13
14impl Timestamp {
15 /// Compose from `(millis, counter)` parts. `millis` must fit in the top
16 /// 48 bits — Unix-epoch ms stays under 2^48 until roughly year 10895.
17 /// A larger value would shift its high bits off the top of the `u64`
18 /// (`<<` discards them rather than panicking) and silently wrap the wall
19 /// component back toward zero, shattering monotonicity — so we panic
20 /// instead. Unreachable for any real timestamp.
21 pub fn from_parts(millis: u64, counter: u16) -> Self {
22 assert!(
23 millis >> (u64::BITS - COUNTER_BITS) == 0,
24 "millis {millis} exceeds the 48-bit wall field"
25 );
26 Self {
27 data: (millis << COUNTER_BITS) | counter as u64,
28 }
29 }
30
31 /// Reconstruct from the packed u64 representation.
32 pub fn from_raw(data: u64) -> Self {
33 Self { data }
34 }
35
36 /// The packed u64 representation: `(millis << 16) | counter`.
37 pub fn raw(&self) -> u64 {
38 self.data
39 }
40
41 /// Wall-clock component: Unix epoch milliseconds. Top 48 bits.
42 pub fn millis(&self) -> u64 {
43 self.data >> COUNTER_BITS
44 }
45
46 /// Monotonic counter within a given millisecond. Low 16 bits.
47 pub fn counter(&self) -> u16 {
48 (self.data & COUNTER_MASK) as u16
49 }
50}
51
52impl From<u64> for Timestamp {
53 fn from(data: u64) -> Self {
54 Self { data }
55 }
56}
57
58impl From<Timestamp> for u64 {
59 fn from(ts: Timestamp) -> u64 {
60 ts.data
61 }
62}
63
64const COUNTER_BITS: u32 = 16;
65const COUNTER_MASK: u64 = (1 << COUNTER_BITS) - 1;
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 #[test]
72 fn timestamp_parts_roundtrip() {
73 // Given wall + counter, from_parts + accessors must agree.
74 let t = Timestamp::from_parts(1_700_000_000_000, 42);
75 assert_eq!(t.millis(), 1_700_000_000_000);
76 assert_eq!(t.counter(), 42);
77 // raw() returns the packed form: (millis << 16) | counter.
78 assert_eq!(t.raw(), (1_700_000_000_000u64 << 16) | 42);
79 }
80
81 #[test]
82 fn timestamp_ordering_matches_raw_u64() {
83 // Counter breaks ties within the same millis.
84 let earlier = Timestamp::from_parts(100, 5);
85 let later_counter = Timestamp::from_parts(100, 6);
86 assert!(later_counter > earlier);
87 // Millis strictly dominates counter — even max counter at a lower
88 // millis loses to counter 0 at the next millis.
89 let next_ms = Timestamp::from_parts(101, 0);
90 let max_counter_prev = Timestamp::from_parts(100, u16::MAX);
91 assert!(next_ms > max_counter_prev);
92 }
93
94 #[test]
95 #[should_panic = "exceeds the 48-bit wall field"]
96 fn from_parts_rejects_wall_past_ceiling() {
97 // The wall field is 48 bits. A millis value at the ceiling would
98 // shift its top bit out and wrap the timestamp back toward zero,
99 // breaking monotonicity — we panic instead. (Year ~10895; never a
100 // real timestamp.) `counter_saturation_advances_wall` (in the clock
101 // module) deliberately tops out one ms below this so it exercises
102 // saturation safely.
103 let _ = Timestamp::from_parts(1 << (u64::BITS - COUNTER_BITS), 0);
104 }
105}