use std::future::Future;
use std::time::Duration;
#[cfg(all(
target_arch = "wasm32",
target_os = "unknown",
not(feature = "wasm_js")
))]
pub mod host_clock;
#[cfg(all(
target_arch = "wasm32",
target_os = "unknown",
not(feature = "wasm_js")
))]
pub use host_clock::{Instant, SystemTime, UNIX_EPOCH};
#[cfg(all(target_arch = "wasm32", target_os = "unknown", feature = "wasm_js"))]
pub use web_time::{Instant, SystemTime, UNIX_EPOCH};
#[cfg(all(target_arch = "wasm32", not(target_os = "unknown")))]
pub use std::time::{Instant, SystemTime, UNIX_EPOCH};
#[cfg(not(target_arch = "wasm32"))]
pub use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(not(target_arch = "wasm32"))]
pub use tokio::time::Instant;
pub async fn sleep(duration: Duration) {
#[cfg(all(
target_arch = "wasm32",
target_os = "unknown",
not(feature = "wasm_js")
))]
{
let deadline = Instant::now() + duration;
while Instant::now() < deadline {
std::hint::spin_loop();
}
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown", feature = "wasm_js"))]
send_wrapper::SendWrapper::new(gloo_timers::future::TimeoutFuture::new(duration_millis(
duration,
)))
.await;
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
tokio::time::sleep(duration).await;
}
#[derive(Debug)]
pub struct TimeoutElapsed;
pub async fn timeout<F: Future>(
duration: Duration,
future: F,
) -> Result<F::Output, TimeoutElapsed> {
#[cfg(all(
target_arch = "wasm32",
target_os = "unknown",
not(feature = "wasm_js")
))]
{
use std::pin::pin;
use std::task::Poll;
let deadline = Instant::now() + duration;
let mut future = pin!(future);
return std::future::poll_fn(move |cx| {
let result = future.as_mut().poll(cx);
if Instant::now() >= deadline {
Poll::Ready(Err(TimeoutElapsed))
} else {
result.map(Ok)
}
})
.await;
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown", feature = "wasm_js"))]
{
use futures_util::future::{Either, select};
let timer = send_wrapper::SendWrapper::new(gloo_timers::future::TimeoutFuture::new(
duration_millis(duration),
));
futures_util::pin_mut!(future, timer);
match select(future, timer).await {
Either::Left((output, _)) => Ok(output),
Either::Right(_) => Err(TimeoutElapsed),
}
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
tokio::time::timeout(duration, future)
.await
.map_err(|_| TimeoutElapsed)
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown", feature = "wasm_js"))]
fn duration_millis(duration: Duration) -> u32 {
(duration.as_secs_f64() * 1000.0)
.ceil()
.clamp(0.0, u32::MAX as f64) as u32
}
pub(crate) fn to_chrono_utc(t: SystemTime) -> chrono::DateTime<chrono::Utc> {
let epoch = || chrono::DateTime::from_timestamp(0, 0).expect("epoch is representable");
match t.duration_since(UNIX_EPOCH) {
Ok(dur) => chrono::DateTime::from_timestamp(dur.as_secs() as i64, dur.subsec_nanos())
.unwrap_or_else(epoch),
Err(e) => {
let dur = e.duration();
let (secs, nanos) = if dur.subsec_nanos() == 0 {
(-(dur.as_secs() as i64), 0)
} else {
(
-(dur.as_secs() as i64) - 1,
1_000_000_000 - dur.subsec_nanos(),
)
};
chrono::DateTime::from_timestamp(secs, nanos).unwrap_or_else(epoch)
}
}
}
pub(crate) fn now_utc() -> chrono::DateTime<chrono::Utc> {
to_chrono_utc(SystemTime::now())
}
pub(crate) fn from_chrono<Tz: chrono::TimeZone>(dt: chrono::DateTime<Tz>) -> SystemTime {
let utc = dt.with_timezone(&chrono::Utc);
let secs = utc.timestamp();
let nanos = utc.timestamp_subsec_nanos();
if secs >= 0 {
UNIX_EPOCH + std::time::Duration::new(secs as u64, nanos)
} else if nanos == 0 {
UNIX_EPOCH - std::time::Duration::new((-secs) as u64, 0)
} else {
UNIX_EPOCH - std::time::Duration::new((-secs - 1) as u64, 1_000_000_000 - nanos)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn now_utc_tracks_the_system_clock() {
let before = chrono::Utc::now();
let sampled = now_utc();
let after = chrono::Utc::now();
assert!(
sampled >= before && sampled <= after,
"now_utc() {sampled} outside [{before}, {after}]"
);
}
#[test]
fn now_utc_is_monotonic_across_calls() {
assert!(now_utc() <= now_utc());
}
#[test]
fn to_chrono_utc_epoch() {
let dt = to_chrono_utc(UNIX_EPOCH);
assert_eq!((dt.timestamp(), dt.timestamp_subsec_nanos()), (0, 0));
}
#[test]
fn to_chrono_utc_pre_epoch_whole_second() {
let t = UNIX_EPOCH - std::time::Duration::from_secs(2);
let dt = to_chrono_utc(t);
assert_eq!((dt.timestamp(), dt.timestamp_subsec_nanos()), (-2, 0));
}
#[test]
fn to_chrono_utc_pre_epoch_subsecond() {
let t = UNIX_EPOCH - std::time::Duration::from_millis(500);
let dt = to_chrono_utc(t);
assert_eq!(
(dt.timestamp(), dt.timestamp_subsec_nanos()),
(-1, 500_000_000)
);
assert_eq!(dt.to_rfc3339(), "1969-12-31T23:59:59.500+00:00");
}
#[test]
fn from_chrono_pre_epoch_subsecond() {
let dt = chrono::DateTime::from_timestamp(-1, 500_000_000).unwrap();
let t = from_chrono(dt);
assert_eq!(
t.duration_since(UNIX_EPOCH).unwrap_err().duration(),
std::time::Duration::from_millis(500)
);
}
#[test]
fn round_trip_pre_epoch_subsecond() {
let original = UNIX_EPOCH - std::time::Duration::from_millis(500);
let round_tripped = from_chrono(to_chrono_utc(original));
assert_eq!(round_tripped, original);
}
#[test]
fn round_trip_post_epoch_subsecond() {
let original = UNIX_EPOCH + std::time::Duration::from_millis(500);
let round_tripped = from_chrono(to_chrono_utc(original));
assert_eq!(round_tripped, original);
}
}
#[cfg(feature = "terminal")]
#[derive(Clone, Default)]
pub(crate) struct InputWaitClock {
inner: std::sync::Arc<std::sync::Mutex<InputWaitState>>,
}
#[cfg(feature = "terminal")]
#[derive(Default)]
struct InputWaitState {
paused_since: Option<Instant>,
total: Duration,
}
#[cfg(feature = "terminal")]
impl InputWaitClock {
fn lock(&self) -> std::sync::MutexGuard<'_, InputWaitState> {
self.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub(crate) fn pause(&self) -> InputWaitGuard {
let mut state = self.lock();
if state.paused_since.is_none() {
state.paused_since = Some(Instant::now());
}
InputWaitGuard {
clock: self.clone(),
}
}
fn excluded(&self) -> Duration {
let state = self.lock();
state.total
+ state
.paused_since
.map(|since| {
Instant::now()
.checked_duration_since(since)
.unwrap_or_default()
})
.unwrap_or_default()
}
}
#[cfg(feature = "terminal")]
pub(crate) struct InputWaitGuard {
clock: InputWaitClock,
}
#[cfg(feature = "terminal")]
impl Drop for InputWaitGuard {
fn drop(&mut self) {
let mut state = self.clock.lock();
if let Some(since) = state.paused_since.take() {
state.total += Instant::now()
.checked_duration_since(since)
.unwrap_or_default();
}
}
}
#[cfg(feature = "terminal")]
pub(crate) async fn timeout_excluding_input_wait<F: Future>(
duration: Duration,
clock: &InputWaitClock,
future: F,
) -> Result<F::Output, TimeoutElapsed> {
let start = Instant::now();
let excluded_at_start = clock.excluded();
let mut future = std::pin::pin!(future);
let mut remaining = duration;
loop {
if let Ok(output) = timeout(remaining, future.as_mut()).await {
return Ok(output);
}
let elapsed = Instant::now()
.checked_duration_since(start)
.unwrap_or_default();
let excluded = clock.excluded().saturating_sub(excluded_at_start);
let active = elapsed.saturating_sub(excluded);
remaining = duration.saturating_sub(active);
if remaining.is_zero() {
return Err(TimeoutElapsed);
}
}
}