rialo-types 0.18.1

Rialo Types
Documentation
// Copyright (c) Subzero Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

//! Strongly-typed millisecond time units.
//!
//! Historically the codebase used a bare `type TimestampMs = u64` for both
//! *absolute* instants (e.g. a block commit time) and *relative* spans (e.g. an
//! update period or timeout). Because both were the same primitive, nothing
//! stopped a span being passed where an instant was expected, or two instants
//! being added together. These two newtypes split the concept apart and define
//! the only arithmetic that makes sense between them:
//!
//! - `TimestampMs ± DurationMs = TimestampMs`
//! - `TimestampMs − TimestampMs = DurationMs` (via [`TimestampMs::saturating_duration_since`])
//! - `DurationMs ± DurationMs = DurationMs`
//! - adding two [`TimestampMs`] together is intentionally **not** possible.
//!
//! Both types are `#[repr(transparent)]` and serialize identically to the
//! underlying `u64` (serde transparent; borsh of a single-field struct), so
//! existing on-disk and on-chain encodings are unchanged.

use std::{
    fmt,
    ops::{Add, Sub},
    str::FromStr,
    time::Duration,
};

use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};

/// An absolute point in time, measured in milliseconds since the Unix epoch.
#[derive(
    BorshSerialize,
    BorshDeserialize,
    Clone,
    Copy,
    Debug,
    Default,
    Eq,
    Hash,
    Ord,
    PartialEq,
    PartialOrd,
    Serialize,
    Deserialize,
)]
#[serde(transparent)]
#[repr(transparent)]
pub struct TimestampMs(u64);

impl TimestampMs {
    /// The Unix epoch (0 ms).
    pub const ZERO: Self = Self(0);
    /// The largest representable timestamp.
    pub const MAX: Self = Self(u64::MAX);

    /// Creates a timestamp from raw milliseconds since the Unix epoch.
    pub const fn new(millis: u64) -> Self {
        Self(millis)
    }

    /// Returns the raw milliseconds since the Unix epoch.
    pub const fn as_millis(self) -> u64 {
        self.0
    }

    /// Returns the milliseconds as little-endian bytes.
    pub const fn to_le_bytes(self) -> [u8; 8] {
        self.0.to_le_bytes()
    }

    /// Constructs from little-endian milliseconds bytes.
    pub const fn from_le_bytes(bytes: [u8; 8]) -> Self {
        Self(u64::from_le_bytes(bytes))
    }

    /// Advances this instant by `rhs`, saturating at [`TimestampMs::MAX`].
    pub const fn saturating_add(self, rhs: DurationMs) -> Self {
        Self(self.0.saturating_add(rhs.0))
    }

    /// Moves this instant back by `rhs`, saturating at [`TimestampMs::ZERO`].
    pub const fn saturating_sub(self, rhs: DurationMs) -> Self {
        Self(self.0.saturating_sub(rhs.0))
    }

    /// Moves this instant back by `rhs`, returning `None` on underflow.
    pub const fn checked_sub(self, rhs: DurationMs) -> Option<Self> {
        match self.0.checked_sub(rhs.0) {
            Some(v) => Some(Self(v)),
            None => None,
        }
    }

    /// Advances this instant by `rhs`, returning `None` on overflow.
    pub const fn checked_add(self, rhs: DurationMs) -> Option<Self> {
        match self.0.checked_add(rhs.0) {
            Some(v) => Some(Self(v)),
            None => None,
        }
    }

    /// Returns the duration elapsed from `earlier` to `self`, saturating at
    /// [`DurationMs::ZERO`] if `earlier` is later than `self`.
    pub const fn saturating_duration_since(self, earlier: Self) -> DurationMs {
        DurationMs(self.0.saturating_sub(earlier.0))
    }
}

impl From<u64> for TimestampMs {
    fn from(millis: u64) -> Self {
        Self(millis)
    }
}

impl From<TimestampMs> for u64 {
    fn from(ts: TimestampMs) -> Self {
        ts.0
    }
}

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

impl FromStr for TimestampMs {
    type Err = std::num::ParseIntError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        s.parse::<u64>().map(Self)
    }
}

impl Add<DurationMs> for TimestampMs {
    type Output = TimestampMs;
    fn add(self, rhs: DurationMs) -> Self::Output {
        TimestampMs(self.0.saturating_add(rhs.0))
    }
}

impl Sub<DurationMs> for TimestampMs {
    type Output = TimestampMs;
    fn sub(self, rhs: DurationMs) -> Self::Output {
        TimestampMs(self.0.saturating_sub(rhs.0))
    }
}

impl Sub<TimestampMs> for TimestampMs {
    type Output = DurationMs;
    fn sub(self, rhs: TimestampMs) -> Self::Output {
        DurationMs(self.0.saturating_sub(rhs.0))
    }
}

/// A relative span of time, measured in milliseconds (a period, timeout, or delay).
#[derive(
    BorshSerialize,
    BorshDeserialize,
    Clone,
    Copy,
    Debug,
    Default,
    Eq,
    Hash,
    Ord,
    PartialEq,
    PartialOrd,
    Serialize,
    Deserialize,
)]
#[serde(transparent)]
#[repr(transparent)]
pub struct DurationMs(u64);

impl DurationMs {
    /// A zero-length span.
    pub const ZERO: Self = Self(0);
    /// The largest representable span.
    pub const MAX: Self = Self(u64::MAX);

    /// Creates a span from raw milliseconds.
    pub const fn new(millis: u64) -> Self {
        Self(millis)
    }

    /// Creates a span from whole seconds, saturating on overflow.
    pub const fn from_secs(secs: u64) -> Self {
        Self(secs.saturating_mul(1000))
    }

    /// Returns the span in raw milliseconds.
    pub const fn as_millis(self) -> u64 {
        self.0
    }

    /// Adds two spans, saturating at [`DurationMs::MAX`].
    pub const fn saturating_add(self, rhs: Self) -> Self {
        Self(self.0.saturating_add(rhs.0))
    }

    /// Subtracts two spans, saturating at [`DurationMs::ZERO`].
    pub const fn saturating_sub(self, rhs: Self) -> Self {
        Self(self.0.saturating_sub(rhs.0))
    }
}

impl From<u64> for DurationMs {
    fn from(millis: u64) -> Self {
        Self(millis)
    }
}

impl From<DurationMs> for u64 {
    fn from(span: DurationMs) -> Self {
        span.0
    }
}

impl From<DurationMs> for Duration {
    fn from(span: DurationMs) -> Self {
        Duration::from_millis(span.0)
    }
}

impl fmt::Display for DurationMs {
    // Plain number (no "ms" suffix) so `Display` round-trips through `FromStr`,
    // which clap's `default_value_t` relies on (it formats the default, then
    // re-parses it with the value parser).
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl FromStr for DurationMs {
    type Err = std::num::ParseIntError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        s.parse::<u64>().map(Self)
    }
}

impl Add for DurationMs {
    type Output = DurationMs;
    fn add(self, rhs: Self) -> Self::Output {
        DurationMs(self.0.saturating_add(rhs.0))
    }
}

impl Sub for DurationMs {
    type Output = DurationMs;
    fn sub(self, rhs: Self) -> Self::Output {
        DurationMs(self.0.saturating_sub(rhs.0))
    }
}

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

    #[test]
    fn instant_plus_duration_is_instant() {
        let t = TimestampMs::new(1_000);
        assert_eq!(
            t.saturating_add(DurationMs::new(500)),
            TimestampMs::new(1_500)
        );
        assert_eq!(t + DurationMs::new(500), TimestampMs::new(1_500));
    }

    #[test]
    fn instant_minus_instant_is_duration() {
        let a = TimestampMs::new(1_500);
        let b = TimestampMs::new(1_000);
        assert_eq!(a.saturating_duration_since(b), DurationMs::new(500));
        assert_eq!(a - b, DurationMs::new(500));
    }

    #[test]
    fn saturation_clamps() {
        assert_eq!(
            TimestampMs::ZERO.saturating_sub(DurationMs::new(1)),
            TimestampMs::ZERO
        );
        assert_eq!(
            TimestampMs::MAX.saturating_add(DurationMs::new(1)),
            TimestampMs::MAX
        );
        assert_eq!(
            TimestampMs::new(1).saturating_duration_since(TimestampMs::new(5)),
            DurationMs::ZERO
        );
    }

    #[test]
    fn serde_is_transparent() {
        let t = TimestampMs::new(42);
        assert_eq!(serde_json::to_string(&t).unwrap(), "42");
        let d: DurationMs = serde_json::from_str("7").unwrap();
        assert_eq!(d, DurationMs::new(7));
    }

    #[test]
    fn borsh_roundtrips() {
        let t = TimestampMs::new(123);
        let bytes = borsh::to_vec(&t).unwrap();
        assert_eq!(bytes, 123u64.to_le_bytes());
        assert_eq!(borsh::from_slice::<TimestampMs>(&bytes).unwrap(), t);
    }

    // Consensus blocks/commits (`CommitV1.timestamp_ms`) and the blockpool
    // control-state snapshot both persist the block timestamp via `bcs`; pin
    // that a `TimestampMs` field encodes byte-identically to the `u64` it
    // replaced so those on-disk encodings are unchanged.
    #[test]
    fn bcs_matches_u64() {
        for raw in [0u64, 1, 42, 0xdead_beef, u64::MAX] {
            assert_eq!(
                bcs::to_bytes(&TimestampMs::new(raw)).unwrap(),
                bcs::to_bytes(&raw).unwrap()
            );
            assert_eq!(
                bcs::to_bytes(&DurationMs::new(raw)).unwrap(),
                bcs::to_bytes(&raw).unwrap()
            );
            assert_eq!(
                bcs::from_bytes::<TimestampMs>(&bcs::to_bytes(&raw).unwrap()).unwrap(),
                TimestampMs::new(raw)
            );
        }
    }

    // Plugin/indexer payloads reach `bincode`; pin identity there too, both
    // standalone and under the big-endian fixint config used for ordered keys.
    #[test]
    fn bincode_matches_u64() {
        use bincode::Options;
        for raw in [0u64, 1, 42, u64::MAX] {
            assert_eq!(
                bincode::serialize(&TimestampMs::new(raw)).unwrap(),
                bincode::serialize(&raw).unwrap()
            );
        }
        let cfg = bincode::DefaultOptions::new()
            .with_big_endian()
            .with_fixint_encoding();
        assert_eq!(
            cfg.serialize(&TimestampMs::new(42)).unwrap(),
            cfg.serialize(&42u64).unwrap()
        );
    }
}