Skip to main content

swr_runtime_web/
lib.rs

1//! Browser implementation of the [`swr_core::Runtime`] trait: tasks run on the browser event loop via `spawn_local`, timers via
2//! `gloo-timers`, time via the Performance API (`web_time`), plus a shared,
3//! reference-counted focus/online event source forwarding DOM events to
4//! [`SwrClient::broadcast`].
5//!
6//! On non-wasm targets this crate compiles to an empty library.
7#![cfg(target_arch = "wasm32")]
8#![deny(missing_docs)]
9
10use std::cell::RefCell;
11
12use gloo_events::EventListener;
13use swr_core::{Instant, Runtime, RuntimeFuture, SwrClient, SwrEvent};
14
15/// [`Runtime`] backed by the browser event loop.
16///
17/// `sleep_until` is implemented over `setTimeout`, so deadlines further out
18/// than `u32::MAX` milliseconds (~49.7 days) are not supported.
19#[derive(Clone, Copy, Debug, Default)]
20pub struct WebRuntime;
21
22impl WebRuntime {
23    /// Create the runtime.
24    pub fn new() -> Self {
25        Self
26    }
27}
28
29impl Runtime for WebRuntime {
30    fn now(&self) -> Instant {
31        // web_time::Instant: Performance-API-backed on wasm (the std version
32        // would panic here).
33        Instant::now()
34    }
35
36    fn spawn(&self, fut: RuntimeFuture) {
37        wasm_bindgen_futures::spawn_local(fut);
38    }
39
40    fn sleep_until(&self, at: Instant) -> RuntimeFuture {
41        Box::pin(async move {
42            // The delay is computed at poll time, not scheduling time.
43            let delay = at.saturating_duration_since(Instant::now());
44            gloo_timers::future::sleep(delay).await;
45        })
46    }
47}
48
49/// RAII registration forwarding browser events to a client:
50/// window `focus` and `visibilitychange`-to-visible become
51/// [`SwrEvent::Focus`], window `online` becomes [`SwrEvent::Online`].
52///
53/// One shared set of DOM listeners is registered globally and
54/// reference-counted: the first attachment adds them, dropping the last
55/// [`WebEventSource`] removes them. Every attached client receives every
56/// event.
57#[must_use = "dropping the WebEventSource detaches the client from browser events"]
58pub struct WebEventSource {
59    id: u64,
60}
61
62impl WebEventSource {
63    /// Attach `client` to the shared browser event listeners.
64    ///
65    /// # Panics
66    ///
67    /// Panics outside a browser environment (no `window`/`document`), e.g.
68    /// under plain Node.
69    pub fn attach(client: &SwrClient) -> Self {
70        REGISTRY.with(|registry| registry.borrow_mut().attach(client.clone()))
71    }
72}
73
74impl Drop for WebEventSource {
75    fn drop(&mut self) {
76        REGISTRY.with(|registry| registry.borrow_mut().detach(self.id));
77    }
78}
79
80thread_local! {
81    static REGISTRY: RefCell<Registry> = RefCell::new(Registry::default());
82}
83
84#[derive(Default)]
85struct Registry {
86    next_id: u64,
87    clients: Vec<(u64, SwrClient)>,
88    /// The single set of DOM listeners; `EventListener` detaches on drop.
89    listeners: Option<Vec<EventListener>>,
90}
91
92impl Registry {
93    fn attach(&mut self, client: SwrClient) -> WebEventSource {
94        let id = self.next_id;
95        self.next_id += 1;
96        self.clients.push((id, client));
97        if self.listeners.is_none() {
98            self.listeners = Some(register_dom_listeners());
99        }
100        WebEventSource { id }
101    }
102
103    fn detach(&mut self, id: u64) {
104        self.clients.retain(|(client_id, _)| *client_id != id);
105        if self.clients.is_empty() {
106            self.listeners = None;
107        }
108    }
109}
110
111fn register_dom_listeners() -> Vec<EventListener> {
112    let window = web_sys::window().expect("browser environment: window missing");
113    let document = window
114        .document()
115        .expect("browser environment: document missing");
116    vec![
117        EventListener::new(&window, "focus", |_event| broadcast_all(SwrEvent::Focus)),
118        EventListener::new(&window, "online", |_event| broadcast_all(SwrEvent::Online)),
119        EventListener::new(&document, "visibilitychange", |_event| {
120            // Only the transition back to visible counts as a focus event.
121            if document_visible() {
122                broadcast_all(SwrEvent::Focus);
123            }
124        }),
125    ]
126}
127
128fn document_visible() -> bool {
129    web_sys::window()
130        .and_then(|window| window.document())
131        .is_some_and(|document| document.visibility_state() == web_sys::VisibilityState::Visible)
132}
133
134fn broadcast_all(ev: SwrEvent) {
135    // Snapshot the clients first: a broadcast may synchronously drop or
136    // attach sources, which would otherwise alias the RefCell borrow.
137    let clients: Vec<SwrClient> = REGISTRY.with(|registry| {
138        registry
139            .borrow()
140            .clients
141            .iter()
142            .map(|(_, client)| client.clone())
143            .collect()
144    });
145    for client in clients {
146        client.broadcast(ev);
147    }
148}