cross-runtime 0.1.0

One task, timer, and runtime surface across native Tokio and browser WebAssembly
Documentation
#[cfg(not(target_arch = "wasm32"))]
mod native {
    use std::future::Future;

    pub use tokio::task::{JoinError, JoinHandle};

    /// Spawn a `Send` task on the current Tokio runtime.
    pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
    where
        F: Future + Send + 'static,
        F::Output: Send + 'static,
    {
        tokio::spawn(future)
    }

    /// Spawn a local task on the current Tokio `LocalSet`.
    pub fn spawn_local<F>(future: F) -> JoinHandle<F::Output>
    where
        F: Future + 'static,
        F::Output: 'static,
    {
        tokio::task::spawn_local(future)
    }
}

#[cfg(target_arch = "wasm32")]
mod web {
    use futures_channel::oneshot;
    use futures_util::future::{AbortHandle, Abortable};
    use std::fmt;
    use std::future::Future;
    use std::pin::Pin;
    use std::sync::Arc;
    use std::sync::atomic::{AtomicBool, Ordering};
    use std::task::{Context, Poll};

    /// The task ended without producing its output.
    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    pub struct JoinError;

    impl JoinError {
        /// Whether the task was cancelled.
        #[must_use]
        pub const fn is_cancelled(&self) -> bool {
            true
        }
    }

    impl fmt::Display for JoinError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            f.write_str("task was cancelled")
        }
    }

    impl std::error::Error for JoinError {}

    /// A detached-on-drop task handle matching Tokio's lifecycle semantics.
    pub struct JoinHandle<T> {
        abort: AbortHandle,
        result: oneshot::Receiver<Result<T, JoinError>>,
        finished: Arc<AtomicBool>,
    }

    impl<T> JoinHandle<T> {
        /// Request cancellation without waiting for task cleanup.
        pub fn abort(&self) {
            self.abort.abort();
        }

        /// Whether the task has completed or observed cancellation.
        #[must_use]
        pub fn is_finished(&self) -> bool {
            self.finished.load(Ordering::Acquire)
        }
    }

    impl<T> Future for JoinHandle<T> {
        type Output = Result<T, JoinError>;

        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
            match Pin::new(&mut self.result).poll(cx) {
                Poll::Ready(Ok(result)) => Poll::Ready(result),
                Poll::Ready(Err(_)) => Poll::Ready(Err(JoinError)),
                Poll::Pending => Poll::Pending,
            }
        }
    }

    impl<T> fmt::Debug for JoinHandle<T> {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            f.debug_struct("JoinHandle")
                .field("finished", &self.is_finished())
                .finish()
        }
    }

    fn parts<T>() -> (
        futures_util::future::AbortRegistration,
        oneshot::Sender<Result<T, JoinError>>,
        JoinHandle<T>,
        Arc<AtomicBool>,
    ) {
        let (abort, registration) = AbortHandle::new_pair();
        let (send, result) = oneshot::channel();
        let finished = Arc::new(AtomicBool::new(false));
        let handle = JoinHandle {
            abort,
            result,
            finished: finished.clone(),
        };
        (registration, send, handle, finished)
    }

    /// Spawn a `Send` task which may follow its waker across browser workers.
    pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
    where
        F: Future + Send + 'static,
        F::Output: Send + 'static,
    {
        let (registration, send, handle, finished) = parts();
        web_task::spawn(async move {
            let result = Abortable::new(future, registration)
                .await
                .map_err(|_| JoinError);
            finished.store(true, Ordering::Release);
            let _ = send.send(result);
        })
        .detach();
        handle
    }

    /// Spawn a task pinned to the current browser thread.
    pub fn spawn_local<F>(future: F) -> JoinHandle<F::Output>
    where
        F: Future + 'static,
        F::Output: 'static,
    {
        let (registration, send, handle, finished) = parts();
        web_task::spawn_local(async move {
            let result = Abortable::new(future, registration)
                .await
                .map_err(|_| JoinError);
            finished.store(true, Ordering::Release);
            let _ = send.send(result);
        })
        .detach();
        handle
    }
}

#[cfg(not(target_arch = "wasm32"))]
pub use native::*;
#[cfg(target_arch = "wasm32")]
pub use web::*;