pubky_timestamp/
lib.rs

1#![doc = include_str!("../README.md")]
2//! ## Feature flags
3#![doc = document_features::document_features!()]
4//!
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8use std::array::TryFromSliceError;
9use std::error::Error;
10use std::fmt::Display;
11use std::time::{Duration, SystemTime};
12use std::{
13    ops::{Add, AddAssign, Sub, SubAssign},
14    sync::Mutex,
15};
16
17use once_cell::sync::Lazy;
18
19/// ~0.4% chance of none of 10 clocks have matching id.
20const CLOCK_MASK: u64 = (1 << 8) - 1;
21const TIME_MASK: u64 = !0 >> 8;
22
23pub struct TimestampFactory {
24    clock_id: u64,
25    last_time: u64,
26}
27
28impl TimestampFactory {
29    /// Create a [TimestampFactory] with a random [TimestampFactory::clock_id],
30    /// unless [getrandom()] returned and error, in which case it defaults to `0`.
31    pub fn new() -> Self {
32        let mut bytes = [0; 8];
33        let _ = getrandom::getrandom(&mut bytes);
34
35        Self {
36            clock_id: u64::from_le_bytes(bytes) & CLOCK_MASK,
37            last_time: system_time() & TIME_MASK,
38        }
39    }
40
41    /// Set the factory's `clock_id`
42    pub fn clock_id(mut self, clock_id: u8) -> TimestampFactory {
43        self.clock_id = clock_id as u64;
44        self
45    }
46
47    /// Generate a new [Timestamp]
48    pub fn now(&mut self) -> Timestamp {
49        // Ensure strict monotonicity.
50        self.last_time = (system_time() & TIME_MASK).max(self.last_time + CLOCK_MASK + 1);
51
52        // Add clock_id to the end of the timestamp
53        Timestamp(self.last_time | self.clock_id)
54    }
55}
56
57impl Default for TimestampFactory {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63pub static DEFAULT_FACTORY: Lazy<Mutex<TimestampFactory>> =
64    Lazy::new(|| Mutex::new(TimestampFactory::default()));
65
66/// Strictly monotonic timestamp since [SystemTime::UNIX_EPOCH] in microseconds.
67///
68/// The purpose of this timestamp is to unique per "user", not globally,
69/// it achieves this by:
70///     1. Override the last byte with a random `clock_id`, reducing the probability
71///         of two matching timestamps across multiple machines/threads.
72///     2. Guarantee that the remaining 3 bytes are ever increasing (strictly monotonic) within
73///         the same thread regardless of the wall clock value
74///
75/// This timestamp is also serialized as BE bytes to remain sortable.
76/// If a `utf-8` encoding is necessary, it is encoded as [base32::Alphabet::Crockford]
77/// to act as a sortable Id.
78///
79/// U64 (through pubky) of microseconds is valid for the next ~600 thousand years!
80/// i64 (in mainline)   of microseconds is valid for the next ~300 thousand years!
81#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
82pub struct Timestamp(u64);
83
84impl Timestamp {
85    /// Generate a [Timestamp] from the [DEFAULT_FACTORY].
86    pub fn now() -> Self {
87        DEFAULT_FACTORY.lock().unwrap().now()
88    }
89
90    #[cfg(feature = "httpdate")]
91    pub fn parse_http_date(date: &str) -> Result<Self, httpdate::Error> {
92        httpdate::parse_http_date(date).map(Timestamp::from)
93    }
94
95    /// Return big endian bytes representation of this timestamp.
96    pub fn to_bytes(&self) -> [u8; 8] {
97        self.0.to_be_bytes()
98    }
99
100    /// Return the internal `u64` representation of this [Timestamp].
101    pub fn as_u64(&self) -> u64 {
102        self.0
103    }
104
105    #[cfg(feature = "httpdate")]
106    pub fn format_http_date(&self) -> String {
107        httpdate::fmt_http_date(self.to_owned().into())
108    }
109}
110
111impl Default for Timestamp {
112    fn default() -> Self {
113        Timestamp::now()
114    }
115}
116
117impl TryFrom<&[u8]> for Timestamp {
118    type Error = TryFromSliceError;
119
120    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
121        let bytes: [u8; 8] = bytes.try_into()?;
122
123        Ok(bytes.into())
124    }
125}
126
127impl From<Timestamp> for [u8; 8] {
128    fn from(timestamp: Timestamp) -> Self {
129        timestamp.0.to_be_bytes()
130    }
131}
132
133impl From<[u8; 8]> for Timestamp {
134    fn from(bytes: [u8; 8]) -> Self {
135        Self(u64::from_be_bytes(bytes))
136    }
137}
138
139impl From<u64> for Timestamp {
140    fn from(inner: u64) -> Self {
141        Self(inner)
142    }
143}
144
145impl From<&Timestamp> for Timestamp {
146    fn from(timestamp: &Timestamp) -> Self {
147        *timestamp
148    }
149}
150
151impl From<Timestamp> for u64 {
152    fn from(value: Timestamp) -> Self {
153        value.as_u64()
154    }
155}
156
157impl From<Timestamp> for SystemTime {
158    fn from(timestamp: Timestamp) -> Self {
159        let secs = timestamp.0 / 1_000_000; // Extract seconds
160        let subsec_nanos = (timestamp.0 % 1_000_000) * 1_000; // Convert remaining microseconds to nanoseconds
161
162        SystemTime::UNIX_EPOCH + Duration::new(secs, subsec_nanos as u32)
163    }
164}
165
166impl From<SystemTime> for Timestamp {
167    fn from(system_time: SystemTime) -> Self {
168        (system_time
169            .duration_since(SystemTime::UNIX_EPOCH)
170            .expect("time drift")
171            .as_micros() as u64)
172            .into()
173    }
174}
175
176#[cfg(feature = "httpdate")]
177impl From<Timestamp> for httpdate::HttpDate {
178    fn from(value: Timestamp) -> Self {
179        SystemTime::from(value).into()
180    }
181}
182
183#[cfg(feature = "httpdate")]
184impl From<httpdate::HttpDate> for Timestamp {
185    fn from(value: httpdate::HttpDate) -> Self {
186        SystemTime::from(value).into()
187    }
188}
189
190// === Operations ===
191
192impl Add<u64> for Timestamp {
193    type Output = Timestamp;
194
195    fn add(self, rhs: u64) -> Self::Output {
196        Timestamp(self.0.checked_add(rhs).unwrap_or(u64::MAX))
197    }
198}
199
200impl Sub<u64> for Timestamp {
201    type Output = Timestamp;
202
203    fn sub(self, rhs: u64) -> Self::Output {
204        self.0.saturating_sub(rhs).into()
205    }
206}
207
208impl AddAssign<u64> for Timestamp {
209    fn add_assign(&mut self, other: u64) {
210        self.0 = self.0.checked_add(other).unwrap_or(u64::MAX);
211    }
212}
213
214impl SubAssign<u64> for Timestamp {
215    fn sub_assign(&mut self, other: u64) {
216        self.0 = self.0.saturating_sub(other);
217    }
218}
219
220impl Add<Timestamp> for Timestamp {
221    type Output = Timestamp;
222
223    fn add(self, rhs: Timestamp) -> Self::Output {
224        self + rhs.0
225    }
226}
227
228impl Sub<Timestamp> for Timestamp {
229    type Output = Timestamp;
230
231    fn sub(self, rhs: Timestamp) -> Self::Output {
232        self - rhs.0
233    }
234}
235
236impl AddAssign<Timestamp> for Timestamp {
237    fn add_assign(&mut self, other: Timestamp) {
238        self.0 = self.0.checked_add(other.0).unwrap_or(u64::MAX);
239    }
240}
241
242impl SubAssign<Timestamp> for Timestamp {
243    fn sub_assign(&mut self, other: Timestamp) {
244        self.0 = self.0.saturating_sub(other.0)
245    }
246}
247
248// === Serialization ===
249
250#[cfg(feature = "serde")]
251impl Serialize for Timestamp {
252    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
253    where
254        S: serde::Serializer,
255    {
256        let bytes = self.to_bytes();
257        bytes.serialize(serializer)
258    }
259}
260
261#[cfg(feature = "serde")]
262impl<'de> Deserialize<'de> for Timestamp {
263    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
264    where
265        D: serde::Deserializer<'de>,
266    {
267        let bytes: [u8; 8] = Deserialize::deserialize(deserializer)?;
268        Ok(Timestamp(u64::from_be_bytes(bytes)))
269    }
270}
271
272// === String representation (sortable base32 encoding) ===
273
274impl Display for Timestamp {
275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276        #[cfg(feature = "base32")]
277        {
278            let bytes: [u8; 8] = self.to_owned().into();
279            f.write_str(&base32::encode(base32::Alphabet::Crockford, &bytes))
280        }
281        #[cfg(not(feature = "base32"))]
282        f.write_str(&format!("Timestamp ({})", self.0))
283    }
284}
285
286impl TryFrom<String> for Timestamp {
287    type Error = InvalidEncoding;
288
289    fn try_from(value: String) -> Result<Self, Self::Error> {
290        #[cfg(feature = "base32")]
291        return match base32::decode(base32::Alphabet::Crockford, &value) {
292            Some(vec) => {
293                let bytes: [u8; 8] = vec.try_into().map_err(|_| InvalidEncoding)?;
294
295                Ok(bytes.into())
296            }
297            None => Err(InvalidEncoding),
298        };
299
300        #[cfg(not(feature = "base32"))]
301        Ok(Self(
302            value[11..value.len() - 1]
303                .parse()
304                .map_err(|_| InvalidEncoding)?,
305        ))
306    }
307}
308
309/// Return the number of microseconds since [SystemTime::UNIX_EPOCH]
310fn system_time() -> u64 {
311    #[cfg(not(target_arch = "wasm32"))]
312    {
313        SystemTime::now()
314            .duration_since(SystemTime::UNIX_EPOCH)
315            .expect("time drift")
316            .as_micros() as u64
317    }
318    #[cfg(target_arch = "wasm32")]
319    {
320        // Won't be an issue for more than 5000 years!
321        (js_sys::Date::now() as u64 )
322        // Turn milliseconds to microseconds
323        * 1000
324    }
325}
326
327#[derive(Debug)]
328pub struct InvalidEncoding;
329
330impl Error for InvalidEncoding {}
331
332impl Display for InvalidEncoding {
333    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334        f.write_str("Invalid timestamp string encoding")
335    }
336}
337
338#[cfg(test)]
339mod tests {
340    use std::collections::HashSet;
341
342    use super::*;
343
344    #[test]
345    fn strictly_monotonic() {
346        const COUNT: usize = 100;
347
348        let mut set = HashSet::with_capacity(COUNT);
349        let mut vec = Vec::with_capacity(COUNT);
350
351        for _ in 0..COUNT {
352            let timestamp = Timestamp::now();
353
354            set.insert(timestamp.clone());
355            vec.push(timestamp);
356        }
357
358        let mut ordered = vec.clone();
359        ordered.sort();
360
361        assert_eq!(set.len(), COUNT, "unique");
362        assert_eq!(ordered, vec, "ordered");
363    }
364
365    #[test]
366    fn strings() {
367        const COUNT: usize = 100;
368
369        let mut set = HashSet::with_capacity(COUNT);
370        let mut vec = Vec::with_capacity(COUNT);
371
372        for _ in 0..COUNT {
373            let string = Timestamp::now().to_string();
374
375            set.insert(string.clone());
376            vec.push(string)
377        }
378
379        let mut ordered = vec.clone();
380        ordered.sort();
381
382        assert_eq!(set.len(), COUNT, "unique");
383        assert_eq!(ordered, vec, "ordered");
384    }
385
386    #[test]
387    fn to_from_string() {
388        let timestamp = Timestamp::now();
389        let string = timestamp.to_string();
390        let decoded: Timestamp = string.try_into().unwrap();
391
392        assert_eq!(decoded, timestamp)
393    }
394
395    #[cfg(feature = "serde")]
396    #[test]
397    fn serde() {
398        let timestamp = Timestamp::now();
399
400        let serialized = postcard::to_allocvec(&timestamp).unwrap();
401
402        assert_eq!(serialized, timestamp.to_bytes());
403
404        let deserialized: Timestamp = postcard::from_bytes(&serialized).unwrap();
405
406        assert_eq!(deserialized, timestamp);
407    }
408
409    #[cfg(feature = "httpdate")]
410    #[test]
411    fn httpdate() {
412        let timestamp = Timestamp::now();
413
414        let httpdate = timestamp.format_http_date();
415
416        assert_eq!(
417            Timestamp::parse_http_date(&httpdate).unwrap().0,
418            timestamp.0 - (timestamp.0 % 1000_000) // Ignore sub seconds
419        )
420    }
421}