ppoppo-clock 0.1.0

Universal Clock + Timer port for ppoppo workspace — chat-as-infrastructure primitive
Documentation
use crate::{Clock, Timer, Tz, ZonedDateTime};
use futures::future::BoxFuture;
use time::{Date, Duration, Month, OffsetDateTime};

/// Real wall clock backed by the system clock.
pub struct WallClock;

impl Clock for WallClock {
    fn now_utc(&self) -> OffsetDateTime {
        OffsetDateTime::now_utc()
    }

    fn now_in(&self, tz: &Tz) -> ZonedDateTime {
        let utc = OffsetDateTime::now_utc();
        ZonedDateTime::new(utc, tz.clone())
    }

    fn today_in(&self, tz: &Tz) -> Date {
        let p = self.now_in(tz).local_parts();
        Month::try_from(p.month_1)
            .ok()
            .and_then(|m| Date::from_calendar_date(p.year, m, p.day).ok())
            .unwrap_or_else(|| OffsetDateTime::now_utc().date())
    }

    fn now_unix_millis(&self) -> i64 {
        (OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000) as i64
    }
}

/// Tokio-backed async timer.
pub struct TokioTimer;

impl Timer for TokioTimer {
    fn sleep(&self, dur: Duration) -> BoxFuture<'static, ()> {
        let std_dur = to_std_duration(dur);
        Box::pin(async move {
            tokio::time::sleep(std_dur).await;
        })
    }

    fn next_tick(&self) -> BoxFuture<'static, ()> {
        Box::pin(async {
            tokio::task::yield_now().await;
        })
    }
}

fn to_std_duration(dur: Duration) -> std::time::Duration {
    let nanos = dur.whole_nanoseconds();
    if nanos <= 0 {
        std::time::Duration::ZERO
    } else {
        std::time::Duration::from_nanos(nanos as u64)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::Clock as ClockTrait;
    use crate::Timer as TimerTrait;
    use std::sync::Arc;
    use std::time::Instant;

    fn assert_send_sync<T: Send + Sync>() {}

    #[test]
    fn wall_clock_and_tokio_timer_are_send_sync() {
        assert_send_sync::<WallClock>();
        assert_send_sync::<TokioTimer>();
    }

    #[test]
    fn now_utc_is_recent() {
        let before = OffsetDateTime::now_utc();
        let clock = WallClock;
        let t = clock.now_utc();
        let after = OffsetDateTime::now_utc();
        assert!(t >= before - Duration::seconds(1));
        assert!(t <= after + Duration::seconds(1));
    }

    #[test]
    fn now_in_preserves_tz() {
        let clock = WallClock;
        let tz = Tz::seoul();
        let zdt = clock.now_in(&tz);
        assert_eq!(zdt.tz(), &tz);
    }

    #[test]
    fn now_unix_millis_positive_and_recent() {
        let before_ms = (OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000) as i64;
        let clock = WallClock;
        let ms = clock.now_unix_millis();
        let after_ms = (OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000) as i64;
        assert!(ms > 0, "unix millis must be positive");
        assert!(ms >= before_ms - 100, "must be >= before sample");
        assert!(ms <= after_ms + 100, "must be <= after sample");
    }

    #[tokio::test]
    async fn tokio_timer_sleep_completes() {
        let timer = TokioTimer;
        let start = Instant::now();
        timer.sleep(Duration::milliseconds(50)).await;
        let elapsed = start.elapsed();
        assert!(elapsed.as_millis() >= 45, "sleep must take at least 45ms");
        assert!(elapsed.as_millis() < 500, "sleep must not take more than 500ms");
    }

    #[tokio::test]
    async fn tokio_timer_next_tick_completes_immediately() {
        let timer = TokioTimer;
        let start = Instant::now();
        timer.next_tick().await;
        assert!(start.elapsed().as_millis() < 100);
    }

    #[test]
    fn wall_clock_arc_dyn_dispatch() {
        let c: Arc<dyn ClockTrait> = Arc::new(WallClock);
        let _ = c.now_utc();
    }

    #[test]
    fn tokio_timer_arc_dyn_dispatch() {
        let t: Arc<dyn TimerTrait> = Arc::new(TokioTimer);
        let _ = t.sleep(Duration::ZERO);
    }
}