Skip to main content

dynoxide/storage_backend/
clock.rs

1//! `Clock` capability used by the trait surface to abstract time access.
2//!
3//! Clock injection is scoped to the call sites that the
4//! [`StorageBackend`](super::StorageBackend) trait surfaces, namely the
5//! stream and TTL paths in [`crate::streams`] and [`crate::ttl`]. The other
6//! wall-clock call sites that compile on every target (the idempotency cache
7//! and the action-handler `created_at` stamps) read time through `web_time`,
8//! which sources the browser clock on wasm and `std::time` everywhere else.
9//! Snapshot epoch helpers stay on `std::time` because they sit inside
10//! native-only code the trait does not expose.
11
12use std::sync::Arc;
13use web_time::{SystemTime, UNIX_EPOCH};
14
15/// Provides wall-clock time to the trait's stream and TTL paths.
16///
17/// Implementations must be `Send + Sync` so a single shared `Arc<dyn Clock>`
18/// can sit inside [`crate::storage::Storage`].
19pub trait Clock: Send + Sync {
20    /// Whole seconds since the Unix epoch.
21    fn now_unix_secs(&self) -> u64;
22
23    /// Fractional seconds since the Unix epoch, retaining sub-second precision
24    /// for callers that need it (e.g., `cached_at` in import flows that route
25    /// through stream events).
26    fn now_unix_secs_f64(&self) -> f64;
27}
28
29/// Production clock backed by [`web_time::SystemTime`] (`std::time` on native).
30#[derive(Debug, Default, Clone, Copy)]
31pub struct SystemClock;
32
33impl SystemClock {
34    /// Return a shareable [`Arc<dyn Clock>`] handle to a `SystemClock`.
35    pub fn arc() -> Arc<dyn Clock> {
36        Arc::new(Self)
37    }
38}
39
40impl Clock for SystemClock {
41    fn now_unix_secs(&self) -> u64 {
42        SystemTime::now()
43            .duration_since(UNIX_EPOCH)
44            .unwrap_or_default()
45            .as_secs()
46    }
47
48    fn now_unix_secs_f64(&self) -> f64 {
49        let d = SystemTime::now()
50            .duration_since(UNIX_EPOCH)
51            .unwrap_or_default();
52        d.as_secs_f64()
53    }
54}
55
56/// Clock whose value is set explicitly. Intended for tests; carrying it in
57/// release builds costs only the size of the type itself, and it lets
58/// integration tests outside `src/` reach `ManualClock` without juggling
59/// feature flags.
60///
61/// Use [`ManualClock::new`] to start at a specific epoch, then [`ManualClock::set`]
62/// or [`ManualClock::tick`] to advance time deterministically inside tests.
63#[derive(Debug, Default, Clone)]
64pub struct ManualClock {
65    inner: Arc<std::sync::Mutex<f64>>,
66}
67
68impl ManualClock {
69    /// Construct a `ManualClock` pinned at `secs` epoch seconds.
70    pub fn new(secs: u64) -> Self {
71        Self {
72            inner: Arc::new(std::sync::Mutex::new(secs as f64)),
73        }
74    }
75
76    /// Set the current time to exactly `secs` epoch seconds.
77    pub fn set(&self, secs: u64) {
78        if let Ok(mut guard) = self.inner.lock() {
79            *guard = secs as f64;
80        }
81    }
82
83    /// Advance the clock by `delta`.
84    pub fn tick(&self, delta: std::time::Duration) {
85        if let Ok(mut guard) = self.inner.lock() {
86            *guard += delta.as_secs_f64();
87        }
88    }
89
90    /// Return a shareable `Arc<dyn Clock>` handle to this `ManualClock`.
91    /// The handle stays in sync with the original via the shared inner state.
92    pub fn arc(&self) -> Arc<dyn Clock> {
93        Arc::new(self.clone())
94    }
95}
96
97impl Clock for ManualClock {
98    fn now_unix_secs(&self) -> u64 {
99        self.inner.lock().map(|v| *v as u64).unwrap_or_default()
100    }
101
102    fn now_unix_secs_f64(&self) -> f64 {
103        self.inner.lock().map(|v| *v).unwrap_or_default()
104    }
105}