mod error;
mod post;
use std::{
pin::Pin,
task::{Context, Poll, ready},
};
use futures::{FutureExt as _, TryFutureExt as _, channel::oneshot, future};
use post::Postable;
pub use post::{AsJs, Post, PostExt};
use wasm_bindgen::prelude::{JsValue, wasm_bindgen};
use wasm_bindgen_futures::JsFuture;
use web_sys::{js_sys, wasm_bindgen};
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[wasm_bindgen(module = "/src/Client.js")]
extern "C" {
#[wasm_bindgen(js_name = "web_thread$Client")]
type Client;
#[wasm_bindgen(constructor, js_class = "web_thread$Client")]
fn new(module: JsValue, memory: JsValue) -> Client;
#[wasm_bindgen(js_class = "web_thread$Client", method)]
fn run(
this: &Client,
code: JsValue,
context: JsValue,
transfer: js_sys::Array,
) -> js_sys::Promise;
#[wasm_bindgen(js_class = "web_thread$Client", method)]
fn destroy(this: &Client);
}
pub struct Thread(Client);
pin_project_lite::pin_project! {
pub struct Task<T> {
result: future::Either<
future::MapErr<JsFuture, fn(JsValue) -> Error>,
future::Ready<Result<JsValue>>,
>,
_phantom: std::marker::PhantomData<T>,
}
}
impl<T: Post> Future for Task<T> {
type Output = Result<T>;
fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Ready(Ok(T::from_js(ready!(self.result.poll_unpin(context))?)?))
}
}
pin_project_lite::pin_project! {
pub struct SendTask<T> {
task: Task<()>,
receiver: oneshot::Receiver<T>,
}
}
impl<T: Send> Future for SendTask<T> {
type Output = Result<T>;
fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
ready!(self.task.poll_unpin(context))?;
Poll::Ready(Ok(
ready!(self.receiver.poll_unpin(context)).expect("task already completed successfully")
))
}
}
impl Thread {
#[must_use]
pub fn new() -> Self {
Self(Client::new(wasm_bindgen::module(), wasm_bindgen::memory()))
}
pub fn run<Context: Post, F: Future<Output: Post> + 'static>(
&self,
context: Context,
code: impl FnOnce(Context) -> F + Send + 'static,
) -> Task<F::Output> {
#![allow(clippy::needless_pass_by_value)]
let transfer = context.transferables();
Task {
_phantom: std::marker::PhantomData,
result: match context.to_js() {
Ok(context) => future::Either::Left(
JsFuture::from(self.0.run(Code::new(code).into(), context, transfer))
.map_err(Into::into),
),
Err(error) => future::Either::Right(future::ready(Err(error.into()))),
},
}
}
pub fn run_send<Context: Post, F: Future<Output: Send> + 'static>(
&self,
context: Context,
code: impl FnOnce(Context) -> F + Send + 'static,
) -> SendTask<F::Output> {
let (sender, receiver) = oneshot::channel();
SendTask {
task: self.run(context, |context| {
code(context).map(|outcome| {
let _ = sender.send(outcome);
})
}),
receiver,
}
}
}
impl Default for Thread {
fn default() -> Self {
Self::new()
}
}
impl Drop for Thread {
fn drop(&mut self) {
self.0.destroy();
}
}
pub type Error = error::Error;
type JsTask = std::pin::Pin<Box<dyn Future<Output = Result<Postable, JsValue>>>>;
type RemoteTask = Box<dyn FnOnce(JsValue) -> JsTask + Send>;
struct Code {
code: Option<Box<RemoteTask>>,
}
impl Code {
fn new<F: Future<Output: Post> + 'static, Context: Post>(
code: impl FnOnce(Context) -> F + Send + 'static,
) -> Self {
Self {
code: Some(Box::new(Box::new(|context| {
Box::pin(async move { Postable::new(code(Context::from_js(context)?).await) })
}))),
}
}
async fn call_once(mut self, context: JsValue) -> Result<Postable, JsValue> {
(*self.code.take().expect("code called more than once"))(context).await
}
unsafe fn from_js_value(js_value: &JsValue) -> Self {
#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
Self {
code: Some(unsafe { Box::from_raw(js_value.as_f64().unwrap() as u32 as _) }),
}
}
}
impl From<Code> for JsValue {
fn from(code: Code) -> Self {
(Box::into_raw(code.code.expect("serializing consumed code")) as u32).into()
}
}
#[doc(hidden)]
#[wasm_bindgen]
pub async unsafe fn __web_thread_worker_entry_point(
code: JsValue,
context: JsValue,
) -> Result<JsValue, JsValue> {
let code = unsafe { Code::from_js_value(&code) };
serde_wasm_bindgen::to_value(&code.call_once(context).await?).map_err(Into::into)
}
#[wasm_bindgen(module = "/src/worker.js")]
extern "C" {
fn _non_existent_function();
}