Skip to main content

re_async/
lib.rs

1//! Async runtime abstractions for native and `WebAssembly` targets.
2
3use std::fmt::Debug;
4use std::future::Future;
5use std::time::Duration;
6
7mod read_at;
8
9pub use read_at::AsyncReadAt;
10
11/// Waits for at least `duration`.
12#[cfg(not(target_arch = "wasm32"))]
13#[inline]
14pub async fn sleep(duration: Duration) {
15    tokio::time::sleep(duration).await;
16}
17
18/// Waits for at least `duration` using the browser's timer queue.
19///
20/// Browser timers use signed 32-bit millisecond delays, so longer durations are clamped.
21#[cfg(target_arch = "wasm32")]
22pub async fn sleep(duration: Duration) {
23    let milliseconds = i32::try_from(duration.as_millis()).unwrap_or(i32::MAX);
24    spawn_local_with_result(async move {
25        let mut callback = |resolve: js_sys::Function, _reject: js_sys::Function| {
26            web_sys::window()
27                .expect("browser window should exist")
28                .set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, milliseconds)
29                .expect("browser timer should be created");
30        };
31
32        js_sys::Promise::new(&mut callback)
33            .await
34            .expect("browser timer should complete");
35    })
36    .await
37    .expect("browser timer task should not be canceled while it is awaited");
38}
39
40/// Yields to the browser event loop so other tasks can run.
41#[cfg(target_arch = "wasm32")]
42#[inline]
43pub async fn yield_now() {
44    use wasm_bindgen::JsCast as _;
45
46    // Use `scheduler.yield()`, if available. More information:
47    // https://developer.mozilla.org/docs/Web/API/Scheduler/yield
48    let global = js_sys::global();
49    if let Ok(scheduler) = js_sys::Reflect::get(&global, &"scheduler".into())
50        && let Ok(yield_fn) = js_sys::Reflect::get(&scheduler, &"yield".into())
51        && let Some(yield_fn) = yield_fn.dyn_ref::<js_sys::Function>()
52    {
53        let promise = yield_fn
54            .call0(&scheduler)
55            .expect("scheduler.yield should return a promise")
56            .dyn_into::<js_sys::Promise>()
57            .expect("scheduler.yield should return a promise");
58        promise.await.expect("scheduler.yield should complete");
59    } else {
60        sleep(Duration::ZERO).await;
61    }
62}
63
64#[cfg(not(target_arch = "wasm32"))]
65pub trait WasmNotSend: Send {}
66
67#[cfg(target_arch = "wasm32")]
68pub trait WasmNotSend {}
69
70#[cfg(not(target_arch = "wasm32"))]
71impl<T: Send> WasmNotSend for T {}
72
73#[cfg(target_arch = "wasm32")]
74impl<T> WasmNotSend for T {}
75
76#[derive(Debug, thiserror::Error)]
77pub enum AsyncRuntimeError {
78    /// Tokio returned an error.
79    #[error("Tokio error: {0}")]
80    TokioError(String),
81}
82
83/// A handle to an async executor supplied by the application.
84///
85/// On native targets this wraps a Tokio runtime handle.
86/// On `WebAssembly` it dispatches futures to the browser's local executor.
87#[derive(Clone)]
88pub struct AsyncRuntimeHandle {
89    #[cfg(not(target_arch = "wasm32"))]
90    tokio: tokio::runtime::Handle,
91}
92
93impl Debug for AsyncRuntimeHandle {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        f.debug_struct("AsyncRuntimeHandle").finish()
96    }
97}
98
99impl AsyncRuntimeHandle {
100    #[cfg(not(target_arch = "wasm32"))]
101    pub fn new_native(tokio: tokio::runtime::Handle) -> Self {
102        Self { tokio }
103    }
104
105    #[cfg(target_arch = "wasm32")]
106    pub fn new_web() -> Self {
107        Self {}
108    }
109
110    #[cfg(not(target_arch = "wasm32"))]
111    pub fn inner(&self) -> &tokio::runtime::Handle {
112        &self.tokio
113    }
114
115    /// Captures the current Tokio runtime on native or the browser executor on `WebAssembly`.
116    #[cfg_attr(target_arch = "wasm32", expect(clippy::unnecessary_wraps))]
117    pub fn from_current_tokio_runtime_or_wasmbindgen() -> Result<Self, AsyncRuntimeError> {
118        cfg_select! {
119            target_arch = "wasm32" => {
120                Ok(Self::new_web())
121            }
122            _ => {
123                Ok(Self::new_native(
124                    tokio::runtime::Handle::try_current()
125                        .map_err(|err| AsyncRuntimeError::TokioError(err.to_string()))?
126                        .clone(),
127                ))
128            }
129        }
130    }
131
132    #[cfg(target_arch = "wasm32")]
133    #[expect(clippy::unused_self)]
134    pub fn spawn_future<F>(&self, future: F)
135    where
136        F: Future<Output = ()> + WasmNotSend + 'static,
137    {
138        spawn_local(future);
139    }
140
141    #[cfg(not(target_arch = "wasm32"))]
142    pub fn spawn_future<F>(&self, future: F)
143    where
144        F: Future<Output = ()> + WasmNotSend + 'static,
145    {
146        self.tokio.spawn(future);
147    }
148}
149
150/// Indicates that a browser task was canceled before producing its result.
151#[cfg(target_arch = "wasm32")]
152#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
153#[error("browser task was canceled")]
154pub struct TaskCancelled;
155
156/// Spawns a future on the browser's local executor.
157#[cfg(target_arch = "wasm32")]
158#[inline]
159#[expect(
160    clippy::disallowed_methods,
161    reason = "this is the workspace's browser executor boundary"
162)]
163pub fn spawn_local(future: impl Future<Output = ()> + 'static) {
164    js_sys::futures::spawn_local(future);
165}
166
167/// Spawns a possibly non-`Send` browser future and returns a `Send` future for its result.
168///
169/// Dropping the returned future cancels the spawned future.
170#[cfg(target_arch = "wasm32")]
171pub fn spawn_local_with_result<F, T>(
172    future: F,
173) -> impl Future<Output = Result<T, TaskCancelled>> + Send + 'static
174where
175    F: Future<Output = T> + 'static,
176    T: Send + 'static,
177{
178    use futures::TryFutureExt as _;
179    use futures::future::{Either, select};
180    use futures::pin_mut;
181
182    let (mut sender, receiver) = futures::channel::oneshot::channel();
183
184    spawn_local(async move {
185        let cancellation = sender.cancellation();
186        pin_mut!(future, cancellation);
187
188        if let Either::Left((result, _)) = select(future, cancellation).await {
189            sender.send(result).ok();
190        }
191    });
192
193    receiver.map_err(|_err| TaskCancelled)
194}
195
196#[cfg(all(test, not(target_arch = "wasm32")))]
197mod native_tests {
198    #[test]
199    fn supplied_runtime_does_not_need_to_be_entered_when_spawning() {
200        let runtime = tokio::runtime::Builder::new_current_thread() // NOLINT: the test must supply a runtime to the handle under test
201            .build()
202            .unwrap();
203        let handle = super::AsyncRuntimeHandle::new_native(runtime.handle().clone());
204        let (sender, receiver) = tokio::sync::oneshot::channel();
205
206        handle.spawn_future(async move {
207            sender.send(42).ok();
208        });
209
210        assert_eq!(runtime.block_on(receiver), Ok(42));
211    }
212}
213
214#[cfg(all(test, target_arch = "wasm32"))]
215mod web_tests {
216    use futures::channel::oneshot;
217    use wasm_bindgen_test::wasm_bindgen_test;
218
219    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
220
221    #[wasm_bindgen_test]
222    async fn returns_result() {
223        assert_eq!(super::spawn_local_with_result(async { 42 }).await, Ok(42));
224    }
225
226    #[wasm_bindgen_test]
227    async fn sleeps() {
228        super::sleep(std::time::Duration::ZERO).await;
229    }
230
231    #[wasm_bindgen_test]
232    async fn yield_now_allows_other_tasks_to_run() {
233        let (other_task_tx, mut other_task_rx) = oneshot::channel();
234        let (result_tx, result_rx) = oneshot::channel();
235
236        // We defer this to later, so that we can first observe the second task.
237        super::spawn_local(async move {
238            super::yield_now().await;
239            result_tx.send(other_task_rx.try_recv()).ok();
240        });
241        super::spawn_local(async move {
242            other_task_tx.send(()).ok();
243        });
244
245        assert_eq!(result_rx.await, Ok(Ok(Some(()))));
246    }
247
248    #[wasm_bindgen_test]
249    async fn dropping_result_cancels_spawned_future() {
250        struct NotifyOnDrop(Option<oneshot::Sender<()>>);
251
252        impl Drop for NotifyOnDrop {
253            fn drop(&mut self) {
254                self.0
255                    .take()
256                    .expect("drop notification should be sent once")
257                    .send(())
258                    .ok();
259            }
260        }
261
262        let (started_tx, started_rx) = oneshot::channel();
263        let (cancelled_tx, cancelled_rx) = oneshot::channel();
264        let task = super::spawn_local_with_result(async move {
265            let _notify_on_drop = NotifyOnDrop(Some(cancelled_tx));
266            started_tx.send(()).ok();
267            futures::future::pending::<()>().await;
268        });
269
270        started_rx.await.expect("spawned future should start");
271        drop(task);
272        cancelled_rx
273            .await
274            .expect("dropping the result should cancel the spawned future");
275    }
276}