Project homepage Mailing List  Warmcat.com  API Docs  Github Mirror 
    npro  
 Modern all-safe Rust Network Protocol library supporting h1, h2, h3, ws, wt sans-IO and with socket IO + tls
git clone https://npro.rs/repo/npro
 
root / crates / npro-test / tests / ws_server_replay.rs
Author[]Andy Green <andy@warmcat.com> 2026-10-03 09:24 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-10-05 06:32 UTC
Tree49b2f2183a707d5fe3b89325735df0fb7bfee2cb   Raw Patch
 
npro-core: monotonic time as an input
npro-core: monotonic time as an input

C's sansIO half takes the service thread's time (lws_wsi_now(), set by an
embedder with lws_service_set_now()), and never reads a clock to decide
anything.  Here `now` is an argument of every entry point that can decide
by it, and the protocol crates read no clock at all.

Instant is a point on the IO side's monotonic clock, in microseconds as
C's lws_usec_t is, from an origin the IO side picks.  Intervals are
core::time::Duration.  The arithmetic is checked or saturating, never
wrapping:
- checked_add gives None past the clock's range, and truncates below a
  microsecond;
- saturating_add is for deadlines, which must never come out earlier than
  asked;
- saturating_duration_since is zero for an instant that is not later.

Wall time, which the h1 date header and cookies will need, comes with the
first thing that uses it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019kg5Eemy68ZaqDBcUJQG6J
diff --git a/crates/npro-core/src/lib.rs b/crates/npro-core/src/lib.rs index b6f33f5..d3cb55e 100644 --- a/crates/npro-core/src/lib.rs +++ b/crates/npro-core/src/lib.rs @@ -11,4 +11,5 @@ pub mod base64; pub mod random; pub mod sha1; +pub mod time; pub mod utf8; diff --git a/crates/npro-core/src/time.rs b/crates/npro-core/src/time.rs new file mode 100644 index 0000000..5b2e070 --- /dev/null +++ b/crates/npro-core/src/time.rs @@ -0,0 +1,99 @@ +//! Time, as an input. +//! +//! C's sansIO half asks the service thread's time (`lws_wsi_now()`), which +//! an embedder sets with `lws_service_set_now()`; it never reads a clock to +//! decide anything. Here the caller passes `now` into every entry point +//! that can decide by it, and asks the connection when it next needs to be +//! called. Nothing in the protocol crates reads a clock. +//! +//! An [`Instant`] is a point on the IO side's monotonic clock, in +//! microseconds as C's `lws_usec_t` is, from an origin the IO side chooses. +//! Intervals are [`core::time::Duration`]. + +use core::time::Duration; + +/// A point on the monotonic clock, in microseconds from an origin the IO +/// side chooses. Instants from different clocks must not be mixed. +/// +/// ``` +/// use core::time::Duration; +/// use npro_core::time::Instant; +/// +/// let t0 = Instant::from_micros(1_000_000_000); +/// let t1 = t0.checked_add(Duration::from_millis(5)).unwrap(); +/// assert_eq!(t1.as_micros(), 1_000_005_000); +/// assert_eq!(t1.saturating_duration_since(t0), Duration::from_millis(5)); +/// assert_eq!(t0.saturating_duration_since(t1), Duration::ZERO); +/// ``` +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Instant(u64); + +impl Instant { + /// The instant `us` microseconds after the clock's origin. + #[must_use] + pub const fn from_micros(us: u64) -> Self { + Self(us) + } + + /// Microseconds since the clock's origin. + #[must_use] + pub const fn as_micros(self) -> u64 { + self.0 + } + + /// The instant `d` later, or `None` past the clock's range. Below a + /// microsecond, `d` is truncated. + #[must_use] + pub fn checked_add(self, d: Duration) -> Option<Self> { + let us = u64::try_from(d.as_micros()).ok()?; + self.0.checked_add(us).map(Self) + } + + /// The instant `d` later, or the clock's last instant. For a deadline + /// that must never come out earlier than asked. + #[must_use] + pub fn saturating_add(self, d: Duration) -> Self { + self.checked_add(d).unwrap_or(Self(u64::MAX)) + } + + /// How long after `earlier` this is, or zero if it is not later. + #[must_use] + pub const fn saturating_duration_since(self, earlier: Self) -> Duration { + Duration::from_micros(self.0.saturating_sub(earlier.0)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn adding_stays_in_range() { + let end = Instant::from_micros(u64::MAX - 1); + assert_eq!(end.checked_add(Duration::from_micros(2)), None); + assert_eq!( + end.saturating_add(Duration::from_micros(2)), + Instant::from_micros(u64::MAX) + ); + assert_eq!(Instant::from_micros(0).checked_add(Duration::MAX), None); + } + + #[test] + fn sub_microsecond_is_truncated() { + let t = Instant::from_micros(10); + assert_eq!(t.checked_add(Duration::from_nanos(999)), Some(t)); + assert_eq!( + t.checked_add(Duration::from_nanos(1_500)), + Some(Instant::from_micros(11)) + ); + } + + #[test] + fn instants_order_as_their_micros() { + assert!(Instant::from_micros(1) < Instant::from_micros(2)); + assert_eq!( + Instant::from_micros(7).saturating_duration_since(Instant::from_micros(3)), + Duration::from_micros(4) + ); + } +}
Page fetched 0s ago, creation time: 2ms (vhost etag hits: 0%, cache hits: 0%)