| Author | Andy Green <andy@warmcat.com> 2026-10-03 09:24 UTC | | Committer | Andy Green <andy@warmcat.com> 2026-10-05 06:32 UTC | | Tree | 49b2f2183a707d5fe3b89325735df0fb7bfee2cb 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)
+ );
+ }
+}
|