ppoppo-clock 3.0.0

Universal Clock + Timer port for ppoppo workspace — chat-as-infrastructure primitive
Documentation
// WASM arm: WasmClock + WasmTimer.
//
// WasmClock sources "now" from `js_sys::Date::now()` (f64 epoch millis) — the
// only clock readout the browser offers synchronously. All zone math happens
// in jiff against the bundled tzdb (identical to the native arm; RFC
// RFC_202607130309_jiff-migration D5 overturned the zero-bundle Intl design).
// The one remaining Intl call is `browser_local_tz()` — a *name* probe
// (`resolvedOptions().timeZone`), resolved against jiff's tzdb.
//
// WasmTimer awaits a `Promise` that `Window.setTimeout` resolves. `BoxFuture`
// carries no `Send` on wasm32, so the `JsFuture` is returned as it is.

use crate::{BoxFuture, Clock, Timer};
use jiff::Timestamp;
use jiff::tz::TimeZone;
use std::time::Duration;

/// The browser wall clock: `Date.now()`, resolved to a jiff [`Timestamp`].
#[derive(Debug)]
pub struct WasmClock;

impl Clock for WasmClock {
    fn now(&self) -> Timestamp {
        let ms = js_sys::Date::now(); // f64 millis since epoch
        Timestamp::from_millisecond(ms as i64).unwrap_or(Timestamp::UNIX_EPOCH)
    }
}

/// Read the browser's local IANA timezone name via
/// `Intl.DateTimeFormat.resolvedOptions()` and resolve it in jiff's tzdb.
///
/// Falls back to `TimeZone::UTC` if the browser returns an unrecognised name.
/// Call once at app startup and provide the result via Leptos context.
pub fn browser_local_tz() -> TimeZone {
    use js_sys::{Array, Intl, Object, Reflect};
    use wasm_bindgen::JsValue;

    let fmt = Intl::DateTimeFormat::new(&Array::new(), &Object::new());
    let resolved = fmt.resolved_options();
    Reflect::get(&resolved, &JsValue::from_str("timeZone"))
        .ok()
        .and_then(|v| v.as_string())
        .and_then(|s| TimeZone::get(&s).ok())
        .unwrap_or(TimeZone::UTC)
}

/// A timer over `Window.setTimeout`: each sleep awaits the `Promise` the
/// timeout resolves.
#[derive(Debug)]
pub struct WasmTimer;

impl Timer for WasmTimer {
    fn sleep(&self, dur: Duration) -> BoxFuture<'static, ()> {
        let ms = dur.as_millis().min(i32::MAX as u128) as i32;
        let promise = js_sys::Promise::new(&mut |resolve, _reject| {
            if let Some(win) = web_sys::window() {
                let _ = win.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms);
            }
        });
        Box::pin(async move {
            let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
        })
    }

    fn next_tick(&self) -> BoxFuture<'static, ()> {
        self.sleep(Duration::ZERO)
    }
}