Skip to main content

ppoppo_clock/
wasm.rs

1// WASM arm: WasmClock + WasmTimer.
2//
3// WasmClock sources "now" from `js_sys::Date::now()` (f64 epoch millis) — the
4// only clock readout the browser offers synchronously. All zone math happens
5// in jiff against the bundled tzdb (identical to the native arm; RFC
6// RFC_202607130309_jiff-migration D5 overturned the zero-bundle Intl design).
7// The one remaining Intl call is `browser_local_tz()` — a *name* probe
8// (`resolvedOptions().timeZone`), resolved against jiff's tzdb.
9//
10// WasmTimer uses `Window.setTimeout` wired through a `futures::channel::oneshot`
11// so that the returned `BoxFuture` is `Send` (the JsFuture itself is !Send, but
12// awaiting a oneshot::Receiver is Send).
13
14use crate::{Clock, Timer};
15use futures::channel::oneshot;
16use jiff::Timestamp;
17use jiff::tz::TimeZone;
18use std::pin::Pin;
19use std::time::Duration;
20use wasm_bindgen::JsCast;
21
22/// The browser wall clock: `Date.now()`, resolved to a jiff [`Timestamp`].
23#[derive(Debug)]
24pub struct WasmClock;
25
26impl Clock for WasmClock {
27    fn now(&self) -> Timestamp {
28        let ms = js_sys::Date::now(); // f64 millis since epoch
29        Timestamp::from_millisecond(ms as i64).unwrap_or(Timestamp::UNIX_EPOCH)
30    }
31}
32
33/// Read the browser's local IANA timezone name via
34/// `Intl.DateTimeFormat.resolvedOptions()` and resolve it in jiff's tzdb.
35///
36/// Falls back to `TimeZone::UTC` if the browser returns an unrecognised name.
37/// Call once at app startup and provide the result via Leptos context.
38pub fn browser_local_tz() -> TimeZone {
39    use js_sys::{Array, Intl, Object, Reflect};
40    use wasm_bindgen::JsValue;
41
42    let fmt = Intl::DateTimeFormat::new(&Array::new(), &Object::new());
43    let resolved = fmt.resolved_options();
44    Reflect::get(&resolved, &JsValue::from_str("timeZone"))
45        .ok()
46        .and_then(|v| v.as_string())
47        .and_then(|s| TimeZone::get(&s).ok())
48        .unwrap_or(TimeZone::UTC)
49}
50
51/// A timer over `Window.setTimeout`, bridged through a oneshot channel so
52/// the returned future is `Send`.
53#[derive(Debug)]
54pub struct WasmTimer;
55
56impl Timer for WasmTimer {
57    fn sleep(&self, dur: Duration) -> Pin<Box<dyn Future<Output = ()> + Send>> {
58        let ms = dur.as_millis().min(i32::MAX as u128) as i32;
59        let (tx, rx) = oneshot::channel::<()>();
60
61        // Wire setTimeout → oneshot sender. The closure captures `tx` (which is Send).
62        // `Closure::once` is the approved wasm-bindgen pattern for fire-once callbacks.
63        let closure = wasm_bindgen::closure::Closure::once(move || {
64            let _ = tx.send(());
65        });
66        if let Some(win) = web_sys::window() {
67            let _ = win.set_timeout_with_callback_and_timeout_and_arguments_0(
68                closure.as_ref().unchecked_ref(),
69                ms,
70            );
71        }
72        // Leak the closure so JS can call it after Rust drops ownership.
73        // Memory is reclaimed by JS GC once the callback fires.
74        closure.forget();
75
76        Box::pin(async move {
77            // rx is Send; this future is Send.
78            let _ = rx.await;
79        })
80    }
81
82    fn next_tick(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
83        self.sleep(Duration::ZERO)
84    }
85}