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}