Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 59 additions & 2 deletions wacore/src/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,16 @@ pub trait TimeProvider: Send + Sync + 'static {
fn now_millis(&self) -> i64;
}

/// Default wall-clock provider using `chrono`.
/// Default wall-clock provider using `chrono` (native targets only).
///
/// cfg-gated off `wasm32` for the same reason the monotonic clock is: there is
/// no backend for `chrono::Utc::now()` on `wasm32-unknown-unknown` (we don't pull
/// `wasmbind`), so it falls through to `SystemTime::now()`, which panics. The
/// wasm default is [`UnsetWasmTimeProvider`].
#[cfg(not(target_arch = "wasm32"))]
struct ChronoTimeProvider;

#[cfg(not(target_arch = "wasm32"))]
impl TimeProvider for ChronoTimeProvider {
// The single legitimate call to `chrono::Utc::now()`: this IS the default
// provider backing `wacore::time::now_utc()`. Everywhere else must go
Expand All @@ -40,6 +47,29 @@ impl TimeProvider for ChronoTimeProvider {
}
}

/// WASM default when no wall-clock provider is registered. Unlike the monotonic
/// clock, the wall clock has no internal source on `wasm32` (and we won't panic
/// like `chrono::Utc::now()` would), so this returns epoch (0) and warns once.
/// Embedders MUST call [`set_time_provider`] with a real provider (e.g. backed
/// by `Date.now()`) before the first timestamp.
#[cfg(target_arch = "wasm32")]
struct UnsetWasmTimeProvider;

#[cfg(target_arch = "wasm32")]
impl TimeProvider for UnsetWasmTimeProvider {
fn now_millis(&self) -> i64 {
use std::sync::atomic::{AtomicBool, Ordering};
static WARNED: AtomicBool = AtomicBool::new(false);
if !WARNED.swap(true, Ordering::Relaxed) {
log::warn!(
"wacore::time: no wall-clock provider set on wasm32; returning epoch. \
Call set_time_provider() before the first timestamp."
);
}
0
}
}

static TIME_PROVIDER: OnceLock<Box<dyn TimeProvider>> = OnceLock::new();

/// Set a custom wall-clock provider. Must be called before any time function
Expand All @@ -54,10 +84,20 @@ pub fn set_time_provider(provider: impl TimeProvider) -> Result<(), &'static str
#[inline]
pub fn now_millis() -> i64 {
TIME_PROVIDER
.get_or_init(|| Box::new(ChronoTimeProvider))
.get_or_init(default_time_provider)
Comment thread
jlucaso1 marked this conversation as resolved.
.now_millis()
}

#[cfg(not(target_arch = "wasm32"))]
fn default_time_provider() -> Box<dyn TimeProvider> {
Box::new(ChronoTimeProvider)
}

#[cfg(target_arch = "wasm32")]
fn default_time_provider() -> Box<dyn TimeProvider> {
Box::new(UnsetWasmTimeProvider)
}

/// Current time in seconds since Unix epoch.
#[inline]
pub fn now_secs() -> i64 {
Expand Down Expand Up @@ -273,3 +313,20 @@ impl std::ops::Sub<Instant> for Instant {
self.saturating_duration_since(rhs)
}
}

#[cfg(test)]
mod tests {
use super::*;

// The native default must yield a real wall-clock time, not the wasm fallback's
// epoch. Guards the cfg-split refactor that keeps wasm32 from panicking.
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn native_default_time_provider_returns_real_time() {
let ms = default_time_provider().now_millis();
assert!(
ms > 1_600_000_000_000,
"expected a post-2020 timestamp, got {ms}"
);
}
}
Loading