use futures::{future, Future, FutureExt as _};
#[cfg(not(web))]
pub trait MaybeSend: Send {}
#[cfg(not(web))]
impl<T: Send> MaybeSend for T {}
#[cfg(not(web))]
pub trait MaybeSync: Sync {}
#[cfg(not(web))]
impl<T: Sync> MaybeSync for T {}
#[cfg(web)]
pub trait MaybeSend {}
#[cfg(web)]
impl<T> MaybeSend for T {}
#[cfg(web)]
pub trait MaybeSync {}
#[cfg(web)]
impl<T> MaybeSync for T {}
pub async fn run_detached<F, R>(future: F) -> R
where
F: Future<Output = R> + MaybeSend + 'static,
R: MaybeSend + 'static,
{
#[cfg(not(web))]
{
join_detached(tokio::task::spawn(future)).await
}
#[cfg(web)]
{
let (tx, rx) = futures::channel::oneshot::channel();
wasm_bindgen_futures::spawn_local(async move {
if tx.send(future.await).is_err() {
tracing::debug!("run_detached: receiver dropped before result was delivered");
}
});
rx.await
.expect("spawned task dropped without sending its result")
}
}
#[cfg(not(web))]
async fn join_detached<R>(handle: tokio::task::JoinHandle<R>) -> R {
match handle.await {
Ok(output) => output,
Err(error) if error.is_panic() => std::panic::resume_unwind(error.into_panic()),
Err(error) => {
tracing::warn!(%error, "detached task cancelled; waiting for the shutdown that caused it");
future::pending().await
}
}
}
pub struct Task<R> {
abort_handle: future::AbortHandle,
output: future::RemoteHandle<Result<R, future::Aborted>>,
}
impl<R: 'static> Task<R> {
fn spawn_<F: Future<Output = R>, T>(
future: F,
spawn: impl FnOnce(future::Remote<future::Abortable<F>>) -> T,
) -> Self {
let (abortable_future, abort_handle) = future::abortable(future);
let (task, output) = abortable_future.remote_handle();
spawn(task);
Self {
abort_handle,
output,
}
}
#[cfg(not(web))]
pub fn spawn<F: Future<Output = R> + Send + 'static>(future: F) -> Self
where
R: Send,
{
Self::spawn_(future, tokio::task::spawn)
}
#[cfg(web)]
pub fn spawn<F: Future<Output = R> + 'static>(future: F) -> Self {
Self::spawn_(future, wasm_bindgen_futures::spawn_local)
}
pub fn ready(value: R) -> Self {
Self::spawn_(async { value }, |fut| {
fut.now_or_never().expect("the future is ready")
})
}
pub async fn cancel(self) {
self.abort_handle.abort();
self.output.await.ok();
}
pub fn forget(self) {
self.output.forget();
}
}
impl<R: 'static> std::future::IntoFuture for Task<R> {
type Output = R;
type IntoFuture = future::Map<
future::RemoteHandle<Result<R, future::Aborted>>,
fn(Result<R, future::Aborted>) -> R,
>;
fn into_future(self) -> Self::IntoFuture {
self.output
.map(|result| result.expect("we have the only AbortHandle"))
}
}
#[cfg(all(test, not(web)))]
mod tests {
use std::time::Duration;
use super::*;
#[tokio::test]
async fn a_cancelled_detached_task_waits_instead_of_panicking() {
let handle = tokio::task::spawn(future::pending::<()>());
handle.abort();
assert!(
tokio::time::timeout(Duration::from_millis(50), join_detached(handle))
.await
.is_err(),
"a cancelled task has no value to yield, so joining it must not resolve",
);
}
#[tokio::test]
async fn a_panicking_detached_task_still_propagates() {
let joined = tokio::task::spawn(async {
run_detached(async { panic!("the detached task failed") }).await
})
.await;
let error = joined.expect_err("the panic must not be swallowed");
let payload = error.into_panic();
assert_eq!(
payload.downcast_ref::<&str>().copied(),
Some("the detached task failed"),
"the original panic payload must survive the join",
);
}
}