Skip to content

Commit

Permalink
Makes the wasm32-wasip1/2 target a first-class citizen for Leptos's…
Browse files Browse the repository at this point in the history
… Server-Side (#3063)

* feat: WIP wasi integrations crate

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* feat(server_fn): add generic types

This commit adds `From` implementations for the
`Req` and `Res` types using abstraction that are deemed
"platform-agnostic".

Indeed, both the `http` and `bytes` crates contains types
that allows us to represent HTTP Request and Response,
while being capable to target unconventional platforms
(they even have `no-std` support). This allows the
server_fn functions to target new platforms,
for example, the `wasm32-wasip*` targets.

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* chore(server_fn): generic types cleanup

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* feat(integrations/wasi): make WASI a first-class citizen of leptos server-side

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* WIP: chore(any_spawner): make the futures::Executor runable

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* fix(server_fn): include `generic` in axum.

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* chore(any_spawner): some clippy suggestions

I ran clippy in really annoying mode since I am still
learning Rust and I want to write clean idiomatic code.
I took suggestions that I thought made sense, if any
maintainers think those are *too much*, I can relax
those changes:

* Use `core` instead of `std` to ease migration to `no_std`
  (https://rust-lang.github.io/rust-clippy/master/index.html#/std_instead_of_core)
* Add documentation on exported types and statics
* Bring some types in, with `use`
* Add `#[non_exhaustive]` on types we are not sure we
  won't extend (https://rust-lang.github.io/rust-clippy/master/index.html#exhaustive_enums)
* Add `#[inline]` to help the compiler when doing
  cross-crate compilation and Link-Time optimization
  is not enabled. (https://rust-lang.github.io/rust-clippy/master/index.html#/missing_inline_in_public_items)
* Use generic types instead of anonymous `impl` params
  so callers can use the `::<>` turbofish syntax (https://rust-lang.github.io/rust-clippy/master/index.html#/impl_trait_in_params)

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* chore(leptos_wasi): fine-tune linter and clean-up

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* feat(leptos_wasi): better handling of server fn with form

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* chore: cargo fmt

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* chore: remove custom clippy

Remove clippy crate rules since it
seems to make tests fails in tests.

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* chore: use `wasi` crate

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* chore: revert changes to any_spawner

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* chore: simpler crate features + cleanup

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* feat(any_spawner): add local custom executor

This commit adds a single-thread "local"
custom executor, which is useful for environments
like `wasm32` targets.

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* feat(leptos_wasi): async runtime

This commit adds a single-threaded
async runtime for `wasm32-wasip*`
targets.

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* feat(leptos_wasi): error handling

This commit adds error types for the users
to implement better error handling.

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* chore: migrate integration off-tree

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* chore(ci): fix formatting

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* chore: remove ref to leptos_wasi in Cargo.toml

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* chore(ci): fix fmt

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* chore(ci): remove explicit into_inter()

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

* chore(ci): make generic mutually exclusive with other options

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>

---------

Signed-off-by: Enzo "raskyld" Nocera <enzo@nocera.eu>
raskyld authored Nov 2, 2024
1 parent 13f7387 commit 2ef1723
Showing 10 changed files with 260 additions and 5 deletions.
40 changes: 37 additions & 3 deletions any_spawner/src/lib.rs
Original file line number Diff line number Diff line change
@@ -291,9 +291,10 @@ impl Executor {
///
/// Returns `Err(_)` if an executor has already been set.
pub fn init_custom_executor(
custom_executor: impl CustomExecutor + 'static,
custom_executor: impl CustomExecutor + Send + Sync + 'static,
) -> Result<(), ExecutorError> {
static EXECUTOR: OnceLock<Box<dyn CustomExecutor>> = OnceLock::new();
static EXECUTOR: OnceLock<Box<dyn CustomExecutor + Send + Sync>> =
OnceLock::new();
EXECUTOR
.set(Box::new(custom_executor))
.map_err(|_| ExecutorError::AlreadySet)?;
@@ -311,13 +312,46 @@ impl Executor {
.map_err(|_| ExecutorError::AlreadySet)?;
Ok(())
}

/// Locally sets a custom executor as the executor used to spawn tasks
/// in the current thread.
///
/// Returns `Err(_)` if an executor has already been set.
pub fn init_local_custom_executor(
custom_executor: impl CustomExecutor + 'static,
) -> Result<(), ExecutorError> {
thread_local! {
static EXECUTOR: OnceLock<Box<dyn CustomExecutor>> = OnceLock::new();
}
EXECUTOR.with(|this| {
this.set(Box::new(custom_executor))
.map_err(|_| ExecutorError::AlreadySet)
})?;

SPAWN
.set(|fut| {
EXECUTOR.with(|this| this.get().unwrap().spawn(fut));
})
.map_err(|_| ExecutorError::AlreadySet)?;
SPAWN_LOCAL
.set(|fut| {
EXECUTOR.with(|this| this.get().unwrap().spawn_local(fut));
})
.map_err(|_| ExecutorError::AlreadySet)?;
POLL_LOCAL
.set(|| {
EXECUTOR.with(|this| this.get().unwrap().poll_local());
})
.map_err(|_| ExecutorError::AlreadySet)?;
Ok(())
}
}

/// A trait for custom executors.
/// Custom executors can be used to integrate with any executor that supports spawning futures.
///
/// All methods can be called recursively.
pub trait CustomExecutor: Send + Sync {
pub trait CustomExecutor {
/// Spawns a future, usually on a thread pool.
fn spawn(&self, fut: PinnedFuture<()>);
/// Spawns a local future. May require calling `poll_local` to make progress.
9 changes: 9 additions & 0 deletions leptos_macro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -48,6 +48,7 @@ experimental-islands = []
trace-component-props = []
actix = ["server_fn_macro/actix"]
axum = ["server_fn_macro/axum"]
generic = ["server_fn_macro/generic"]

[package.metadata.cargo-all-features]
denylist = ["nightly", "tracing", "trace-component-props"]
@@ -68,6 +69,14 @@ skip_feature_sets = [
"actix",
"axum",
],
[
"actix",
"generic",
],
[
"generic",
"axum",
],
]

[package.metadata.docs.rs]
14 changes: 14 additions & 0 deletions server_fn/Cargo.toml
Original file line number Diff line number Diff line change
@@ -80,6 +80,7 @@ pin-project-lite = "0.2.14"
default = ["json"]
axum-no-default = [
"ssr",
"generic",
"dep:axum",
"dep:hyper",
"dep:http-body-util",
@@ -110,6 +111,7 @@ default-tls = ["reqwest?/default-tls"]
rustls = ["reqwest?/rustls-tls"]
reqwest = ["dep:reqwest"]
ssr = ["inventory"]
generic = []

[package.metadata.docs.rs]
all-features = true
@@ -138,6 +140,10 @@ skip_feature_sets = [
"actix",
"axum",
],
[
"actix",
"generic",
],
[
"browser",
"actix",
@@ -150,6 +156,10 @@ skip_feature_sets = [
"browser",
"reqwest",
],
[
"browser",
"generic",
],
[
"default-tls",
"rustls",
@@ -166,6 +176,10 @@ skip_feature_sets = [
"axum-no-default",
"browser",
],
[
"axum-no-default",
"generic",
],
[
"rkyv",
"json",
6 changes: 6 additions & 0 deletions server_fn/src/lib.rs
Original file line number Diff line number Diff line change
@@ -120,6 +120,12 @@ pub use ::actix_web as actix_export;
#[cfg(feature = "axum-no-default")]
#[doc(hidden)]
pub use ::axum as axum_export;
#[cfg(feature = "generic")]
#[doc(hidden)]
pub use ::bytes as bytes_export;
#[cfg(feature = "generic")]
#[doc(hidden)]
pub use ::http as http_export;
use client::Client;
use codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes};
#[doc(hidden)]
74 changes: 74 additions & 0 deletions server_fn/src/request/generic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! This module uses platform-agnostic abstractions
//! allowing users to run server functions on a wide range of
//! platforms.
//!
//! The crates in use in this crate are:
//!
//! * `bytes`: platform-agnostic manipulation of bytes.
//! * `http`: low-dependency HTTP abstractions' *front-end*.
//!
//! # Users
//!
//! * `wasm32-wasip*` integration crate `leptos_wasi` is using this
//! crate under the hood.
use crate::request::Req;
use bytes::Bytes;
use futures::{
stream::{self, Stream},
StreamExt,
};
use http::Request;
use std::borrow::Cow;

impl<CustErr> Req<CustErr> for Request<Bytes>
where
CustErr: 'static,
{
async fn try_into_bytes(
self,
) -> Result<Bytes, crate::ServerFnError<CustErr>> {
Ok(self.into_body())
}

async fn try_into_string(
self,
) -> Result<String, crate::ServerFnError<CustErr>> {
String::from_utf8(self.into_body().into()).map_err(|err| {
crate::ServerFnError::Deserialization(err.to_string())
})
}

fn try_into_stream(
self,
) -> Result<
impl Stream<Item = Result<Bytes, crate::ServerFnError>> + Send + 'static,
crate::ServerFnError<CustErr>,
> {
Ok(stream::iter(self.into_body())
.ready_chunks(16)
.map(|chunk| Ok(Bytes::from(chunk))))
}

fn to_content_type(&self) -> Option<Cow<'_, str>> {
self.headers()
.get(http::header::CONTENT_TYPE)
.map(|val| String::from_utf8_lossy(val.as_bytes()))
}

fn accepts(&self) -> Option<Cow<'_, str>> {
self.headers()
.get(http::header::ACCEPT)
.map(|val| String::from_utf8_lossy(val.as_bytes()))
}

fn referer(&self) -> Option<Cow<'_, str>> {
self.headers()
.get(http::header::REFERER)
.map(|val| String::from_utf8_lossy(val.as_bytes()))
}

fn as_query(&self) -> Option<&str> {
self.uri().query()
}
}
2 changes: 2 additions & 0 deletions server_fn/src/request/mod.rs
Original file line number Diff line number Diff line change
@@ -12,6 +12,8 @@ pub mod axum;
/// Request types for the browser.
#[cfg(feature = "browser")]
pub mod browser;
#[cfg(feature = "generic")]
pub mod generic;
/// Request types for [`reqwest`].
#[cfg(feature = "reqwest")]
pub mod reqwest;
105 changes: 105 additions & 0 deletions server_fn/src/response/generic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//! This module uses platform-agnostic abstractions
//! allowing users to run server functions on a wide range of
//! platforms.
//!
//! The crates in use in this crate are:
//!
//! * `bytes`: platform-agnostic manipulation of bytes.
//! * `http`: low-dependency HTTP abstractions' *front-end*.
//!
//! # Users
//!
//! * `wasm32-wasip*` integration crate `leptos_wasi` is using this
//! crate under the hood.
use super::Res;
use crate::error::{
ServerFnError, ServerFnErrorErr, ServerFnErrorSerde, SERVER_FN_ERROR_HEADER,
};
use bytes::Bytes;
use futures::{Stream, TryStreamExt};
use http::{header, HeaderValue, Response, StatusCode};
use std::{
fmt::{Debug, Display},
pin::Pin,
str::FromStr,
};
use throw_error::Error;

/// The Body of a Response whose *execution model* can be
/// customised using the variants.
pub enum Body {
/// The response body will be written synchronously.
Sync(Bytes),

/// The response body will be written asynchronously,
/// this execution model is also known as
/// "streaming".
Async(Pin<Box<dyn Stream<Item = Result<Bytes, Error>> + Send + 'static>>),
}

impl From<String> for Body {
fn from(value: String) -> Self {
Body::Sync(Bytes::from(value))
}
}

impl<CustErr> Res<CustErr> for Response<Body>
where
CustErr: Send + Sync + Debug + FromStr + Display + 'static,
{
fn try_from_string(
content_type: &str,
data: String,
) -> Result<Self, ServerFnError<CustErr>> {
let builder = http::Response::builder();
builder
.status(200)
.header(http::header::CONTENT_TYPE, content_type)
.body(data.into())
.map_err(|e| ServerFnError::Response(e.to_string()))
}

fn try_from_bytes(
content_type: &str,
data: Bytes,
) -> Result<Self, ServerFnError<CustErr>> {
let builder = http::Response::builder();
builder
.status(200)
.header(http::header::CONTENT_TYPE, content_type)
.body(Body::Sync(data))
.map_err(|e| ServerFnError::Response(e.to_string()))
}

fn try_from_stream(
content_type: &str,
data: impl Stream<Item = Result<Bytes, ServerFnError<CustErr>>>
+ Send
+ 'static,
) -> Result<Self, ServerFnError<CustErr>> {
let builder = http::Response::builder();
builder
.status(200)
.header(http::header::CONTENT_TYPE, content_type)
.body(Body::Async(Box::pin(
data.map_err(ServerFnErrorErr::from).map_err(Error::from),
)))
.map_err(|e| ServerFnError::Response(e.to_string()))
}

fn error_response(path: &str, err: &ServerFnError<CustErr>) -> Self {
Response::builder()
.status(http::StatusCode::INTERNAL_SERVER_ERROR)
.header(SERVER_FN_ERROR_HEADER, path)
.body(err.ser().unwrap_or_else(|_| err.to_string()).into())
.unwrap()
}

fn redirect(&mut self, path: &str) {
if let Ok(path) = HeaderValue::from_str(path) {
self.headers_mut().insert(header::LOCATION, path);
*self.status_mut() = StatusCode::FOUND;
}
}
}
2 changes: 2 additions & 0 deletions server_fn/src/response/mod.rs
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@ pub mod actix;
/// Response types for the browser.
#[cfg(feature = "browser")]
pub mod browser;
#[cfg(feature = "generic")]
pub mod generic;
/// Response types for Axum.
#[cfg(feature = "axum-no-default")]
pub mod http;
1 change: 1 addition & 0 deletions server_fn_macro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ nightly = []
ssr = []
actix = []
axum = []
generic = []
reqwest = []

[package.metadata.docs.rs]
12 changes: 10 additions & 2 deletions server_fn_macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -527,12 +527,16 @@ pub fn server_macro_impl(
}
} else if cfg!(feature = "axum") {
quote! {
#server_fn_path::axum_export::http::Request<#server_fn_path::axum_export::body::Body>
#server_fn_path::http_export::Request<#server_fn_path::axum_export::body::Body>
}
} else if cfg!(feature = "actix") {
quote! {
#server_fn_path::request::actix::ActixRequest
}
} else if cfg!(feature = "generic") {
quote! {
#server_fn_path::http_export::Request<#server_fn_path::bytes_export::Bytes>
}
} else if let Some(req_ty) = req_ty {
req_ty.to_token_stream()
} else if let Some(req_ty) = preset_req {
@@ -551,12 +555,16 @@ pub fn server_macro_impl(
}
} else if cfg!(feature = "axum") {
quote! {
#server_fn_path::axum_export::http::Response<#server_fn_path::axum_export::body::Body>
#server_fn_path::http_export::Response<#server_fn_path::axum_export::body::Body>
}
} else if cfg!(feature = "actix") {
quote! {
#server_fn_path::response::actix::ActixResponse
}
} else if cfg!(feature = "generic") {
quote! {
#server_fn_path::http_export::Response<#server_fn_path::response::generic::Body>
}
} else if let Some(res_ty) = res_ty {
res_ty.to_token_stream()
} else if let Some(res_ty) = preset_res {

0 comments on commit 2ef1723

Please sign in to comment.