Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Makes the wasm32-wasip1/2 target a first-class citizen for Leptos's Server-Side #3063

Merged
merged 24 commits into from
Nov 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2df55b7
feat: WIP wasi integrations crate
raskyld Sep 24, 2024
59bebfa
feat(server_fn): add generic types
raskyld Sep 28, 2024
86812e5
chore(server_fn): generic types cleanup
raskyld Oct 5, 2024
98ec388
feat(integrations/wasi): make WASI a first-class citizen of leptos se…
raskyld Oct 5, 2024
3d76ba4
WIP: chore(any_spawner): make the futures::Executor runable
raskyld Oct 5, 2024
f0c0939
fix(server_fn): include `generic` in axum.
raskyld Oct 6, 2024
3978a99
chore(any_spawner): some clippy suggestions
raskyld Oct 6, 2024
071da3a
chore(leptos_wasi): fine-tune linter and clean-up
raskyld Oct 6, 2024
afb05b0
feat(leptos_wasi): better handling of server fn with form
raskyld Oct 6, 2024
cc6f5fc
chore: cargo fmt
raskyld Oct 6, 2024
31e114a
chore: remove custom clippy
raskyld Oct 11, 2024
4ecc65a
chore: use `wasi` crate
raskyld Oct 11, 2024
39a2552
chore: revert changes to any_spawner
raskyld Oct 19, 2024
b02e354
chore: simpler crate features + cleanup
raskyld Oct 19, 2024
ae0ff04
feat(any_spawner): add local custom executor
raskyld Oct 20, 2024
5db6233
feat(leptos_wasi): async runtime
raskyld Oct 20, 2024
c2254af
feat(leptos_wasi): error handling
raskyld Oct 23, 2024
345b762
chore: migrate integration off-tree
raskyld Oct 24, 2024
334d1da
chore(ci): fix formatting
raskyld Oct 24, 2024
c909802
chore: remove ref to leptos_wasi in Cargo.toml
raskyld Oct 24, 2024
a90c6b6
Merge branch 'leptos-rs:main' into feat/wasi-support
raskyld Oct 31, 2024
19d53f0
chore(ci): fix fmt
raskyld Oct 31, 2024
6507891
chore(ci): remove explicit into_inter()
raskyld Oct 31, 2024
5b51cd3
chore(ci): make generic mutually exclusive with other options
raskyld Nov 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions any_spawner/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand All @@ -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.
Expand Down
9 changes: 9 additions & 0 deletions leptos_macro/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -68,6 +69,14 @@ skip_feature_sets = [
"actix",
"axum",
],
[
"actix",
"generic",
],
[
"generic",
"axum",
],
]

[package.metadata.docs.rs]
Expand Down
14 changes: 14 additions & 0 deletions server_fn/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -138,6 +140,10 @@ skip_feature_sets = [
"actix",
"axum",
],
[
"actix",
"generic",
],
[
"browser",
"actix",
Expand All @@ -150,6 +156,10 @@ skip_feature_sets = [
"browser",
"reqwest",
],
[
"browser",
"generic",
],
[
"default-tls",
"rustls",
Expand All @@ -166,6 +176,10 @@ skip_feature_sets = [
"axum-no-default",
"browser",
],
[
"axum-no-default",
"generic",
],
[
"rkyv",
"json",
Expand Down
6 changes: 6 additions & 0 deletions server_fn/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
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
Expand Up @@ -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;
Expand Down
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
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions server_fn_macro/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ nightly = []
ssr = []
actix = []
axum = []
generic = []
reqwest = []

[package.metadata.docs.rs]
Expand Down
12 changes: 10 additions & 2 deletions server_fn_macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
Loading