1use std::fmt::Debug;
4use std::future::Future;
5use std::time::Duration;
6
7mod read_at;
8
9pub use read_at::AsyncReadAt;
10
11#[cfg(not(target_arch = "wasm32"))]
13#[inline]
14pub async fn sleep(duration: Duration) {
15 tokio::time::sleep(duration).await;
16}
17
18#[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#[cfg(target_arch = "wasm32")]
42#[inline]
43pub async fn yield_now() {
44 use wasm_bindgen::JsCast as _;
45
46 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 #[error("Tokio error: {0}")]
80 TokioError(String),
81}
82
83#[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 #[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#[cfg(target_arch = "wasm32")]
152#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
153#[error("browser task was canceled")]
154pub struct TaskCancelled;
155
156#[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#[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() .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 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}