#[cfg(any(target_arch = "wasm32", test))]
mod chunks {
use std::time::Duration;
pub(super) fn step_ms(remaining: Duration) -> u32 {
let millis = remaining.as_nanos().div_ceil(1_000_000);
millis.min(i32::MAX as u128) as u32
}
pub(super) fn steps(duration: Duration) -> impl Iterator<Item = u32> {
let mut remaining = duration;
std::iter::from_fn(move || {
if remaining.is_zero() {
return None;
}
let step = step_ms(remaining);
remaining = remaining.saturating_sub(Duration::from_millis(u64::from(step)));
Some(step)
})
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::{step_ms, steps};
const LONGEST: u32 = i32::MAX as u32;
#[test]
fn a_step_is_the_remainder_rounded_up_to_a_millisecond() {
assert_eq!(step_ms(Duration::ZERO), 0);
assert_eq!(step_ms(Duration::from_nanos(1)), 1);
assert_eq!(step_ms(Duration::from_millis(1)), 1);
assert_eq!(step_ms(Duration::from_nanos(1_000_001)), 2);
assert_eq!(step_ms(Duration::from_secs(90)), 90_000);
}
#[test]
fn a_step_never_exceeds_what_set_timeout_accepts() {
assert_eq!(step_ms(Duration::from_millis(u64::from(LONGEST))), LONGEST);
assert_eq!(
step_ms(Duration::from_millis(u64::from(LONGEST) + 1)),
LONGEST
);
assert_eq!(step_ms(Duration::MAX), LONGEST);
}
#[test]
fn a_long_sleep_is_chained_in_whole_steps() {
let total = Duration::from_millis(2 * u64::from(LONGEST) + 5);
assert_eq!(steps(total).collect::<Vec<_>>(), [LONGEST, LONGEST, 5]);
}
#[test]
fn the_last_step_is_rounded_up_so_a_sleep_is_never_short() {
let total = Duration::from_nanos(u64::from(LONGEST) * 1_000_000 + 1);
assert_eq!(steps(total).collect::<Vec<_>>(), [LONGEST, 1]);
}
#[test]
fn no_time_is_no_step_and_a_sub_millisecond_sleep_is_one() {
assert_eq!(steps(Duration::ZERO).count(), 0);
assert_eq!(steps(Duration::from_micros(10)).collect::<Vec<_>>(), [1]);
}
#[test]
fn the_longest_sleep_starts_with_full_steps() {
let first: Vec<u32> = steps(Duration::MAX).take(2).collect();
assert_eq!(first, [LONGEST, LONGEST]);
}
}
}
#[cfg(target_arch = "wasm32")]
mod web {
use std::time::Duration;
use gloo_timers::future::TimeoutFuture;
use js_sys::Promise;
use send_wrapper::SendWrapper;
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::JsFuture;
use web_sys::MessageChannel;
use super::chunks::steps;
use crate::{BoxFuture, Instant, Runtime};
#[derive(Debug, Clone, Copy, Default)]
pub struct BrowserRuntime;
impl Runtime for BrowserRuntime {
fn spawn(&self, task: BoxFuture<()>) {
wasm_bindgen_futures::spawn_local(task);
}
fn sleep(&self, duration: Duration) -> BoxFuture<()> {
Box::pin(SendWrapper::new(async move {
for step in steps(duration) {
TimeoutFuture::new(step).await;
}
}))
}
fn yield_now(&self) -> BoxFuture<()> {
Box::pin(SendWrapper::new(next_macrotask()))
}
fn now(&self) -> Instant {
Instant::now()
}
}
async fn next_macrotask() {
let Ok(channel) = MessageChannel::new() else {
return TimeoutFuture::new(0).await;
};
let receiver = channel.port1();
let delivered = Promise::new(&mut |resolve, _reject| {
receiver.set_onmessage(Some(&resolve));
});
if channel.port2().post_message(&JsValue::UNDEFINED).is_err() {
receiver.set_onmessage(None);
return TimeoutFuture::new(0).await;
}
let _ = JsFuture::from(delivered).await;
receiver.set_onmessage(None);
receiver.close();
channel.port2().close();
}
}
#[cfg(target_arch = "wasm32")]
pub use web::BrowserRuntime;
#[cfg(all(test, not(target_arch = "wasm32")))]
mod native_default {
#[test]
fn the_host_default_stays_tokio() {
assert_eq!(format!("{:?}", crate::default_runtime()), "TokioRuntime");
}
}