multiple fullstack nits/cleanups before release (#4852)
* feat: `:arg` syntax for serverfn * add better decode/encode verify message * allow catch-all query args * add rebind * implement websocket frames for `Streaming` * fix lazy init * wip... serverfnstate * wip... * wip: serverfns require Send/Sync, let's fix that * wip... * small cleanup * fix hang by actually spawning the tokio task * clippy * fix parameter catchall binding * fix lychee * doc tests
This commit is contained in:
1 parent
43373f0be7
commit
5559e463c8
27 files changed
+1363
-1065
No files matched your search
@@ -38,5 +38,5 @@ jobs:
|
|||||||
- name: Run lychee
|
- name: Run lychee
|
||||||
uses: lycheeverse/lychee-action@v2
|
uses: lycheeverse/lychee-action@v2
|
||||||
with:
|
with:
|
||||||
args: --base . --config ./lychee.toml './**/*.md'
|
args: --config ./lychee.toml './**/*.md'
|
||||||
fail: true
|
fail: true
|
||||||
@@ -703,6 +703,11 @@ name = "flat_router"
|
|||||||
path = "examples/06-routing/flat_router.rs"
|
path = "examples/06-routing/flat_router.rs"
|
||||||
doc-scrape-examples = true
|
doc-scrape-examples = true
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "query_params"
|
||||||
|
path = "examples/07-fullstack/query_params.rs"
|
||||||
|
doc-scrape-examples = true
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "server_functions"
|
name = "server_functions"
|
||||||
path = "examples/07-fullstack/server_functions.rs"
|
path = "examples/07-fullstack/server_functions.rs"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ fn main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/api/{name}/?age")]
|
#[get("/api/:name/?age")]
|
||||||
async fn get_message(name: String, age: i32) -> Result<String> {
|
async fn get_message(name: String, age: i32) -> Result<String> {
|
||||||
Ok(format!("Hello {}, you are {} years old!", name, age))
|
Ok(format!("Hello {}, you are {} years old!", name, age))
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
//! An example showcasing query parameters in Dioxus Fullstack server functions.
|
||||||
|
//!
|
||||||
|
//! The query parameter syntax mostly follows axum, but with a few extra conveniences.
|
||||||
|
//! - can rename parameters in the function signature with `?age=age_in_years` where `age_in_years` is Rust variable name
|
||||||
|
//! - can absorb all query params with `?{object}` directly into a struct implementing `Deserialize`
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus::launch(|| {
|
||||||
|
let mut message = use_action(get_message);
|
||||||
|
let mut message_rebind = use_action(get_message_rebind);
|
||||||
|
let mut message_all = use_action(get_message_all);
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
h1 { "Server says: "}
|
||||||
|
div {
|
||||||
|
button { onclick: move |_| message.call(22), "Single" }
|
||||||
|
pre { "{message:?}"}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
button { onclick: move |_| message_rebind.call(25), "Rebind" }
|
||||||
|
pre { "{message_rebind:?}"}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
button { onclick: move |_| message_all.call(Params { age: 30, name: "world".into() }), "Bind all" }
|
||||||
|
pre { "{message_all:?}"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/api/message/?age")]
|
||||||
|
async fn get_message(age: i32) -> Result<String> {
|
||||||
|
Ok(format!("You are {} years old!", age))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/api/rebind/?age=age_in_years")]
|
||||||
|
async fn get_message_rebind(age_in_years: i32) -> Result<String> {
|
||||||
|
Ok(format!("You are {} years old!", age_in_years))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, serde::Serialize, Debug)]
|
||||||
|
struct Params {
|
||||||
|
age: i32,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/api/all/?{query}")]
|
||||||
|
async fn get_message_all(query: Params) -> Result<String> {
|
||||||
|
Ok(format!(
|
||||||
|
"Hello {}, you are {} years old!",
|
||||||
|
query.name, query.age
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -92,8 +92,8 @@
|
|||||||
//! take a `State<T>` extractor cannot be automatically added to the router since the dioxus router
|
//! take a `State<T>` extractor cannot be automatically added to the router since the dioxus router
|
||||||
//! type does not know how to construct the `T` type.
|
//! type does not know how to construct the `T` type.
|
||||||
//!
|
//!
|
||||||
//! These server functions will be registered once the `ServerFnState<T>` layer is added to the app with
|
//! These server functions will be registered once the `ServerState<T>` layer is added to the app with
|
||||||
//! `router = router.layer(ServerFnState::new(your_state))`.
|
//! `router = router.layer(ServerState::new(your_state))`.
|
||||||
//!
|
//!
|
||||||
//! ## Middleware
|
//! ## Middleware
|
||||||
//!
|
//!
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
//! This example shows how to use global state to maintain state between server functions.
|
//! This example shows how to use global state to maintain state between server functions.
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use axum_core::extract::{FromRef, FromRequest};
|
||||||
|
use dioxus::{
|
||||||
|
fullstack::{FullstackContext, extract::State},
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
use reqwest::header::HeaderMap;
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
use {
|
use {
|
||||||
@@ -77,7 +84,68 @@ type BroadcastExtension = axum::Extension<tokio::sync::broadcast::Sender<String>
|
|||||||
|
|
||||||
#[post("/api/broadcast", ext: BroadcastExtension)]
|
#[post("/api/broadcast", ext: BroadcastExtension)]
|
||||||
async fn broadcast_message() -> Result<()> {
|
async fn broadcast_message() -> Result<()> {
|
||||||
|
let rt = Rc::new("asdasd".to_string());
|
||||||
ext.send("New broadcast message".to_string())?;
|
ext.send("New broadcast message".to_string())?;
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||||
|
println!("rt: {}", rt);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Option 4:
|
||||||
|
|
||||||
|
You can use Axum's `State` extractor to provide custom application state to your server functions.
|
||||||
|
|
||||||
|
All ServerFunctions pull in `FullstackContext`, so you need to implement `FromRef<FullstackContext>` for your
|
||||||
|
custom state type. To add your state to your app, you can use `.register_server_functions()` on a router
|
||||||
|
for a given state type, which will automatically add your state into the `FullstackContext` used by your server functions.
|
||||||
|
|
||||||
|
There are two details to note here:
|
||||||
|
|
||||||
|
- You need to implement `FromRef<FullstackContext>` for your custom state type.
|
||||||
|
- Custom extractors need to implement `FromRequest<S>` where `S` is the state type that implements `FromRef<FullstackContext>`.
|
||||||
|
*/
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct MyAppState {
|
||||||
|
abc: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRef<FullstackContext> for MyAppState {
|
||||||
|
fn from_ref(state: &FullstackContext) -> Self {
|
||||||
|
state.extension::<MyAppState>().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CustomExtractor {
|
||||||
|
abc: i32,
|
||||||
|
headermap: HeaderMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> FromRequest<S> for CustomExtractor
|
||||||
|
where
|
||||||
|
MyAppState: FromRef<S>,
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = ();
|
||||||
|
|
||||||
|
async fn from_request(
|
||||||
|
_req: axum::extract::Request,
|
||||||
|
state: &S,
|
||||||
|
) -> std::result::Result<Self, Self::Rejection> {
|
||||||
|
let state = MyAppState::from_ref(state);
|
||||||
|
Ok(CustomExtractor {
|
||||||
|
abc: state.abc,
|
||||||
|
headermap: HeaderMap::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/api/stateful", state: State<MyAppState>, ex: CustomExtractor)]
|
||||||
|
async fn app_state() -> Result<()> {
|
||||||
|
println!("abc: {}", state.abc);
|
||||||
|
println!("state abc: {:?}", ex.abc);
|
||||||
|
println!("headermap: {:?}", ex.headermap);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +163,10 @@ fn main() {
|
|||||||
let router = dioxus::server::router(app)
|
let router = dioxus::server::router(app)
|
||||||
.layer(Extension(tokio::sync::broadcast::channel::<String>(16).0));
|
.layer(Extension(tokio::sync::broadcast::channel::<String>(16).0));
|
||||||
|
|
||||||
|
// To use our custom app state with `State<MyAppState>`, we need to register it
|
||||||
|
// as an extension since our `FromRef<FullstackContext>` implementation relies on it.
|
||||||
|
let router = router.layer(Extension(MyAppState { abc: 42 }));
|
||||||
|
|
||||||
Ok(router)
|
Ok(router)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,9 +211,7 @@ pub mod prelude {
|
|||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use dioxus_server::{
|
pub use dioxus_server::{self, serve, DioxusRouterExt, ServeConfig, ServerFunction};
|
||||||
self, serve, DioxusRouterExt, DioxusRouterFnExt, ServeConfig, ServerFunction,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "router")]
|
#[cfg(feature = "router")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "router")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "router")))]
|
||||||
|
|||||||
@@ -162,6 +162,20 @@ impl From<RequestError> for ServerFnError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<ServerFnError> for HttpError {
|
||||||
|
fn from(value: ServerFnError) -> Self {
|
||||||
|
let status = StatusCode::from_u16(match &value {
|
||||||
|
ServerFnError::ServerError { code, .. } => *code,
|
||||||
|
_ => 500,
|
||||||
|
})
|
||||||
|
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
HttpError {
|
||||||
|
status,
|
||||||
|
message: Some(value.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<HttpError> for ServerFnError {
|
impl From<HttpError> for ServerFnError {
|
||||||
fn from(value: HttpError) -> Self {
|
fn from(value: HttpError) -> Self {
|
||||||
ServerFnError::ServerError {
|
ServerFnError::ServerError {
|
||||||
|
|||||||
@@ -25,6 +25,3 @@ pub use error::*;
|
|||||||
|
|
||||||
pub mod httperror;
|
pub mod httperror;
|
||||||
pub use httperror::*;
|
pub use httperror::*;
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
|
||||||
pub struct DioxusServerState {}
|
|
||||||
@@ -9,10 +9,6 @@ use std::collections::HashSet;
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
tokio::task_local! {
|
|
||||||
static FULLSTACK_CONTEXT: FullstackContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The context provided by dioxus fullstack for server-side rendering.
|
/// The context provided by dioxus fullstack for server-side rendering.
|
||||||
///
|
///
|
||||||
/// This context will only be set on the server during the initial streaming response
|
/// This context will only be set on the server during the initial streaming response
|
||||||
@@ -21,10 +17,18 @@ tokio::task_local! {
|
|||||||
pub struct FullstackContext {
|
pub struct FullstackContext {
|
||||||
// We expose the lock for request headers directly so it needs to be in a separate lock
|
// We expose the lock for request headers directly so it needs to be in a separate lock
|
||||||
request_headers: Arc<RwLock<http::request::Parts>>,
|
request_headers: Arc<RwLock<http::request::Parts>>,
|
||||||
|
|
||||||
// The rest of the fields are only held internally, so we can group them together
|
// The rest of the fields are only held internally, so we can group them together
|
||||||
lock: Arc<RwLock<FullstackContextInner>>,
|
lock: Arc<RwLock<FullstackContextInner>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `FullstackContext` is always set when either
|
||||||
|
// 1. rendering the app via SSR
|
||||||
|
// 2. handling a server function request
|
||||||
|
tokio::task_local! {
|
||||||
|
static FULLSTACK_CONTEXT: FullstackContext;
|
||||||
|
}
|
||||||
|
|
||||||
pub struct FullstackContextInner {
|
pub struct FullstackContextInner {
|
||||||
current_status: StreamingStatus,
|
current_status: StreamingStatus,
|
||||||
current_status_subscribers: HashSet<ReactiveContext>,
|
current_status_subscribers: HashSet<ReactiveContext>,
|
||||||
@@ -78,6 +82,7 @@ impl FullstackContext {
|
|||||||
pub fn commit_initial_chunk(&mut self) {
|
pub fn commit_initial_chunk(&mut self) {
|
||||||
let mut lock = self.lock.write();
|
let mut lock = self.lock.write();
|
||||||
lock.current_status = StreamingStatus::InitialChunkCommitted;
|
lock.current_status = StreamingStatus::InitialChunkCommitted;
|
||||||
|
|
||||||
// The key type is mutable, but the hash is stable through mutations because we hash by pointer
|
// The key type is mutable, but the hash is stable through mutations because we hash by pointer
|
||||||
#[allow(clippy::mutable_key_type)]
|
#[allow(clippy::mutable_key_type)]
|
||||||
let subscribers = std::mem::take(&mut lock.current_status_subscribers);
|
let subscribers = std::mem::take(&mut lock.current_status_subscribers);
|
||||||
@@ -110,17 +115,23 @@ impl FullstackContext {
|
|||||||
FULLSTACK_CONTEXT.scope(self, fut).await
|
FULLSTACK_CONTEXT.scope(self, fut).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract an extension from the current request.
|
||||||
|
pub fn extension<T: Clone + Send + Sync + 'static>(&self) -> Option<T> {
|
||||||
|
let lock = self.request_headers.read();
|
||||||
|
lock.extensions.get::<T>().cloned()
|
||||||
|
}
|
||||||
|
|
||||||
/// Extract an axum extractor from the current request.
|
/// Extract an axum extractor from the current request.
|
||||||
///
|
///
|
||||||
/// The body of the request is always empty when using this method, as the body can only be consumed once in the server
|
/// The body of the request is always empty when using this method, as the body can only be consumed once in the server
|
||||||
/// function extractors.
|
/// function extractors.
|
||||||
pub async fn extract<T: FromRequest<(), M>, M>() -> Result<T, ServerFnError> {
|
pub async fn extract<T: FromRequest<Self, M>, M>() -> Result<T, ServerFnError> {
|
||||||
let this = Self::current()
|
let this = Self::current()
|
||||||
.ok_or_else(|| ServerFnError::new("No FullstackContext found".to_string()))?;
|
.ok_or_else(|| ServerFnError::new("No FullstackContext found".to_string()))?;
|
||||||
|
|
||||||
let parts = this.request_headers.read().clone();
|
let parts = this.request_headers.read().clone();
|
||||||
let request = axum_core::extract::Request::from_parts(parts, Default::default());
|
let request = axum_core::extract::Request::from_parts(parts, Default::default());
|
||||||
match T::from_request(request, &()).await {
|
match T::from_request(request, &this).await {
|
||||||
Ok(res) => Ok(res),
|
Ok(res) => Ok(res),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let resp = err.into_response();
|
let resp = err.into_response();
|
||||||
@@ -255,7 +266,7 @@ pub fn commit_initial_chunk() {
|
|||||||
|
|
||||||
/// Extract an axum extractor from the current request.
|
/// Extract an axum extractor from the current request.
|
||||||
#[deprecated(note = "Use FullstackContext::extract instead", since = "0.7.0")]
|
#[deprecated(note = "Use FullstackContext::extract instead", since = "0.7.0")]
|
||||||
pub fn extract<T: FromRequest<(), M>, M>(
|
pub fn extract<T: FromRequest<FullstackContext, M>, M>(
|
||||||
) -> impl std::future::Future<Output = Result<T, ServerFnError>> {
|
) -> impl std::future::Future<Output = Result<T, ServerFnError>> {
|
||||||
FullstackContext::extract::<T, M>()
|
FullstackContext::extract::<T, M>()
|
||||||
}
|
}
|
||||||
|
|||||||
+280
-431
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,6 @@
|
|||||||
//! A launch function that creates an axum router for the LaunchBuilder
|
//! A launch function that creates an axum router for the LaunchBuilder
|
||||||
|
|
||||||
use crate::{server::DioxusRouterExt, RenderHandleState, ServeConfig};
|
use crate::{server::DioxusRouterExt, FullstackState, ServeConfig};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
@@ -9,10 +9,9 @@ use axum::{
|
|||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use dioxus_cli_config::base_path;
|
use dioxus_cli_config::base_path;
|
||||||
use dioxus_core::Element;
|
use dioxus_core::{ComponentFunction, Element};
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
use dioxus_core::{RenderError, VNode};
|
use dioxus_devtools::{DevserverMsg, HotReloadMsg};
|
||||||
use dioxus_devtools::DevserverMsg;
|
|
||||||
use futures_util::{stream::FusedStream, StreamExt};
|
use futures_util::{stream::FusedStream, StreamExt};
|
||||||
use hyper::body::Incoming;
|
use hyper::body::Incoming;
|
||||||
use hyper_util::server::conn::auto::Builder as HyperBuilder;
|
use hyper_util::server::conn::auto::Builder as HyperBuilder;
|
||||||
@@ -20,14 +19,19 @@ use hyper_util::{
|
|||||||
rt::{TokioExecutor, TokioIo},
|
rt::{TokioExecutor, TokioIo},
|
||||||
service::TowerToHyperService,
|
service::TowerToHyperService,
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::{any::Any, net::SocketAddr, prelude::rust_2024::Future};
|
||||||
use std::{any::Any, collections::HashMap, net::SocketAddr, prelude::rust_2024::Future};
|
use std::{pin::Pin, sync::Arc};
|
||||||
use tokio::net::TcpStream;
|
use subsecond::HotFn;
|
||||||
use tokio_util::task::LocalPoolHandle;
|
use tokio_util::either::Either;
|
||||||
use tower::{Service, ServiceExt as _};
|
use tower::{Service, ServiceExt as _};
|
||||||
|
|
||||||
type ContextList = Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>;
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use {
|
||||||
|
dioxus_core::{RenderError, VNode},
|
||||||
|
tokio::net::TcpListener,
|
||||||
|
};
|
||||||
|
|
||||||
|
type ContextList = Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>;
|
||||||
type BaseComp = fn() -> Element;
|
type BaseComp = fn() -> Element;
|
||||||
|
|
||||||
/// Launch a fullstack app with the given root component.
|
/// Launch a fullstack app with the given root component.
|
||||||
@@ -52,10 +56,6 @@ async fn serve_server(
|
|||||||
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
|
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
|
||||||
platform_config: Vec<Box<dyn Any>>,
|
platform_config: Vec<Box<dyn Any>>,
|
||||||
) {
|
) {
|
||||||
let (devtools_tx, mut devtools_rx) = futures_channel::mpsc::unbounded();
|
|
||||||
|
|
||||||
dioxus_devtools::connect(move |msg| _ = devtools_tx.unbounded_send(msg));
|
|
||||||
|
|
||||||
let mut cfg = platform_config
|
let mut cfg = platform_config
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find_map(|cfg| cfg.downcast::<ServeConfig>().ok().map(|b| *b))
|
.find_map(|cfg| cfg.downcast::<ServeConfig>().ok().map(|b| *b))
|
||||||
@@ -67,236 +67,19 @@ async fn serve_server(
|
|||||||
cfg.context_providers.push(arced);
|
cfg.context_providers.push(arced);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the address the server should run on. If the CLI is running, the CLI proxies fullstack into the main address
|
let cb = move || {
|
||||||
// and we use the generated address the CLI gives us
|
let cfg = cfg.clone();
|
||||||
let address = dioxus_cli_config::fullstack_address_or_localhost();
|
Box::pin(async move {
|
||||||
|
Ok(apply_base_path(
|
||||||
// Create the router and register the server functions under the basepath.
|
|
||||||
let router = apply_base_path(
|
|
||||||
Router::new().serve_dioxus_application(cfg.clone(), original_root),
|
Router::new().serve_dioxus_application(cfg.clone(), original_root),
|
||||||
original_root,
|
original_root,
|
||||||
cfg.clone(),
|
cfg.clone(),
|
||||||
base_path().map(|s| s.to_string()),
|
base_path().map(|s| s.to_string()),
|
||||||
);
|
))
|
||||||
|
}) as _
|
||||||
let task_pool = LocalPoolHandle::new(
|
|
||||||
std::thread::available_parallelism()
|
|
||||||
.map(usize::from)
|
|
||||||
.unwrap_or(1),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut make_service = router.into_make_service();
|
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(address).await.unwrap();
|
|
||||||
|
|
||||||
enum Msg {
|
|
||||||
TcpStream(std::io::Result<(TcpStream, SocketAddr)>),
|
|
||||||
Devtools(DevserverMsg),
|
|
||||||
}
|
|
||||||
|
|
||||||
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(0);
|
|
||||||
let mut hr_idx = 0;
|
|
||||||
|
|
||||||
// Manually loop on accepting connections so we can also respond to devtools messages
|
|
||||||
loop {
|
|
||||||
let res = tokio::select! {
|
|
||||||
res = listener.accept() => Msg::TcpStream(res),
|
|
||||||
msg = devtools_rx.next(), if !devtools_rx.is_terminated() => {
|
|
||||||
if let Some(msg) = msg {
|
|
||||||
Msg::Devtools(msg)
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match res {
|
serve_router(cb, dioxus_cli_config::fullstack_address_or_localhost()).await;
|
||||||
Msg::TcpStream(Ok((tcp_stream, _remote_addr))) => {
|
|
||||||
let this_hr_index = hr_idx;
|
|
||||||
let mut make_service = make_service.clone();
|
|
||||||
let mut shutdown_rx = shutdown_rx.clone();
|
|
||||||
|
|
||||||
task_pool.spawn_pinned(move || async move {
|
|
||||||
let tcp_stream = TokioIo::new(tcp_stream);
|
|
||||||
|
|
||||||
std::future::poll_fn(|cx| {
|
|
||||||
<IntoMakeService<Router> as tower::Service<Request>>::poll_ready(
|
|
||||||
&mut make_service,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let tower_service = make_service
|
|
||||||
.call(())
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.map_request(|req: Request<Incoming>| req.map(Body::new));
|
|
||||||
|
|
||||||
// upgrades needed for websockets
|
|
||||||
let builder = HyperBuilder::new(TokioExecutor::new());
|
|
||||||
let connection = builder.serve_connection_with_upgrades(
|
|
||||||
tcp_stream,
|
|
||||||
TowerToHyperService::new(tower_service),
|
|
||||||
);
|
|
||||||
|
|
||||||
tokio::select! {
|
|
||||||
res = connection => {
|
|
||||||
if let Err(_err) = res {
|
|
||||||
// This error only appears when the client doesn't send a request and
|
|
||||||
// terminate the connection.
|
|
||||||
//
|
|
||||||
// If client sends one request then terminate connection whenever, it doesn't
|
|
||||||
// appear.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_res = shutdown_rx.wait_for(|i| *i == this_hr_index + 1) => {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Msg::TcpStream(Err(_)) => {}
|
|
||||||
// We need to delete our old router and build a new one
|
|
||||||
//
|
|
||||||
// one challenge is that the server functions are sitting in the dlopened lib and no longer
|
|
||||||
// accessible by us (the original process)
|
|
||||||
//
|
|
||||||
// We need to somehow get them out... ?
|
|
||||||
//
|
|
||||||
// for now we just support editing existing server functions
|
|
||||||
Msg::Devtools(devserver_msg) => {
|
|
||||||
match devserver_msg {
|
|
||||||
DevserverMsg::HotReload(hot_reload_msg) => {
|
|
||||||
if hot_reload_msg.for_build_id == Some(dioxus_cli_config::build_id()) {
|
|
||||||
if let Some(table) = hot_reload_msg.jump_table {
|
|
||||||
use crate::ServerFunction;
|
|
||||||
|
|
||||||
unsafe { dioxus_devtools::subsecond::apply_patch(table).unwrap() };
|
|
||||||
|
|
||||||
let mut new_router = Router::new().serve_static_assets();
|
|
||||||
let new_cfg = ServeConfig::new();
|
|
||||||
|
|
||||||
let server_fn_iter = ServerFunction::collect();
|
|
||||||
|
|
||||||
// de-duplicate iteratively by preferring the most recent (first, since it's linked)
|
|
||||||
let mut server_fn_map: HashMap<_, _> = HashMap::new();
|
|
||||||
for f in server_fn_iter.into_iter().rev() {
|
|
||||||
server_fn_map.insert(f.path(), f);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (_, fn_) in server_fn_map {
|
|
||||||
tracing::trace!(
|
|
||||||
"Registering server function: {:?} {:?}",
|
|
||||||
fn_.path(),
|
|
||||||
fn_.method()
|
|
||||||
);
|
|
||||||
new_router = fn_.register_server_fn_on_router(new_router);
|
|
||||||
}
|
|
||||||
|
|
||||||
let hot_root = subsecond::HotFn::current(original_root);
|
|
||||||
let new_root_addr = hot_root.ptr_address().0 as usize as *const ();
|
|
||||||
let new_root = unsafe {
|
|
||||||
std::mem::transmute::<*const (), fn() -> Element>(new_root_addr)
|
|
||||||
};
|
|
||||||
|
|
||||||
crate::document::reset_renderer();
|
|
||||||
|
|
||||||
let state = RenderHandleState::new(new_cfg.clone(), new_root);
|
|
||||||
|
|
||||||
let fallback_handler =
|
|
||||||
axum::routing::get(RenderHandleState::render_handler)
|
|
||||||
.with_state(state);
|
|
||||||
|
|
||||||
make_service = apply_base_path(
|
|
||||||
new_router.fallback(fallback_handler),
|
|
||||||
new_root,
|
|
||||||
new_cfg.clone(),
|
|
||||||
base_path().map(|s| s.to_string()),
|
|
||||||
)
|
|
||||||
.into_make_service();
|
|
||||||
|
|
||||||
shutdown_tx.send_modify(|i| {
|
|
||||||
*i += 1;
|
|
||||||
hr_idx += 1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DevserverMsg::FullReloadStart => {}
|
|
||||||
DevserverMsg::FullReloadFailed => {}
|
|
||||||
DevserverMsg::FullReloadCommand => {}
|
|
||||||
DevserverMsg::Shutdown => {}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_base_path(
|
|
||||||
mut router: Router,
|
|
||||||
root: fn() -> Element,
|
|
||||||
cfg: ServeConfig,
|
|
||||||
base_path: Option<String>,
|
|
||||||
) -> Router {
|
|
||||||
if let Some(base_path) = base_path {
|
|
||||||
let base_path = base_path.trim_matches('/');
|
|
||||||
|
|
||||||
// If there is a base path, nest the router under it and serve the root route manually
|
|
||||||
// Nesting a route in axum only serves /base_path or /base_path/ not both
|
|
||||||
router = Router::new().nest(&format!("/{base_path}/"), router).route(
|
|
||||||
&format!("/{base_path}"),
|
|
||||||
axum::routing::method_routing::get(
|
|
||||||
|state: State<RenderHandleState>, mut request: Request<Body>| async move {
|
|
||||||
// The root of the base path always looks like the root from dioxus fullstack
|
|
||||||
*request.uri_mut() = "/".parse().unwrap();
|
|
||||||
RenderHandleState::render_handler(state, request).await
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.with_state(RenderHandleState::new(cfg, root)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
router
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serve a fullstack dioxus application with a custom axum router.
|
|
||||||
///
|
|
||||||
/// This function sets up an async runtime, enables the default dioxus logger, runs the provided initializer,
|
|
||||||
/// and then starts an axum server with the returned router.
|
|
||||||
///
|
|
||||||
/// The axum router will be bound to the address specified by the `IP` and `PORT` environment variables,
|
|
||||||
/// defaulting to `127.0.0.1:8080` if not set.
|
|
||||||
pub fn serve<F>(mut serve_it: impl FnMut() -> F) -> !
|
|
||||||
where
|
|
||||||
F: Future<Output = Result<Router, anyhow::Error>>,
|
|
||||||
{
|
|
||||||
dioxus_logger::initialize_default();
|
|
||||||
|
|
||||||
let app_future = async move {
|
|
||||||
let router = serve_it().await.expect("Failed to create axum router");
|
|
||||||
let address = dioxus_cli_config::fullstack_address_or_localhost();
|
|
||||||
let listener = tokio::net::TcpListener::bind(address)
|
|
||||||
.await
|
|
||||||
.with_context(|| format!("Failed to bind app to given address: {address}"))
|
|
||||||
.unwrap();
|
|
||||||
tracing::trace!("Listening on {address}");
|
|
||||||
axum::serve::serve(listener, router)
|
|
||||||
.await
|
|
||||||
.expect("Failed to serve axum app");
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
|
||||||
handle.block_on(app_future);
|
|
||||||
} else {
|
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()
|
|
||||||
.unwrap()
|
|
||||||
.block_on(app_future);
|
|
||||||
}
|
|
||||||
|
|
||||||
unreachable!("Serving a fullstack app should never return")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a router that serves the dioxus application at the appropriate base path.
|
/// Create a router that serves the dioxus application at the appropriate base path.
|
||||||
@@ -319,3 +102,189 @@ pub fn router(app: fn() -> Element) -> Router {
|
|||||||
base_path().map(|s| s.to_string()),
|
base_path().map(|s| s.to_string()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serve a fullstack dioxus application with a custom axum router.
|
||||||
|
///
|
||||||
|
/// This function sets up an async runtime, enables the default dioxus logger, runs the provided initializer,
|
||||||
|
/// and then starts an axum server with the returned router.
|
||||||
|
///
|
||||||
|
/// The axum router will be bound to the address specified by the `IP` and `PORT` environment variables,
|
||||||
|
/// defaulting to `127.0.0.1:8080` if not set.
|
||||||
|
///
|
||||||
|
/// This function uses axum to block on serving the application, and will not return.
|
||||||
|
pub fn serve<F>(mut serve_it: impl FnMut() -> F) -> !
|
||||||
|
where
|
||||||
|
F: Future<Output = Result<Router, anyhow::Error>> + 'static,
|
||||||
|
{
|
||||||
|
let cb = move || Box::pin(serve_it()) as _;
|
||||||
|
|
||||||
|
block_on(
|
||||||
|
async move { serve_router(cb, dioxus_cli_config::fullstack_address_or_localhost()).await },
|
||||||
|
);
|
||||||
|
|
||||||
|
unreachable!("Serving a fullstack app should never return")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serve a fullstack dioxus application with a custom axum router.
|
||||||
|
///
|
||||||
|
/// This function enables the dioxus logger and then serves the axum server with hot-reloading support.
|
||||||
|
///
|
||||||
|
/// To enable hot-reloading of the router, the provided `serve_callback` should return a new `Router`
|
||||||
|
/// each time it is called.
|
||||||
|
pub async fn serve_router(
|
||||||
|
mut serve_callback: impl FnMut() -> Pin<Box<dyn Future<Output = Result<Router, anyhow::Error>>>>,
|
||||||
|
addr: SocketAddr,
|
||||||
|
) {
|
||||||
|
dioxus_logger::initialize_default();
|
||||||
|
|
||||||
|
let listener = TcpListener::bind(addr)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("Failed to bind to address {addr}"))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// If we're not in debug mode, just serve the app normally
|
||||||
|
if !cfg!(debug_assertions) {
|
||||||
|
axum::serve(listener, serve_callback().await.unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire up the devtools connection. The sender only sends messages in dev.
|
||||||
|
let (devtools_tx, mut devtools_rx) = futures_channel::mpsc::unbounded();
|
||||||
|
dioxus_devtools::connect(move |msg| _ = devtools_tx.unbounded_send(msg));
|
||||||
|
|
||||||
|
let mut hot_serve_callback = HotFn::current(serve_callback);
|
||||||
|
let mut make_service = hot_serve_callback
|
||||||
|
.call(())
|
||||||
|
.await
|
||||||
|
.map(|router| router.into_make_service())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (shutdown_tx, _) = tokio::sync::broadcast::channel(1);
|
||||||
|
let our_build_id = Some(dioxus_cli_config::build_id());
|
||||||
|
|
||||||
|
// Manually loop on accepting connections so we can also respond to devtools messages
|
||||||
|
loop {
|
||||||
|
let res = tokio::select! {
|
||||||
|
res = listener.accept() => Either::Left(res),
|
||||||
|
Some(msg) = devtools_rx.next(), if !devtools_rx.is_terminated() => Either::Right(msg),
|
||||||
|
else => continue
|
||||||
|
};
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Either::Left(Ok((tcp_stream, _remote_addr))) => {
|
||||||
|
let mut make_service = make_service.clone();
|
||||||
|
let mut shutdown_rx = shutdown_tx.subscribe();
|
||||||
|
|
||||||
|
tokio::task::spawn(async move {
|
||||||
|
let tcp_stream = TokioIo::new(tcp_stream);
|
||||||
|
|
||||||
|
std::future::poll_fn(|cx| {
|
||||||
|
<IntoMakeService<Router> as tower::Service<Request>>::poll_ready(
|
||||||
|
&mut make_service,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("Infallible");
|
||||||
|
|
||||||
|
// upgrades needed for websockets
|
||||||
|
let builder = HyperBuilder::new(TokioExecutor::new());
|
||||||
|
let connection = builder.serve_connection_with_upgrades(
|
||||||
|
tcp_stream,
|
||||||
|
TowerToHyperService::new(
|
||||||
|
make_service
|
||||||
|
.call(())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.map_request(|req: Request<Incoming>| req.map(Body::new)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
res = connection => {
|
||||||
|
if let Err(_err) = res {
|
||||||
|
// This error only appears when the client doesn't send a request and
|
||||||
|
// terminate the connection.
|
||||||
|
//
|
||||||
|
// If client sends one request then terminate connection whenever, it doesn't
|
||||||
|
// appear.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_res = shutdown_rx.recv() => {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle just hot-patches for now.
|
||||||
|
// We don't do RSX hot-reload since usually the client handles that once the page is loaded.
|
||||||
|
//
|
||||||
|
// todo(jon): I *believe* SSR is resilient to RSX changes, but we should verify that...
|
||||||
|
Either::Right(DevserverMsg::HotReload(HotReloadMsg {
|
||||||
|
jump_table: Some(table),
|
||||||
|
for_build_id,
|
||||||
|
..
|
||||||
|
})) if for_build_id == our_build_id => {
|
||||||
|
// Apply the hot-reload patch to the dioxus devtools first
|
||||||
|
unsafe { dioxus_devtools::subsecond::apply_patch(table).unwrap() };
|
||||||
|
|
||||||
|
// Now recreate the router
|
||||||
|
// We panic here because we don't want their app to continue in a maybe-corrupted state
|
||||||
|
make_service = hot_serve_callback
|
||||||
|
.call(())
|
||||||
|
.await
|
||||||
|
.expect("Failed to create new router after hot-patch!")
|
||||||
|
.into_make_service();
|
||||||
|
|
||||||
|
// Make sure to wipe out the renderer state so we don't have stale elements
|
||||||
|
crate::document::reset_renderer();
|
||||||
|
|
||||||
|
_ = shutdown_tx.send(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicitly don't handle RSX hot-reloads on the server
|
||||||
|
// The client will handle that once the page is loaded. If we handled it here,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block_on<T>(app_future: impl Future<Output = T>) {
|
||||||
|
if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
||||||
|
handle.block_on(app_future);
|
||||||
|
} else {
|
||||||
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(app_future);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_base_path<M: 'static>(
|
||||||
|
mut router: Router,
|
||||||
|
root: impl ComponentFunction<(), M> + Send + Sync,
|
||||||
|
cfg: ServeConfig,
|
||||||
|
base_path: Option<String>,
|
||||||
|
) -> Router {
|
||||||
|
if let Some(base_path) = base_path {
|
||||||
|
let base_path = base_path.trim_matches('/');
|
||||||
|
|
||||||
|
// If there is a base path, nest the router under it and serve the root route manually
|
||||||
|
// Nesting a route in axum only serves /base_path or /base_path/ not both
|
||||||
|
router = Router::new().nest(&format!("/{base_path}/"), router).route(
|
||||||
|
&format!("/{base_path}"),
|
||||||
|
axum::routing::method_routing::get(
|
||||||
|
|state: State<FullstackState>, mut request: Request<Body>| async move {
|
||||||
|
// The root of the base path always looks like the root from dioxus fullstack
|
||||||
|
*request.uri_mut() = "/".parse().unwrap();
|
||||||
|
FullstackState::render_handler(state, request).await
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_state(FullstackState::new(cfg, root)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
router
|
||||||
|
}
|
||||||
@@ -6,18 +6,177 @@ use axum::{
|
|||||||
body::Body,
|
body::Body,
|
||||||
extract::State,
|
extract::State,
|
||||||
http::{Request, StatusCode},
|
http::{Request, StatusCode},
|
||||||
response::IntoResponse,
|
response::{IntoResponse, Response},
|
||||||
response::Response,
|
|
||||||
routing::*,
|
routing::*,
|
||||||
};
|
};
|
||||||
use dioxus_core::{Element, VirtualDom};
|
use dioxus_core::{ComponentFunction, VirtualDom};
|
||||||
use http::header::*;
|
use http::header::*;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use tokio_util::task::LocalPoolHandle;
|
||||||
use tower::util::MapResponse;
|
use tower::util::MapResponse;
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
use tower_http::services::fs::ServeFileSystemResponseBody;
|
use tower_http::services::fs::ServeFileSystemResponseBody;
|
||||||
|
|
||||||
|
/// A extension trait with utilities for integrating Dioxus with your Axum router.
|
||||||
|
pub trait DioxusRouterExt {
|
||||||
|
/// Serves the static WASM for your Dioxus application (except the generated index.html).
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust, no_run
|
||||||
|
/// # #![allow(non_snake_case)]
|
||||||
|
/// # use dioxus::prelude::*;
|
||||||
|
/// use dioxus_server::DioxusRouterExt;
|
||||||
|
///
|
||||||
|
/// #[tokio::main]
|
||||||
|
/// async fn main() -> anyhow::Result<()> {
|
||||||
|
/// let addr = dioxus::cli_config::fullstack_address_or_localhost();
|
||||||
|
/// let router = axum::Router::new()
|
||||||
|
/// // Server side render the application, serve static assets, and register server functions
|
||||||
|
/// .serve_static_assets()
|
||||||
|
/// // Server render the application
|
||||||
|
/// // ...
|
||||||
|
/// .with_state(dioxus_server::FullstackState::headless());
|
||||||
|
/// let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||||
|
/// axum::serve(listener, router).await?;
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
fn serve_static_assets(self) -> Router<FullstackState>;
|
||||||
|
|
||||||
|
/// Serves the Dioxus application. This will serve a complete server side rendered application.
|
||||||
|
/// This will serve static assets, server render the application, register server functions, and integrate with hot reloading.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust, no_run
|
||||||
|
/// # #![allow(non_snake_case)]
|
||||||
|
/// # use dioxus::prelude::*;
|
||||||
|
/// use dioxus_server::{DioxusRouterExt, ServeConfig};
|
||||||
|
///
|
||||||
|
/// #[tokio::main]
|
||||||
|
/// async fn main() {
|
||||||
|
/// let addr = dioxus::cli_config::fullstack_address_or_localhost();
|
||||||
|
/// let router = axum::Router::new()
|
||||||
|
/// // Server side render the application, serve static assets, and register server functions
|
||||||
|
/// .serve_dioxus_application(ServeConfig::new(), app);
|
||||||
|
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
|
/// axum::serve(listener, router).await.unwrap();
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn app() -> Element {
|
||||||
|
/// rsx! { "Hello World" }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
fn serve_dioxus_application<M: 'static>(
|
||||||
|
self,
|
||||||
|
cfg: ServeConfig,
|
||||||
|
app: impl ComponentFunction<(), M> + Send + Sync,
|
||||||
|
) -> Router<()>;
|
||||||
|
|
||||||
|
/// Registers server functions with the default handler.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust, no_run
|
||||||
|
/// # use dioxus::prelude::*;
|
||||||
|
/// # use dioxus_server::DioxusRouterExt;
|
||||||
|
/// #[tokio::main]
|
||||||
|
/// async fn main() {
|
||||||
|
/// let addr = dioxus::cli_config::fullstack_address_or_localhost();
|
||||||
|
/// let router = axum::Router::new()
|
||||||
|
/// // Register server functions routes with the default handler
|
||||||
|
/// .register_server_functions()
|
||||||
|
/// .with_state(dioxus_server::FullstackState::headless());
|
||||||
|
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
|
/// axum::serve(listener, router).await.unwrap();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn register_server_functions(self) -> Router<FullstackState>;
|
||||||
|
|
||||||
|
/// Serves a Dioxus application without static assets.
|
||||||
|
/// Sets up server function routes and rendering endpoints only.
|
||||||
|
///
|
||||||
|
/// Useful for WebAssembly environments or when static assets
|
||||||
|
/// are served by another system.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust, no_run
|
||||||
|
/// # use dioxus::prelude::*;
|
||||||
|
/// # use dioxus_server::{DioxusRouterExt, ServeConfig};
|
||||||
|
/// #[tokio::main]
|
||||||
|
/// async fn main() {
|
||||||
|
/// let router = axum::Router::new()
|
||||||
|
/// .serve_api_application(ServeConfig::new(), app)
|
||||||
|
/// .into_make_service();
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn app() -> Element {
|
||||||
|
/// rsx! { "Hello World" }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
fn serve_api_application<M: 'static>(
|
||||||
|
self,
|
||||||
|
cfg: ServeConfig,
|
||||||
|
app: impl ComponentFunction<(), M> + Send + Sync,
|
||||||
|
) -> Router<()>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
impl DioxusRouterExt for Router<FullstackState> {
|
||||||
|
fn register_server_functions(mut self) -> Router<FullstackState> {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
|
||||||
|
for func in ServerFunction::collect() {
|
||||||
|
if seen.insert(format!("{} {}", func.method(), func.path())) {
|
||||||
|
tracing::info!(
|
||||||
|
"Registering server function: {} {}",
|
||||||
|
func.method(),
|
||||||
|
func.path()
|
||||||
|
);
|
||||||
|
|
||||||
|
self = self.route(func.path(), func.method_router())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serve_static_assets(self) -> Router<FullstackState> {
|
||||||
|
let Some(public_path) = public_path() else {
|
||||||
|
return self;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Serve all files in public folder except index.html
|
||||||
|
serve_dir_cached(self, &public_path, &public_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serve_api_application<M: 'static>(
|
||||||
|
self,
|
||||||
|
cfg: ServeConfig,
|
||||||
|
app: impl ComponentFunction<(), M> + Send + Sync,
|
||||||
|
) -> Router<()> {
|
||||||
|
self.register_server_functions()
|
||||||
|
.fallback(get(FullstackState::render_handler))
|
||||||
|
.with_state(FullstackState::new(cfg, app))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serve_dioxus_application<M: 'static>(
|
||||||
|
self,
|
||||||
|
cfg: ServeConfig,
|
||||||
|
app: impl ComponentFunction<(), M> + Send + Sync,
|
||||||
|
) -> Router<()> {
|
||||||
|
self.register_server_functions()
|
||||||
|
.serve_static_assets()
|
||||||
|
.fallback(get(FullstackState::render_handler))
|
||||||
|
.with_state(FullstackState::new(cfg, app))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// SSR renderer handler for Axum with added context injection.
|
/// SSR renderer handler for Axum with added context injection.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
@@ -27,7 +186,7 @@ use tower_http::services::fs::ServeFileSystemResponseBody;
|
|||||||
///
|
///
|
||||||
/// use axum::routing::get;
|
/// use axum::routing::get;
|
||||||
/// use dioxus::prelude::*;
|
/// use dioxus::prelude::*;
|
||||||
/// use dioxus_server::{RenderHandleState, render_handler, ServeConfig};
|
/// use dioxus_server::{FullstackState, render_handler, ServeConfig};
|
||||||
///
|
///
|
||||||
/// fn app() -> Element {
|
/// fn app() -> Element {
|
||||||
/// rsx! {
|
/// rsx! {
|
||||||
@@ -44,206 +203,89 @@ use tower_http::services::fs::ServeFileSystemResponseBody;
|
|||||||
/// // to inject the context into server functions running outside
|
/// // to inject the context into server functions running outside
|
||||||
/// // of an SSR render context.
|
/// // of an SSR render context.
|
||||||
/// .fallback(get(render_handler))
|
/// .fallback(get(render_handler))
|
||||||
/// .with_state(RenderHandleState::new(ServeConfig::new(), app));
|
/// .with_state(FullstackState::new(ServeConfig::new(), app));
|
||||||
///
|
///
|
||||||
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
/// axum::serve(listener, router).await.unwrap();
|
/// axum::serve(listener, router).await.unwrap();
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub async fn render_handler(
|
pub async fn render_handler(
|
||||||
State(state): State<RenderHandleState>,
|
State(state): State<FullstackState>,
|
||||||
request: Request<Body>,
|
request: Request<Body>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
RenderHandleState::render_handler(State(state), request).await
|
FullstackState::render_handler(State(state), request).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A extension trait with utilities for integrating Dioxus with your Axum router.
|
/// State used by [`FullstackState::render_handler`] to render a dioxus component with axum
|
||||||
pub trait DioxusRouterExt<S>: DioxusRouterFnExt<S> {
|
|
||||||
/// Serves the static WASM for your Dioxus application (except the generated index.html).
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust, no_run
|
|
||||||
/// # #![allow(non_snake_case)]
|
|
||||||
/// # use dioxus::prelude::*;
|
|
||||||
/// use dioxus_server::{DioxusRouterExt, DioxusRouterFnExt};
|
|
||||||
///
|
|
||||||
/// #[tokio::main]
|
|
||||||
/// async fn main() -> anyhow::Result<()> {
|
|
||||||
/// let addr = dioxus::cli_config::fullstack_address_or_localhost();
|
|
||||||
/// let router = axum::Router::new()
|
|
||||||
/// // Server side render the application, serve static assets, and register server functions
|
|
||||||
/// .serve_static_assets()
|
|
||||||
/// // Server render the application
|
|
||||||
/// // ...
|
|
||||||
/// .into_make_service();
|
|
||||||
/// let listener = tokio::net::TcpListener::bind(addr).await?;
|
|
||||||
/// axum::serve(listener, router).await?;
|
|
||||||
/// Ok(())
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
fn serve_static_assets(self) -> Self
|
|
||||||
where
|
|
||||||
Self: Sized;
|
|
||||||
|
|
||||||
/// Serves the Dioxus application. This will serve a complete server side rendered application.
|
|
||||||
/// This will serve static assets, server render the application, register server functions, and integrate with hot reloading.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust, no_run
|
|
||||||
/// # #![allow(non_snake_case)]
|
|
||||||
/// # use dioxus::prelude::*;
|
|
||||||
/// use dioxus_server::{DioxusRouterExt, DioxusRouterFnExt, ServeConfig};
|
|
||||||
///
|
|
||||||
/// #[tokio::main]
|
|
||||||
/// async fn main() {
|
|
||||||
/// let addr = dioxus::cli_config::fullstack_address_or_localhost();
|
|
||||||
/// let router = axum::Router::new()
|
|
||||||
/// // Server side render the application, serve static assets, and register server functions
|
|
||||||
/// .serve_dioxus_application(ServeConfig::new(), app)
|
|
||||||
/// .into_make_service();
|
|
||||||
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
|
||||||
/// axum::serve(listener, router).await.unwrap();
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// fn app() -> Element {
|
|
||||||
/// rsx! { "Hello World" }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
fn serve_dioxus_application(self, cfg: ServeConfig, app: fn() -> Element) -> Self
|
|
||||||
where
|
|
||||||
Self: Sized;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
impl<S> DioxusRouterExt<S> for Router<S>
|
|
||||||
where
|
|
||||||
S: Send + Sync + Clone + 'static,
|
|
||||||
{
|
|
||||||
fn serve_static_assets(self) -> Self {
|
|
||||||
let Some(public_path) = public_path() else {
|
|
||||||
return self;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Serve all files in public folder except index.html
|
|
||||||
serve_dir_cached(self, &public_path, &public_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serve_dioxus_application(self, cfg: ServeConfig, app: fn() -> Element) -> Self {
|
|
||||||
self.register_server_functions()
|
|
||||||
.serve_static_assets()
|
|
||||||
.fallback(
|
|
||||||
get(RenderHandleState::render_handler).with_state(RenderHandleState::new(cfg, app)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A extension trait with server function utilities for integrating Dioxus with your Axum router.
|
|
||||||
pub trait DioxusRouterFnExt<S> {
|
|
||||||
/// Registers server functions with the default handler.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust, no_run
|
|
||||||
/// # use dioxus::prelude::*;
|
|
||||||
/// # use dioxus_server::DioxusRouterFnExt;
|
|
||||||
/// #[tokio::main]
|
|
||||||
/// async fn main() {
|
|
||||||
/// let addr = dioxus::cli_config::fullstack_address_or_localhost();
|
|
||||||
/// let router = axum::Router::new()
|
|
||||||
/// // Register server functions routes with the default handler
|
|
||||||
/// .register_server_functions()
|
|
||||||
/// .into_make_service();
|
|
||||||
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
|
||||||
/// axum::serve(listener, router).await.unwrap();
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn register_server_functions(self) -> Self
|
|
||||||
where
|
|
||||||
Self: Sized;
|
|
||||||
|
|
||||||
/// Serves a Dioxus application without static assets.
|
|
||||||
/// Sets up server function routes and rendering endpoints only.
|
|
||||||
///
|
|
||||||
/// Useful for WebAssembly environments or when static assets
|
|
||||||
/// are served by another system.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust, no_run
|
|
||||||
/// # use dioxus::prelude::*;
|
|
||||||
/// # use dioxus_server::{DioxusRouterFnExt, ServeConfig};
|
|
||||||
/// #[tokio::main]
|
|
||||||
/// async fn main() {
|
|
||||||
/// let router = axum::Router::new()
|
|
||||||
/// .serve_api_application(ServeConfig::new(), app)
|
|
||||||
/// .into_make_service();
|
|
||||||
/// // ...
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// fn app() -> Element {
|
|
||||||
/// rsx! { "Hello World" }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
fn serve_api_application(self, cfg: ServeConfig, app: fn() -> Element) -> Self
|
|
||||||
where
|
|
||||||
Self: Sized;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: Send + Sync + Clone + 'static> DioxusRouterFnExt<S> for Router<S> {
|
|
||||||
fn register_server_functions(mut self) -> Self {
|
|
||||||
for func in ServerFunction::collect() {
|
|
||||||
tracing::info!(
|
|
||||||
"Registering server function: {} {}",
|
|
||||||
func.method(),
|
|
||||||
func.path()
|
|
||||||
);
|
|
||||||
|
|
||||||
self = func.register_server_fn_on_router(self);
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serve_api_application(self, cfg: ServeConfig, app: fn() -> Element) -> Self
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
self.register_server_functions().fallback(
|
|
||||||
get(RenderHandleState::render_handler).with_state(RenderHandleState::new(cfg, app)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// State used by [`RenderHandleState::render_handler`] to render a dioxus component with axum
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RenderHandleState {
|
pub struct FullstackState {
|
||||||
config: ServeConfig,
|
config: ServeConfig,
|
||||||
build_virtual_dom: Arc<dyn Fn() -> VirtualDom + Send + Sync>,
|
build_virtual_dom: Arc<dyn Fn() -> VirtualDom + Send + Sync>,
|
||||||
renderers: Arc<SsrRendererPool>,
|
renderers: Arc<SsrRendererPool>,
|
||||||
|
pub(crate) rt: LocalPoolHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderHandleState {
|
impl FullstackState {
|
||||||
/// Create a new [`RenderHandleState`]
|
/// Create a headless [`FullstackState`] without a root component.
|
||||||
pub fn new(config: ServeConfig, root: fn() -> Element) -> Self {
|
///
|
||||||
|
/// This won't render pages, but can still be used to register server functions and serve static assets.
|
||||||
|
pub fn headless() -> Self {
|
||||||
|
let rt = LocalPoolHandle::new(
|
||||||
|
std::thread::available_parallelism()
|
||||||
|
.map(usize::from)
|
||||||
|
.unwrap_or(1),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
renderers: Arc::new(SsrRendererPool::new(4, None)),
|
||||||
|
build_virtual_dom: Arc::new(|| {
|
||||||
|
panic!("No root component provided for headless FullstackState")
|
||||||
|
}),
|
||||||
|
config: ServeConfig::new(),
|
||||||
|
rt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`FullstackState`]
|
||||||
|
pub fn new<M: 'static>(
|
||||||
|
config: ServeConfig,
|
||||||
|
root: impl ComponentFunction<(), M> + Send + Sync + 'static,
|
||||||
|
) -> Self {
|
||||||
|
let rt = LocalPoolHandle::new(
|
||||||
|
std::thread::available_parallelism()
|
||||||
|
.map(usize::from)
|
||||||
|
.unwrap_or(1),
|
||||||
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
renderers: Arc::new(SsrRendererPool::new(4, config.incremental.clone())),
|
renderers: Arc::new(SsrRendererPool::new(4, config.incremental.clone())),
|
||||||
build_virtual_dom: Arc::new(move || VirtualDom::new(root)),
|
build_virtual_dom: Arc::new(move || VirtualDom::new_with_props(root.clone(), ())),
|
||||||
config,
|
config,
|
||||||
|
rt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new [`RenderHandleState`] with a custom [`VirtualDom`] factory. This method can be
|
/// Create a new [`FullstackState`] with a custom [`VirtualDom`] factory. This method can be
|
||||||
/// used to pass context into the root component of your application.
|
/// used to pass context into the root component of your application.
|
||||||
pub fn new_with_virtual_dom_factory(
|
pub fn new_with_virtual_dom_factory(
|
||||||
config: ServeConfig,
|
config: ServeConfig,
|
||||||
build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
|
build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let rt = LocalPoolHandle::new(
|
||||||
|
std::thread::available_parallelism()
|
||||||
|
.map(usize::from)
|
||||||
|
.unwrap_or(1),
|
||||||
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
renderers: Arc::new(SsrRendererPool::new(4, config.incremental.clone())),
|
renderers: Arc::new(SsrRendererPool::new(4, config.incremental.clone())),
|
||||||
config,
|
config,
|
||||||
build_virtual_dom: Arc::new(build_virtual_dom),
|
build_virtual_dom: Arc::new(build_virtual_dom),
|
||||||
|
rt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the [`ServeConfig`] for this [`RenderHandleState`]
|
/// Set the [`ServeConfig`] for this [`FullstackState`]
|
||||||
pub fn with_config(mut self, config: ServeConfig) -> Self {
|
pub fn with_config(mut self, config: ServeConfig) -> Self {
|
||||||
self.config = config;
|
self.config = config;
|
||||||
self
|
self
|
||||||
@@ -258,7 +300,7 @@ impl RenderHandleState {
|
|||||||
///
|
///
|
||||||
/// use axum::routing::get;
|
/// use axum::routing::get;
|
||||||
/// use dioxus::prelude::*;
|
/// use dioxus::prelude::*;
|
||||||
/// use dioxus_server::{RenderHandleState, render_handler, ServeConfig};
|
/// use dioxus_server::{FullstackState, render_handler, ServeConfig};
|
||||||
///
|
///
|
||||||
/// fn app() -> Element {
|
/// fn app() -> Element {
|
||||||
/// rsx! {
|
/// rsx! {
|
||||||
@@ -275,7 +317,7 @@ impl RenderHandleState {
|
|||||||
/// // to inject the context into server functions running outside
|
/// // to inject the context into server functions running outside
|
||||||
/// // of an SSR render context.
|
/// // of an SSR render context.
|
||||||
/// .fallback(get(render_handler))
|
/// .fallback(get(render_handler))
|
||||||
/// .with_state(RenderHandleState::new(ServeConfig::new(), app))
|
/// .with_state(FullstackState::new(ServeConfig::new(), app))
|
||||||
/// .into_make_service();
|
/// .into_make_service();
|
||||||
///
|
///
|
||||||
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
@@ -288,7 +330,7 @@ impl RenderHandleState {
|
|||||||
let response = state
|
let response = state
|
||||||
.renderers
|
.renderers
|
||||||
.clone()
|
.clone()
|
||||||
.render_to(parts, &state.config, {
|
.render_to(parts, &state.config, &state.rt, {
|
||||||
let build_virtual_dom = state.build_virtual_dom.clone();
|
let build_virtual_dom = state.build_virtual_dom.clone();
|
||||||
let context_providers = state.config.context_providers.clone();
|
let context_providers = state.config.context_providers.clone();
|
||||||
move || {
|
move || {
|
||||||
|
|||||||
@@ -1,35 +1,33 @@
|
|||||||
use axum::body::Body;
|
use crate::FullstackState;
|
||||||
use axum::routing::MethodRouter;
|
use axum::{
|
||||||
use axum::Router;
|
body::Body,
|
||||||
use dashmap::DashMap;
|
extract::{Request, State},
|
||||||
use dioxus_fullstack_core::{DioxusServerState, FullstackContext};
|
response::Response,
|
||||||
use http::Method;
|
routing::MethodRouter,
|
||||||
use std::{marker::PhantomData, sync::LazyLock};
|
};
|
||||||
|
use dioxus_fullstack_core::FullstackContext;
|
||||||
pub type AxumRequest = http::Request<Body>;
|
use http::{Method, StatusCode};
|
||||||
pub type AxumResponse = http::Response<Body>;
|
use std::{pin::Pin, prelude::rust_2024::Future};
|
||||||
|
|
||||||
/// A function endpoint that can be called from the client.
|
/// A function endpoint that can be called from the client.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ServerFunction<Caller = ()> {
|
pub struct ServerFunction {
|
||||||
path: &'static str,
|
path: &'static str,
|
||||||
method: Method,
|
method: Method,
|
||||||
handler: fn() -> MethodRouter<DioxusServerState>,
|
handler: fn() -> MethodRouter<FullstackState>,
|
||||||
_phantom: PhantomData<Caller>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerFunction {
|
impl ServerFunction {
|
||||||
/// Create a new server function object.
|
/// Create a new server function object from a MethodRouter
|
||||||
pub const fn new(
|
pub const fn new(
|
||||||
method: Method,
|
method: Method,
|
||||||
path: &'static str,
|
path: &'static str,
|
||||||
handler: fn() -> MethodRouter<DioxusServerState>,
|
handler: fn() -> MethodRouter<FullstackState>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
path,
|
path,
|
||||||
method,
|
method,
|
||||||
handler,
|
handler,
|
||||||
_phantom: PhantomData,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,51 +41,63 @@ impl ServerFunction {
|
|||||||
self.method.clone()
|
self.method.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collect all globally registered server functions
|
||||||
pub fn collect() -> Vec<&'static ServerFunction> {
|
pub fn collect() -> Vec<&'static ServerFunction> {
|
||||||
inventory::iter::<ServerFunction>().collect()
|
inventory::iter::<ServerFunction>().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handler(&self) -> fn() -> MethodRouter<DioxusServerState> {
|
/// Create a `MethodRouter` for this server function that can be mounted on an `axum::Router`.
|
||||||
self.handler
|
///
|
||||||
|
/// This runs the handler inside the required `FullstackContext` scope and populates
|
||||||
|
/// `FullstackContext` so that the handler can use those features.
|
||||||
|
///
|
||||||
|
/// It also runs the server function inside a tokio `LocalPool` to allow !Send futures.
|
||||||
|
pub fn method_router(&self) -> MethodRouter<FullstackState> {
|
||||||
|
(self.handler)()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register_server_fn_on_router<S>(&'static self, router: Router<S>) -> Router<S>
|
/// Creates a new `MethodRouter` for the given method and !Send handler.
|
||||||
where
|
///
|
||||||
S: Send + Sync + Clone + 'static,
|
/// This is used internally by the `ServerFunction` to create the method router that this
|
||||||
{
|
/// server function uses.
|
||||||
// // store Accepts and Referrer in case we need them for redirect (below)
|
#[allow(clippy::type_complexity)]
|
||||||
// let referrer = req.headers().get(REFERER).cloned();
|
pub fn make_handler(
|
||||||
// let accepts_html = req
|
method: Method,
|
||||||
// .headers()
|
handler: fn(State<FullstackContext>, Request) -> Pin<Box<dyn Future<Output = Response>>>,
|
||||||
// .get(ACCEPT)
|
) -> MethodRouter<FullstackState> {
|
||||||
// .and_then(|v| v.to_str().ok())
|
axum::routing::method_routing::on(
|
||||||
// .map(|v| v.contains("text/html"))
|
method
|
||||||
// .unwrap_or(false);
|
.try_into()
|
||||||
|
.expect("MethodFilter only supports standard HTTP methods"),
|
||||||
|
move |state: State<FullstackState>, request: Request| async move {
|
||||||
|
// Allow !Send futures by running in the render handlers pinned local pool
|
||||||
|
let result = state.rt.spawn_pinned(move || async move {
|
||||||
|
use dioxus_fullstack_core::FullstackContext;
|
||||||
|
use http::header::{ACCEPT, LOCATION, REFERER};
|
||||||
|
use http::StatusCode;
|
||||||
|
|
||||||
// // it it accepts text/html (i.e., is a plain form post) and doesn't already have a
|
// todo: we're copying the parts here, but it'd be ideal if we didn't.
|
||||||
// // Location set, then redirect to Referer
|
// We can probably just pass the URI in so the matching logic can work and then
|
||||||
// if accepts_html {
|
// in the server function, do all extraction via FullstackContext. This ensures
|
||||||
// if let Some(referrer) = referrer {
|
// calls to `.remove()` work as expected.
|
||||||
// let has_location = res.headers().get(LOCATION).is_some();
|
|
||||||
// if !has_location {
|
|
||||||
// *res.status_mut() = StatusCode::FOUND;
|
|
||||||
// res.headers_mut().insert(LOCATION, referrer);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
async fn server_context_middleware(
|
|
||||||
request: axum::extract::Request,
|
|
||||||
next: axum::middleware::Next,
|
|
||||||
) -> axum::response::Response {
|
|
||||||
let (parts, body) = request.into_parts();
|
let (parts, body) = request.into_parts();
|
||||||
let server_context = FullstackContext::new(parts.clone());
|
let server_context = FullstackContext::new(parts.clone());
|
||||||
let request = axum::extract::Request::from_parts(parts, body);
|
let request = axum::extract::Request::from_parts(parts, body);
|
||||||
|
|
||||||
|
// store Accepts and Referrer in case we need them for redirect (below)
|
||||||
|
let referrer = request.headers().get(REFERER).cloned();
|
||||||
|
let accepts_html = request
|
||||||
|
.headers()
|
||||||
|
.get(ACCEPT)
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.map(|v| v.contains("text/html"))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
server_context
|
server_context
|
||||||
|
.clone()
|
||||||
.scope(async move {
|
.scope(async move {
|
||||||
// Run the next middleware / handler inside the server context
|
// Run the next middleware / handler inside the server context
|
||||||
let mut response = next.run(request).await;
|
let mut response = handler(State(server_context), request).await;
|
||||||
|
|
||||||
let server_context = FullstackContext::current().expect(
|
let server_context = FullstackContext::current().expect(
|
||||||
"Server context should be available inside the server context scope",
|
"Server context should be available inside the server context scope",
|
||||||
@@ -99,16 +109,35 @@ impl ServerFunction {
|
|||||||
response.headers_mut().extend(headers);
|
response.headers_mut().extend(headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// it it accepts text/html (i.e., is a plain form post) and doesn't already have a
|
||||||
|
// Location set, then redirect to Referer
|
||||||
|
if accepts_html {
|
||||||
|
if let Some(referrer) = referrer {
|
||||||
|
let has_location = response.headers().get(LOCATION).is_some();
|
||||||
|
if !has_location {
|
||||||
|
*response.status_mut() = StatusCode::FOUND;
|
||||||
|
response.headers_mut().insert(LOCATION, referrer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
response
|
response
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}).await;
|
||||||
|
|
||||||
router.route(
|
match result {
|
||||||
self.path(),
|
Ok(response) => response,
|
||||||
((self.handler)())
|
Err(err) => Response::builder()
|
||||||
.with_state(DioxusServerState {})
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
.layer(axum::middleware::from_fn(server_context_middleware)),
|
.body(Body::new(if cfg!(debug_assertions) {
|
||||||
|
format!("Server function panicked: {}", err)
|
||||||
|
} else {
|
||||||
|
"Internal Server Error".to_string()
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,18 +149,3 @@ impl inventory::Collect for ServerFunction {
|
|||||||
®ISTRY
|
®ISTRY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The set of all registered server function paths.
|
|
||||||
pub fn server_fn_paths() -> impl Iterator<Item = (&'static str, Method)> {
|
|
||||||
REGISTERED_SERVER_FUNCTIONS
|
|
||||||
.iter()
|
|
||||||
.map(|item| (item.path(), item.method()))
|
|
||||||
}
|
|
||||||
|
|
||||||
type LazyServerFnMap = LazyLock<DashMap<(String, Method), ServerFunction>>;
|
|
||||||
static REGISTERED_SERVER_FUNCTIONS: LazyServerFnMap = std::sync::LazyLock::new(|| {
|
|
||||||
inventory::iter::<ServerFunction>
|
|
||||||
.into_iter()
|
|
||||||
.map(|obj| ((obj.path().to_string(), obj.method()), obj.clone()))
|
|
||||||
.collect()
|
|
||||||
});
|
|
||||||
@@ -21,12 +21,11 @@ use http::{request::Parts, HeaderMap, StatusCode};
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fmt::Write,
|
fmt::Write,
|
||||||
future::Future,
|
|
||||||
iter::Peekable,
|
iter::Peekable,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
use tokio::task::JoinHandle;
|
use tokio_util::task::LocalPoolHandle;
|
||||||
|
|
||||||
use crate::StreamingMode;
|
use crate::StreamingMode;
|
||||||
|
|
||||||
@@ -99,6 +98,7 @@ impl SsrRendererPool {
|
|||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
parts: Parts,
|
parts: Parts,
|
||||||
cfg: &ServeConfig,
|
cfg: &ServeConfig,
|
||||||
|
rt: &LocalPoolHandle,
|
||||||
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
|
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
|
||||||
) -> Result<
|
) -> Result<
|
||||||
(
|
(
|
||||||
@@ -420,7 +420,8 @@ impl SsrRendererPool {
|
|||||||
myself.renderers.write().unwrap().push(renderer);
|
myself.renderers.write().unwrap().push(renderer);
|
||||||
};
|
};
|
||||||
|
|
||||||
let join_handle = Self::spawn_platform(create_render_future);
|
// Spawn the render future onto the local pool
|
||||||
|
let join_handle = rt.spawn_pinned(create_render_future);
|
||||||
|
|
||||||
// Wait for the initial result which determines the status code
|
// Wait for the initial result which determines the status code
|
||||||
let (status, headers) = initial_result_rx
|
let (status, headers) = initial_result_rx
|
||||||
@@ -763,32 +764,4 @@ impl SsrRendererPool {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn a task in the background. If wasm is enabled, this will use the single threaded tokio runtime
|
|
||||||
fn spawn_platform<Fut>(f: impl FnOnce() -> Fut + Send + 'static) -> JoinHandle<Fut::Output>
|
|
||||||
where
|
|
||||||
Fut: Future + 'static,
|
|
||||||
Fut::Output: Send + 'static,
|
|
||||||
{
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
{
|
|
||||||
use tokio_util::task::LocalPoolHandle;
|
|
||||||
static TASK_POOL: std::sync::OnceLock<LocalPoolHandle> = std::sync::OnceLock::new();
|
|
||||||
|
|
||||||
let pool = TASK_POOL.get_or_init(|| {
|
|
||||||
LocalPoolHandle::new(
|
|
||||||
std::thread::available_parallelism()
|
|
||||||
.map(usize::from)
|
|
||||||
.unwrap_or(1),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
pool.spawn_pinned(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
{
|
|
||||||
tokio::task::spawn_local(f())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ tower-http = { workspace = true, features = ["fs", "limit"], optional = true }
|
|||||||
tower-layer = { version = "0.3.3", optional = true }
|
tower-layer = { version = "0.3.3", optional = true }
|
||||||
|
|
||||||
# payloads
|
# payloads
|
||||||
postcard = { features = ["alloc"], optional = true, workspace = true, default-features = true }
|
postcard = { features = ["alloc", "use-std"], optional = true, workspace = true, default-features = true }
|
||||||
rmp-serde = { version = "1.3", optional = true }
|
rmp-serde = { version = "1.3", optional = true }
|
||||||
async-stream = "0.3.6"
|
async-stream = "0.3.6"
|
||||||
|
|
||||||
|
|||||||
@@ -129,10 +129,14 @@ impl ClientRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new reqwest request builder with the method, url, and headers set from this ClientRequest
|
/// Creates a new reqwest request builder with the method, url, and headers set from this ClientRequest
|
||||||
|
///
|
||||||
|
/// Using this method attaches `X-Request-Client: dioxus` header to the request.
|
||||||
pub fn new_reqwest_request(&self) -> reqwest::RequestBuilder {
|
pub fn new_reqwest_request(&self) -> reqwest::RequestBuilder {
|
||||||
let client = GLOBAL_REQUEST_CLIENT.get_or_init(Self::new_reqwest_client);
|
let client = GLOBAL_REQUEST_CLIENT.get_or_init(Self::new_reqwest_client);
|
||||||
|
|
||||||
let mut req = client.request(self.method.clone(), self.url.clone());
|
let mut req = client
|
||||||
|
.request(self.method.clone(), self.url.clone())
|
||||||
|
.header("X-Request-Client", "dioxus");
|
||||||
|
|
||||||
for (key, value) in self.headers.iter() {
|
for (key, value) in self.headers.iter() {
|
||||||
req = req.header(key, value);
|
req = req.header(key, value);
|
||||||
@@ -141,6 +145,7 @@ impl ClientRequest {
|
|||||||
req
|
req
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Using this method attaches `X-Request-Client-Dioxus` header to the request.
|
||||||
#[cfg(feature = "web")]
|
#[cfg(feature = "web")]
|
||||||
pub fn new_gloo_request(&self) -> gloo_net::http::RequestBuilder {
|
pub fn new_gloo_request(&self) -> gloo_net::http::RequestBuilder {
|
||||||
let mut builder = gloo_net::http::RequestBuilder::new(
|
let mut builder = gloo_net::http::RequestBuilder::new(
|
||||||
@@ -155,6 +160,7 @@ impl ClientRequest {
|
|||||||
)
|
)
|
||||||
.as_str(),
|
.as_str(),
|
||||||
)
|
)
|
||||||
|
.header("X-Request-Client", "dioxus")
|
||||||
.method(self.method.clone());
|
.method(self.method.clone());
|
||||||
|
|
||||||
for (key, value) in self.headers.iter() {
|
for (key, value) in self.headers.iter() {
|
||||||
|
|||||||
@@ -7,8 +7,13 @@ use serde::{de::DeserializeOwned, Serialize};
|
|||||||
pub trait Encoding {
|
pub trait Encoding {
|
||||||
fn content_type() -> &'static str;
|
fn content_type() -> &'static str;
|
||||||
fn stream_content_type() -> &'static str;
|
fn stream_content_type() -> &'static str;
|
||||||
fn to_bytes(data: impl Serialize) -> Option<Bytes>;
|
fn to_bytes(data: impl Serialize) -> Option<Bytes> {
|
||||||
fn from_bytes<O: DeserializeOwned>(bytes: Bytes) -> Option<O>;
|
let mut buf = Vec::new();
|
||||||
|
Self::encode(data, &mut buf)?;
|
||||||
|
Some(buf.into())
|
||||||
|
}
|
||||||
|
fn encode(data: impl Serialize, buf: &mut Vec<u8>) -> Option<usize>;
|
||||||
|
fn decode<O: DeserializeOwned>(bytes: Bytes) -> Option<O>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct JsonEncoding;
|
pub struct JsonEncoding;
|
||||||
@@ -19,11 +24,14 @@ impl Encoding for JsonEncoding {
|
|||||||
fn stream_content_type() -> &'static str {
|
fn stream_content_type() -> &'static str {
|
||||||
"application/stream+json"
|
"application/stream+json"
|
||||||
}
|
}
|
||||||
fn to_bytes(data: impl Serialize) -> Option<Bytes> {
|
|
||||||
serde_json::to_vec(&data).ok().map(Into::into)
|
fn encode(data: impl Serialize, mut buf: &mut Vec<u8>) -> Option<usize> {
|
||||||
|
let len = buf.len();
|
||||||
|
serde_json::to_writer(&mut buf, &data).ok()?;
|
||||||
|
Some(buf.len() - len)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_bytes<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
|
fn decode<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
|
||||||
serde_json::from_slice(&bytes).ok()
|
serde_json::from_slice(&bytes).ok()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,14 +44,15 @@ impl Encoding for CborEncoding {
|
|||||||
fn stream_content_type() -> &'static str {
|
fn stream_content_type() -> &'static str {
|
||||||
"application/stream+cbor"
|
"application/stream+cbor"
|
||||||
}
|
}
|
||||||
fn to_bytes(data: impl Serialize) -> Option<Bytes> {
|
|
||||||
let mut buf = Vec::new();
|
fn decode<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
|
||||||
ciborium::into_writer(&data, &mut buf).ok()?;
|
ciborium::de::from_reader(bytes.as_ref()).ok()
|
||||||
Some(buf.into())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_bytes<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
|
fn encode(data: impl Serialize, mut buf: &mut Vec<u8>) -> Option<usize> {
|
||||||
ciborium::de::from_reader(bytes.as_ref()).ok()
|
let len = buf.len();
|
||||||
|
ciborium::into_writer(&data, &mut buf).ok()?;
|
||||||
|
Some(buf.len() - len)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,11 +66,14 @@ impl Encoding for PostcardEncoding {
|
|||||||
fn stream_content_type() -> &'static str {
|
fn stream_content_type() -> &'static str {
|
||||||
"application/stream+postcard"
|
"application/stream+postcard"
|
||||||
}
|
}
|
||||||
fn to_bytes(data: impl Serialize) -> Option<Bytes> {
|
|
||||||
postcard::to_allocvec(&data).ok().map(Into::into)
|
fn encode(data: impl Serialize, mut buf: &mut Vec<u8>) -> Option<usize> {
|
||||||
|
let len = buf.len();
|
||||||
|
postcard::to_io(&data, &mut buf).ok()?;
|
||||||
|
Some(buf.len() - len)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_bytes<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
|
fn decode<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
|
||||||
postcard::from_bytes(bytes.as_ref()).ok()
|
postcard::from_bytes(bytes.as_ref()).ok()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,11 +88,13 @@ impl Encoding for MsgPackEncoding {
|
|||||||
fn stream_content_type() -> &'static str {
|
fn stream_content_type() -> &'static str {
|
||||||
"application/stream+msgpack"
|
"application/stream+msgpack"
|
||||||
}
|
}
|
||||||
fn to_bytes(data: impl Serialize) -> Option<Bytes> {
|
fn encode(data: impl Serialize, buf: &mut Vec<u8>) -> Option<usize> {
|
||||||
rmp_serde::to_vec(&data).ok().map(Into::into)
|
let len = buf.len();
|
||||||
|
rmp_serde::encode::write(buf, &data).ok()?;
|
||||||
|
Some(buf.len() - len)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_bytes<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
|
fn decode<O: DeserializeOwned>(bytes: Bytes) -> Option<O> {
|
||||||
rmp_serde::from_slice(&bytes).ok()
|
rmp_serde::from_slice(&bytes).ok()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,20 @@ impl<T: Send + Sync + 'static> Lazy<T> {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the value of the `Lazy` instance. This will block the current thread if the
|
||||||
|
/// value is not yet initialized.
|
||||||
|
pub fn get(&self) -> &T {
|
||||||
|
if self.constructor.is_none() {
|
||||||
|
return self.value.get().expect("Lazy value is not initialized. Make sure to call `initialize` before dereferencing.");
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.value.get().is_none() {
|
||||||
|
self.initialize().expect("Failed to initialize lazy value");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.value.get().unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Send + Sync + 'static> Default for Lazy<T> {
|
impl<T: Send + Sync + 'static> Default for Lazy<T> {
|
||||||
@@ -100,15 +114,13 @@ impl<T: Send + Sync + 'static> std::ops::Deref for Lazy<T> {
|
|||||||
type Target = T;
|
type Target = T;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
if self.constructor.is_none() {
|
self.get()
|
||||||
return self.value.get().expect("Lazy value is not initialized. Make sure to call `initialize` before dereferencing.");
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if self.value.get().is_none() {
|
|
||||||
self.initialize().expect("Failed to initialize lazy value");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.value.get().unwrap()
|
impl<T: std::fmt::Debug + Send + Sync + 'static> std::fmt::Debug for Lazy<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("Lazy").field("value", self.get()).finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,8 +141,14 @@ where
|
|||||||
{
|
{
|
||||||
let ptr: F = unsafe { std::mem::zeroed() };
|
let ptr: F = unsafe { std::mem::zeroed() };
|
||||||
let fut = ptr();
|
let fut = ptr();
|
||||||
let rt = tokio::runtime::Handle::current();
|
return std::thread::spawn(move || {
|
||||||
return std::thread::spawn(move || rt.block_on(fut).map_err(|e| e.into()))
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(fut)
|
||||||
|
.map_err(|e| e.into())
|
||||||
|
})
|
||||||
.join()
|
.join()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,9 @@ pub mod payloads {
|
|||||||
pub mod header;
|
pub mod header;
|
||||||
pub use header::*;
|
pub use header::*;
|
||||||
|
|
||||||
|
pub mod query;
|
||||||
|
pub use query::*;
|
||||||
|
|
||||||
#[cfg(feature = "ws")]
|
#[cfg(feature = "ws")]
|
||||||
pub mod websocket;
|
pub mod websocket;
|
||||||
#[cfg(feature = "ws")]
|
#[cfg(feature = "ws")]
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ use crate::{
|
|||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum_core::extract::{FromRequest, Request};
|
use axum_core::extract::{FromRequest, Request};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use dioxus_fullstack_core::{DioxusServerState, RequestError};
|
use dioxus_fullstack_core::RequestError;
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use send_wrapper::SendWrapper;
|
use send_wrapper::SendWrapper;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -172,7 +172,7 @@ pub mod req_to {
|
|||||||
impl<T, O, R> EncodeRequest<T, O, R> for &&&&&&&&&ServerFnEncoder<T, O>
|
impl<T, O, R> EncodeRequest<T, O, R> for &&&&&&&&&ServerFnEncoder<T, O>
|
||||||
where
|
where
|
||||||
T: 'static,
|
T: 'static,
|
||||||
O: FromRequest<DioxusServerState> + IntoRequest<R>,
|
O: IntoRequest<R>,
|
||||||
{
|
{
|
||||||
type VerifyEncode = EncodeIsVerified;
|
type VerifyEncode = EncodeIsVerified;
|
||||||
fn fetch_client(
|
fn fetch_client(
|
||||||
@@ -214,6 +214,8 @@ pub mod req_to {
|
|||||||
pub use decode_ok::*;
|
pub use decode_ok::*;
|
||||||
mod decode_ok {
|
mod decode_ok {
|
||||||
|
|
||||||
|
use crate::{CantDecode, DecodeIsVerified};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Convert the reqwest response into the desired type, in place.
|
/// Convert the reqwest response into the desired type, in place.
|
||||||
@@ -222,13 +224,16 @@ mod decode_ok {
|
|||||||
/// This is because FromResponse types are more specialized and can handle things like websockets and files.
|
/// This is because FromResponse types are more specialized and can handle things like websockets and files.
|
||||||
/// DeserializeOwned types are more general and can handle things like JSON responses.
|
/// DeserializeOwned types are more general and can handle things like JSON responses.
|
||||||
pub trait RequestDecodeResult<T, R> {
|
pub trait RequestDecodeResult<T, R> {
|
||||||
|
type VerifyDecode;
|
||||||
fn decode_client_response(
|
fn decode_client_response(
|
||||||
&self,
|
&self,
|
||||||
res: Result<R, RequestError>,
|
res: Result<R, RequestError>,
|
||||||
) -> impl Future<Output = Result<Result<T, ServerFnError>, RequestError>> + Send;
|
) -> impl Future<Output = Result<Result<T, ServerFnError>, RequestError>> + Send;
|
||||||
|
fn verify_can_deserialize(&self) -> Self::VerifyDecode;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: FromResponse<R>, E, R> RequestDecodeResult<T, R> for &&&ServerFnDecoder<Result<T, E>> {
|
impl<T: FromResponse<R>, E, R> RequestDecodeResult<T, R> for &&&ServerFnDecoder<Result<T, E>> {
|
||||||
|
type VerifyDecode = DecodeIsVerified;
|
||||||
fn decode_client_response(
|
fn decode_client_response(
|
||||||
&self,
|
&self,
|
||||||
res: Result<R, RequestError>,
|
res: Result<R, RequestError>,
|
||||||
@@ -240,11 +245,15 @@ mod decode_ok {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
fn verify_can_deserialize(&self) -> Self::VerifyDecode {
|
||||||
|
DecodeIsVerified
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: DeserializeOwned, E> RequestDecodeResult<T, ClientResponse>
|
impl<T: DeserializeOwned, E> RequestDecodeResult<T, ClientResponse>
|
||||||
for &&ServerFnDecoder<Result<T, E>>
|
for &&ServerFnDecoder<Result<T, E>>
|
||||||
{
|
{
|
||||||
|
type VerifyDecode = DecodeIsVerified;
|
||||||
fn decode_client_response(
|
fn decode_client_response(
|
||||||
&self,
|
&self,
|
||||||
res: Result<ClientResponse, RequestError>,
|
res: Result<ClientResponse, RequestError>,
|
||||||
@@ -304,6 +313,24 @@ mod decode_ok {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
fn verify_can_deserialize(&self) -> Self::VerifyDecode {
|
||||||
|
DecodeIsVerified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, R, E> RequestDecodeResult<T, R> for &ServerFnDecoder<Result<T, E>> {
|
||||||
|
type VerifyDecode = CantDecode;
|
||||||
|
|
||||||
|
fn decode_client_response(
|
||||||
|
&self,
|
||||||
|
_res: Result<R, RequestError>,
|
||||||
|
) -> impl Future<Output = Result<Result<T, ServerFnError>, RequestError>> + Send {
|
||||||
|
async move { unimplemented!() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_can_deserialize(&self) -> Self::VerifyDecode {
|
||||||
|
CantDecode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait RequestDecodeErr<T, E> {
|
pub trait RequestDecodeErr<T, E> {
|
||||||
@@ -460,33 +487,34 @@ pub use req_from::*;
|
|||||||
pub mod req_from {
|
pub mod req_from {
|
||||||
use super::*;
|
use super::*;
|
||||||
use axum::{extract::FromRequestParts, response::Response};
|
use axum::{extract::FromRequestParts, response::Response};
|
||||||
|
use dioxus_fullstack_core::FullstackContext;
|
||||||
|
|
||||||
pub trait ExtractRequest<In, Out, H, M = ()> {
|
pub trait ExtractRequest<In, Out, H, M = ()> {
|
||||||
fn extract_axum(
|
fn extract_axum(
|
||||||
&self,
|
&self,
|
||||||
state: DioxusServerState,
|
state: FullstackContext,
|
||||||
request: Request,
|
request: Request,
|
||||||
map: fn(In) -> Out,
|
map: fn(In) -> Out,
|
||||||
) -> impl Future<Output = Result<(H, Out), Response>> + 'static;
|
) -> impl Future<Output = Result<(Out, H), Response>> + 'static;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// When you're extracting entirely on the server, we need to reject client-consuning request bodies
|
/// When you're extracting entirely on the server, we need to reject client-consuning request bodies
|
||||||
/// This sits above priority in the combined headers on server / body on client case.
|
/// This sits above priority in the combined headers on server / body on client case.
|
||||||
impl<In, M, H> ExtractRequest<In, (), H, M> for &&&&&&&&&&&ServerFnEncoder<In, ()>
|
impl<In, M, H> ExtractRequest<In, (), H, M> for &&&&&&&&&&&ServerFnEncoder<In, ()>
|
||||||
where
|
where
|
||||||
H: FromRequest<DioxusServerState, M> + 'static,
|
H: FromRequest<FullstackContext, M> + 'static,
|
||||||
{
|
{
|
||||||
fn extract_axum(
|
fn extract_axum(
|
||||||
&self,
|
&self,
|
||||||
state: DioxusServerState,
|
state: FullstackContext,
|
||||||
request: Request,
|
request: Request,
|
||||||
_map: fn(In) -> (),
|
_map: fn(In) -> (),
|
||||||
) -> impl Future<Output = Result<(H, ()), Response>> + 'static {
|
) -> impl Future<Output = Result<((), H), Response>> + 'static {
|
||||||
async move {
|
async move {
|
||||||
H::from_request(request, &state)
|
H::from_request(request, &state)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.into_response())
|
.map_err(|e| e.into_response())
|
||||||
.map(|out| (out, ()))
|
.map(|out| ((), out))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -496,14 +524,14 @@ pub mod req_from {
|
|||||||
where
|
where
|
||||||
In: DeserializeOwned + 'static,
|
In: DeserializeOwned + 'static,
|
||||||
Out: 'static,
|
Out: 'static,
|
||||||
H: FromRequestParts<DioxusServerState>,
|
H: FromRequestParts<FullstackContext>,
|
||||||
{
|
{
|
||||||
fn extract_axum(
|
fn extract_axum(
|
||||||
&self,
|
&self,
|
||||||
_state: DioxusServerState,
|
_state: FullstackContext,
|
||||||
request: Request,
|
request: Request,
|
||||||
map: fn(In) -> Out,
|
map: fn(In) -> Out,
|
||||||
) -> impl Future<Output = Result<(H, Out), Response>> + 'static {
|
) -> impl Future<Output = Result<(Out, H), Response>> + 'static {
|
||||||
async move {
|
async move {
|
||||||
let (mut parts, body) = request.into_parts();
|
let (mut parts, body) = request.into_parts();
|
||||||
let headers = H::from_request_parts(&mut parts, &_state)
|
let headers = H::from_request_parts(&mut parts, &_state)
|
||||||
@@ -525,7 +553,7 @@ pub mod req_from {
|
|||||||
.map_err(|e| ServerFnError::from(e).into_response())
|
.map_err(|e| ServerFnError::from(e).into_response())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
Ok((headers, out))
|
Ok((out, headers))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -533,15 +561,15 @@ pub mod req_from {
|
|||||||
/// We skip the BodySerialize wrapper and just go for the output type directly.
|
/// We skip the BodySerialize wrapper and just go for the output type directly.
|
||||||
impl<In, Out, M, H> ExtractRequest<In, Out, H, M> for &&&&&&&&&ServerFnEncoder<In, Out>
|
impl<In, Out, M, H> ExtractRequest<In, Out, H, M> for &&&&&&&&&ServerFnEncoder<In, Out>
|
||||||
where
|
where
|
||||||
Out: FromRequest<DioxusServerState, M> + 'static,
|
Out: FromRequest<FullstackContext, M> + 'static,
|
||||||
H: FromRequestParts<DioxusServerState>,
|
H: FromRequestParts<FullstackContext>,
|
||||||
{
|
{
|
||||||
fn extract_axum(
|
fn extract_axum(
|
||||||
&self,
|
&self,
|
||||||
state: DioxusServerState,
|
state: FullstackContext,
|
||||||
request: Request,
|
request: Request,
|
||||||
_map: fn(In) -> Out,
|
_map: fn(In) -> Out,
|
||||||
) -> impl Future<Output = Result<(H, Out), Response>> + 'static {
|
) -> impl Future<Output = Result<(Out, H), Response>> + 'static {
|
||||||
async move {
|
async move {
|
||||||
let (mut parts, body) = request.into_parts();
|
let (mut parts, body) = request.into_parts();
|
||||||
let headers = H::from_request_parts(&mut parts, &state)
|
let headers = H::from_request_parts(&mut parts, &state)
|
||||||
@@ -554,7 +582,7 @@ pub mod req_from {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| e.into_response());
|
.map_err(|e| e.into_response());
|
||||||
|
|
||||||
res.map(|out| (headers, out))
|
res.map(|out| (out, headers))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -616,7 +644,7 @@ mod resp {
|
|||||||
|
|
||||||
#[allow(clippy::result_large_err)]
|
#[allow(clippy::result_large_err)]
|
||||||
pub trait MakeAxumError<E> {
|
pub trait MakeAxumError<E> {
|
||||||
fn make_axum_error(self, result: Result<Response, E>) -> Result<Response, Response>;
|
fn make_axum_error(self, result: Result<Response, E>) -> Response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the status code from the error type if possible.
|
/// Get the status code from the error type if possible.
|
||||||
@@ -639,9 +667,9 @@ mod resp {
|
|||||||
where
|
where
|
||||||
E: AsStatusCode + From<ServerFnError> + Serialize + DeserializeOwned + Display,
|
E: AsStatusCode + From<ServerFnError> + Serialize + DeserializeOwned + Display,
|
||||||
{
|
{
|
||||||
fn make_axum_error(self, result: Result<Response, E>) -> Result<Response, Response> {
|
fn make_axum_error(self, result: Result<Response, E>) -> Response {
|
||||||
match result {
|
match result {
|
||||||
Ok(res) => Ok(res),
|
Ok(res) => res,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let status_code = err.as_status_code();
|
let status_code = err.as_status_code();
|
||||||
let err = ErrorPayload {
|
let err = ErrorPayload {
|
||||||
@@ -656,19 +684,16 @@ mod resp {
|
|||||||
HeaderValue::from_static("application/json"),
|
HeaderValue::from_static("application/json"),
|
||||||
);
|
);
|
||||||
*resp.status_mut() = status_code;
|
*resp.status_mut() = status_code;
|
||||||
Err(resp)
|
resp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> MakeAxumError<CapturedError> for &&ServerFnDecoder<Result<T, CapturedError>> {
|
impl<T> MakeAxumError<CapturedError> for &&ServerFnDecoder<Result<T, CapturedError>> {
|
||||||
fn make_axum_error(
|
fn make_axum_error(self, result: Result<Response, CapturedError>) -> Response {
|
||||||
self,
|
|
||||||
result: Result<Response, CapturedError>,
|
|
||||||
) -> Result<Response, Response> {
|
|
||||||
match result {
|
match result {
|
||||||
Ok(res) => Ok(res),
|
Ok(res) => res,
|
||||||
|
|
||||||
// Optimize the case where we have sole ownership of the error
|
// Optimize the case where we have sole ownership of the error
|
||||||
Err(errr) if errr._strong_count() == 1 => {
|
Err(errr) if errr._strong_count() == 1 => {
|
||||||
@@ -721,19 +746,16 @@ mod resp {
|
|||||||
HeaderValue::from_static("application/json"),
|
HeaderValue::from_static("application/json"),
|
||||||
);
|
);
|
||||||
*resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
*resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||||
Err(resp)
|
resp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> MakeAxumError<anyhow::Error> for &&ServerFnDecoder<Result<T, anyhow::Error>> {
|
impl<T> MakeAxumError<anyhow::Error> for &&ServerFnDecoder<Result<T, anyhow::Error>> {
|
||||||
fn make_axum_error(
|
fn make_axum_error(self, result: Result<Response, anyhow::Error>) -> Response {
|
||||||
self,
|
|
||||||
result: Result<Response, anyhow::Error>,
|
|
||||||
) -> Result<Response, Response> {
|
|
||||||
match result {
|
match result {
|
||||||
Ok(res) => Ok(res),
|
Ok(res) => res,
|
||||||
Err(errr) => {
|
Err(errr) => {
|
||||||
// The `WithHttpError` trait emits ServerFnErrors so we can downcast them here
|
// The `WithHttpError` trait emits ServerFnErrors so we can downcast them here
|
||||||
// to create richer responses.
|
// to create richer responses.
|
||||||
@@ -775,19 +797,16 @@ mod resp {
|
|||||||
HeaderValue::from_static("application/json"),
|
HeaderValue::from_static("application/json"),
|
||||||
);
|
);
|
||||||
*resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
*resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||||
Err(resp)
|
resp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> MakeAxumError<StatusCode> for &&ServerFnDecoder<Result<T, StatusCode>> {
|
impl<T> MakeAxumError<StatusCode> for &&ServerFnDecoder<Result<T, StatusCode>> {
|
||||||
fn make_axum_error(
|
fn make_axum_error(self, result: Result<Response, StatusCode>) -> Response {
|
||||||
self,
|
|
||||||
result: Result<Response, StatusCode>,
|
|
||||||
) -> Result<Response, Response> {
|
|
||||||
match result {
|
match result {
|
||||||
Ok(resp) => Ok(resp),
|
Ok(resp) => resp,
|
||||||
Err(status) => {
|
Err(status) => {
|
||||||
let body = serde_json::to_string(&ErrorPayload::<()> {
|
let body = serde_json::to_string(&ErrorPayload::<()> {
|
||||||
code: status.as_u16(),
|
code: status.as_u16(),
|
||||||
@@ -801,19 +820,16 @@ mod resp {
|
|||||||
HeaderValue::from_static("application/json"),
|
HeaderValue::from_static("application/json"),
|
||||||
);
|
);
|
||||||
*resp.status_mut() = status;
|
*resp.status_mut() = status;
|
||||||
Err(resp)
|
resp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> MakeAxumError<HttpError> for &ServerFnDecoder<Result<T, HttpError>> {
|
impl<T> MakeAxumError<HttpError> for &ServerFnDecoder<Result<T, HttpError>> {
|
||||||
fn make_axum_error(
|
fn make_axum_error(self, result: Result<Response, HttpError>) -> Response {
|
||||||
self,
|
|
||||||
result: Result<Response, HttpError>,
|
|
||||||
) -> Result<Response, Response> {
|
|
||||||
match result {
|
match result {
|
||||||
Ok(resp) => Ok(resp),
|
Ok(resp) => resp,
|
||||||
Err(http_err) => {
|
Err(http_err) => {
|
||||||
let body = serde_json::to_string(&ErrorPayload::<()> {
|
let body = serde_json::to_string(&ErrorPayload::<()> {
|
||||||
code: http_err.status.as_u16(),
|
code: http_err.status.as_u16(),
|
||||||
@@ -829,7 +845,7 @@ mod resp {
|
|||||||
HeaderValue::from_static("application/json"),
|
HeaderValue::from_static("application/json"),
|
||||||
);
|
);
|
||||||
*resp.status_mut() = http_err.status;
|
*resp.status_mut() = http_err.status;
|
||||||
Err(resp)
|
resp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ impl<T: From<String>> FromResponse for Html<T> {
|
|||||||
|
|
||||||
impl<T> IntoRequest for Json<T>
|
impl<T> IntoRequest for Json<T>
|
||||||
where
|
where
|
||||||
T: Serialize + 'static,
|
T: Serialize + 'static + DeserializeOwned,
|
||||||
{
|
{
|
||||||
fn into_request(self, request: ClientRequest) -> impl Future<Output = ClientResult> + 'static {
|
fn into_request(self, request: ClientRequest) -> impl Future<Output = ClientResult> + 'static {
|
||||||
async move { request.send_json(&self.0).await }
|
async move { request.send_json(&self.0).await }
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ pub use axum::extract::Form;
|
|||||||
|
|
||||||
impl<T> IntoRequest for Form<T>
|
impl<T> IntoRequest for Form<T>
|
||||||
where
|
where
|
||||||
T: Serialize + 'static,
|
T: Serialize + 'static + DeserializeOwned,
|
||||||
{
|
{
|
||||||
fn into_request(self, req: ClientRequest) -> impl Future<Output = ClientResult> + 'static {
|
fn into_request(self, req: ClientRequest) -> impl Future<Output = ClientResult> + 'static {
|
||||||
async move { req.send_form(&self.0).await }
|
async move { req.send_form(&self.0).await }
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use crate::ServerFnError;
|
||||||
|
use axum::extract::FromRequestParts;
|
||||||
|
use http::request::Parts;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
/// An extractor that deserializes query parameters into the given type `T`.
|
||||||
|
///
|
||||||
|
/// This uses `serde_qs` under the hood to support complex query parameter structures.
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
pub struct Query<T>(pub T);
|
||||||
|
|
||||||
|
impl<T, S> FromRequestParts<S> for Query<T>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned,
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = ServerFnError;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
let inner: T = serde_qs::from_str(parts.uri.query().unwrap_or_default())
|
||||||
|
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
|
||||||
|
Ok(Self(inner))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Deref for Query<T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,18 +9,83 @@ use axum_core::response::IntoResponse;
|
|||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use dioxus_fullstack_core::{HttpError, RequestError};
|
use dioxus_fullstack_core::{HttpError, RequestError};
|
||||||
use futures::{Stream, StreamExt};
|
use futures::{Stream, StreamExt};
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
use futures_channel::mpsc::UnboundedSender;
|
||||||
use headers::{ContentType, Header};
|
use headers::{ContentType, Header};
|
||||||
use send_wrapper::SendWrapper;
|
use send_wrapper::SendWrapper;
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use std::{future::Future, marker::PhantomData, pin::Pin};
|
use std::{future::Future, marker::PhantomData, pin::Pin};
|
||||||
|
|
||||||
|
/// A stream of text data.
|
||||||
|
///
|
||||||
|
/// # Chunking
|
||||||
|
///
|
||||||
|
/// Note that strings sent by the server might not arrive in the same chunking as they were sent.
|
||||||
|
///
|
||||||
|
/// This is because the underlying transport layer (HTTP/2 or HTTP/3) may choose to split or combine
|
||||||
|
/// chunks for efficiency.
|
||||||
|
///
|
||||||
|
/// If you need to preserve individual string boundaries, consider using `ChunkedTextStream` or another
|
||||||
|
/// encoding that preserves chunk boundaries.
|
||||||
pub type TextStream = Streaming<String>;
|
pub type TextStream = Streaming<String>;
|
||||||
|
|
||||||
|
/// A stream of binary data.
|
||||||
|
///
|
||||||
|
/// # Chunking
|
||||||
|
///
|
||||||
|
/// Note that bytes sent by the server might not arrive in the same chunking as they were sent.
|
||||||
|
/// This is because the underlying transport layer (HTTP/2 or HTTP/3) may choose to split or combine
|
||||||
|
/// chunks for efficiency.
|
||||||
|
///
|
||||||
|
/// If you need to preserve individual byte boundaries, consider using `ChunkedByteStream` or another
|
||||||
|
/// encoding that preserves chunk boundaries.
|
||||||
pub type ByteStream = Streaming<Bytes>;
|
pub type ByteStream = Streaming<Bytes>;
|
||||||
|
|
||||||
|
/// A stream of JSON-encoded data.
|
||||||
|
///
|
||||||
|
/// # Chunking
|
||||||
|
///
|
||||||
|
/// Normally, it's not possible to stream JSON over HTTP because browsers are free to re-chunk
|
||||||
|
/// data as they see fit. However, this implementation manually frames each JSON as if it were an unmasked
|
||||||
|
/// websocket message.
|
||||||
|
///
|
||||||
|
/// If you need to send a stream of JSON data without framing, consider using TextStream instead and
|
||||||
|
/// manually handling JSON buffering.
|
||||||
pub type JsonStream<T> = Streaming<T, JsonEncoding>;
|
pub type JsonStream<T> = Streaming<T, JsonEncoding>;
|
||||||
|
|
||||||
|
/// A stream of Cbor-encoded data.
|
||||||
|
///
|
||||||
|
/// # Chunking
|
||||||
|
///
|
||||||
|
/// Normally, it's not possible to stream JSON over HTTP because browsers are free to re-chunk
|
||||||
|
/// data as they see fit. However, this implementation manually frames each item as if it were an unmasked
|
||||||
|
/// websocket message.
|
||||||
pub type CborStream<T> = Streaming<T, CborEncoding>;
|
pub type CborStream<T> = Streaming<T, CborEncoding>;
|
||||||
|
|
||||||
|
/// A stream of manually chunked binary data.
|
||||||
|
///
|
||||||
|
/// This encoding preserves chunk boundaries by framing each chunk with its length, using Websocket
|
||||||
|
/// Framing.
|
||||||
|
pub type ChunkedByteStream = Streaming<Bytes, CborEncoding>;
|
||||||
|
|
||||||
|
/// A stream of manually chunked text data.
|
||||||
|
///
|
||||||
|
/// This encoding preserves chunk boundaries by framing each chunk with its length, using Websocket
|
||||||
|
/// Framing.
|
||||||
|
pub type ChunkedTextStream = Streaming<String, CborEncoding>;
|
||||||
|
|
||||||
/// A streaming payload.
|
/// A streaming payload.
|
||||||
///
|
///
|
||||||
|
/// ## Frames and Chunking
|
||||||
|
///
|
||||||
|
/// The streaming payload sends and receives data in discrete chunks or "frames". The size is converted
|
||||||
|
/// to hex and sent before each chunk, followed by a CRLF, the chunk data, and another CRLF.
|
||||||
|
///
|
||||||
|
/// This mimics actual HTTP chunked transfer encoding, but allows us to define our own framing
|
||||||
|
/// protocol on top of it.
|
||||||
|
///
|
||||||
|
/// Arbitrary bytes can be encoded between these frames, but the frames do come with some overhead.
|
||||||
|
///
|
||||||
/// ## Browser Support for Streaming Input
|
/// ## Browser Support for Streaming Input
|
||||||
///
|
///
|
||||||
/// Browser fetch requests do not currently support full request duplexing, which
|
/// Browser fetch requests do not currently support full request duplexing, which
|
||||||
@@ -58,9 +123,9 @@ impl<T: 'static + Send, E> Streaming<T, E> {
|
|||||||
pub fn new(value: impl Stream<Item = T> + Send + 'static) -> Self {
|
pub fn new(value: impl Stream<Item = T> + Send + 'static) -> Self {
|
||||||
// Box and pin the incoming stream and store as a trait object
|
// Box and pin the incoming stream and store as a trait object
|
||||||
Self {
|
Self {
|
||||||
output_stream: Box::pin(futures::stream::empty()) as _,
|
|
||||||
input_stream: Box::pin(value.map(|item| Ok(item)))
|
input_stream: Box::pin(value.map(|item| Ok(item)))
|
||||||
as Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>,
|
as Pin<Box<dyn Stream<Item = Result<T, StreamingError>> + Send>>,
|
||||||
|
output_stream: Box::pin(futures::stream::empty()) as _,
|
||||||
encoding: PhantomData,
|
encoding: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,9 +134,7 @@ impl<T: 'static + Send, E> Streaming<T, E> {
|
|||||||
///
|
///
|
||||||
/// The callback is provided an `UnboundedSender` that can be used to send items to the stream.
|
/// The callback is provided an `UnboundedSender` that can be used to send items to the stream.
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub fn spawn<F>(
|
pub fn spawn<F>(callback: impl FnOnce(UnboundedSender<T>) -> F + Send + 'static) -> Self
|
||||||
callback: impl FnOnce(futures_channel::mpsc::UnboundedSender<T>) -> F + Send + 'static,
|
|
||||||
) -> Self
|
|
||||||
where
|
where
|
||||||
F: Future<Output = ()> + 'static,
|
F: Future<Output = ()> + 'static,
|
||||||
T: Send,
|
T: Send,
|
||||||
@@ -92,6 +155,17 @@ impl<T: 'static + Send, E> Streaming<T, E> {
|
|||||||
pub fn into_inner(self) -> impl Stream<Item = Result<T, StreamingError>> + Send {
|
pub fn into_inner(self) -> impl Stream<Item = Result<T, StreamingError>> + Send {
|
||||||
self.input_stream
|
self.input_stream
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a streaming payload from an existing stream of bytes.
|
||||||
|
///
|
||||||
|
/// This uses the internal framing mechanism to decode the stream into items of type `T`.
|
||||||
|
fn from_bytes(stream: impl Stream<Item = Result<T, StreamingError>> + Send + 'static) -> Self {
|
||||||
|
Self {
|
||||||
|
input_stream: Box::pin(stream),
|
||||||
|
output_stream: Box::pin(futures::stream::empty()) as _,
|
||||||
|
encoding: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S, U> From<S> for TextStream
|
impl<S, U> From<S> for TextStream
|
||||||
@@ -100,11 +174,7 @@ where
|
|||||||
U: Into<String>,
|
U: Into<String>,
|
||||||
{
|
{
|
||||||
fn from(value: S) -> Self {
|
fn from(value: S) -> Self {
|
||||||
Self {
|
Self::new(value.map(|data| data.into()))
|
||||||
input_stream: Box::pin(value.map(|data| Ok(data.into()))),
|
|
||||||
output_stream: Box::pin(futures::stream::empty()) as _,
|
|
||||||
encoding: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,11 +199,7 @@ where
|
|||||||
E: Encoding,
|
E: Encoding,
|
||||||
{
|
{
|
||||||
fn from(value: S) -> Self {
|
fn from(value: S) -> Self {
|
||||||
Self {
|
Self::from_bytes(value.map(|data| Ok(data.into())))
|
||||||
input_stream: Box::pin(value.map(|data| Ok(data.into()))),
|
|
||||||
output_stream: Box::pin(futures::stream::empty()) as _,
|
|
||||||
encoding: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +224,7 @@ impl IntoResponse for Streaming<Bytes> {
|
|||||||
impl<T: DeserializeOwned + Serialize + 'static, E: Encoding> IntoResponse for Streaming<T, E> {
|
impl<T: DeserializeOwned + Serialize + 'static, E: Encoding> IntoResponse for Streaming<T, E> {
|
||||||
fn into_response(self) -> axum_core::response::Response {
|
fn into_response(self) -> axum_core::response::Response {
|
||||||
let res = self.input_stream.map(|r| match r {
|
let res = self.input_stream.map(|r| match r {
|
||||||
Ok(res) => match E::to_bytes(&res) {
|
Ok(res) => match encode_stream_frame::<T, E>(res) {
|
||||||
Some(bytes) => Ok(bytes),
|
Some(bytes) => Ok(bytes),
|
||||||
None => Err(StreamingError::Failed),
|
None => Err(StreamingError::Failed),
|
||||||
},
|
},
|
||||||
@@ -218,7 +284,7 @@ impl<T: DeserializeOwned + Serialize + 'static + Send, E: Encoding> FromResponse
|
|||||||
SendWrapper::new(async move {
|
SendWrapper::new(async move {
|
||||||
let client_stream = Box::pin(SendWrapper::new(res.bytes_stream().map(
|
let client_stream = Box::pin(SendWrapper::new(res.bytes_stream().map(
|
||||||
|byte| match byte {
|
|byte| match byte {
|
||||||
Ok(bytes) => match E::from_bytes(bytes) {
|
Ok(bytes) => match decode_stream_frame::<T, E>(bytes) {
|
||||||
Some(res) => Ok(res),
|
Some(res) => Ok(res),
|
||||||
None => Err(StreamingError::Decoding),
|
None => Err(StreamingError::Decoding),
|
||||||
},
|
},
|
||||||
@@ -330,7 +396,7 @@ impl<T: DeserializeOwned + Serialize + 'static + Send, E: Encoding, S> FromReque
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
input_stream: Box::pin(futures::stream::empty()),
|
input_stream: Box::pin(futures::stream::empty()),
|
||||||
output_stream: Box::pin(stream.map(|byte| match byte {
|
output_stream: Box::pin(stream.map(|byte| match byte {
|
||||||
Ok(bytes) => match E::from_bytes(bytes) {
|
Ok(bytes) => match decode_stream_frame::<T, E>(bytes) {
|
||||||
Some(res) => Ok(res),
|
Some(res) => Ok(res),
|
||||||
None => Err(StreamingError::Decoding),
|
None => Err(StreamingError::Decoding),
|
||||||
},
|
},
|
||||||
@@ -380,11 +446,11 @@ impl<T: DeserializeOwned + Serialize + 'static + Send, E: Encoding> IntoRequest
|
|||||||
async move {
|
async move {
|
||||||
builder
|
builder
|
||||||
.header("Content-Type", E::stream_content_type())?
|
.header("Content-Type", E::stream_content_type())?
|
||||||
.send_body_stream(
|
.send_body_stream(self.input_stream.map(|r| {
|
||||||
self.input_stream.map(|r| {
|
r.and_then(|item| {
|
||||||
r.and_then(|item| E::to_bytes(&item).ok_or(StreamingError::Failed))
|
encode_stream_frame::<T, E>(item).ok_or(StreamingError::Failed)
|
||||||
}),
|
})
|
||||||
)
|
}))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,3 +469,113 @@ impl<T, E: Encoding> std::fmt::Debug for Streaming<T, E> {
|
|||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This function encodes a single frame of a streaming payload using the specified encoding.
|
||||||
|
///
|
||||||
|
/// The resulting `Bytes` object is encoded as a websocket frame, so you can send it over a streaming
|
||||||
|
/// HTTP response or even a websocket connection.
|
||||||
|
///
|
||||||
|
/// Note that the packet is not masked, as it is assumed to be sent over a trusted connection.
|
||||||
|
pub fn encode_stream_frame<T: Serialize, E: Encoding>(data: T) -> Option<Bytes> {
|
||||||
|
// We use full advantage of `BytesMut` here, writing a maximally full frame and then shrinking it
|
||||||
|
// down to size at the end.
|
||||||
|
//
|
||||||
|
// Also note we don't do any masking over this data since it's not going over an untrusted
|
||||||
|
// network like a websocket would.
|
||||||
|
//
|
||||||
|
// We allocate 10 extra bytes to account for framing overhead, which we'll shrink after
|
||||||
|
let mut bytes = vec![0u8; 10];
|
||||||
|
|
||||||
|
E::encode(data, &mut bytes)?;
|
||||||
|
|
||||||
|
let len = (bytes.len() - 10) as u64;
|
||||||
|
let opcode = 0x82; // FIN + binary opcode
|
||||||
|
|
||||||
|
// Write the header directly into the allocated space.
|
||||||
|
let offset = if len <= 125 {
|
||||||
|
bytes[8] = opcode;
|
||||||
|
bytes[9] = len as u8;
|
||||||
|
8
|
||||||
|
} else if len <= u16::MAX as u64 {
|
||||||
|
bytes[6] = opcode;
|
||||||
|
bytes[7] = 126;
|
||||||
|
let len_bytes = (len as u16).to_be_bytes();
|
||||||
|
bytes[8] = len_bytes[0];
|
||||||
|
bytes[9] = len_bytes[1];
|
||||||
|
6
|
||||||
|
} else {
|
||||||
|
bytes[0] = opcode;
|
||||||
|
bytes[1] = 127;
|
||||||
|
bytes[2..10].copy_from_slice(&len.to_be_bytes());
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shrink down to the actual used size - is zero copy!
|
||||||
|
Some(Bytes::from(bytes).slice(offset..))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a websocket-framed streaming payload produced by [`encode_stream_frame`].
|
||||||
|
///
|
||||||
|
/// This function returns `None` if the frame is invalid or cannot be decoded.
|
||||||
|
///
|
||||||
|
/// It cannot handle masked frames, as those are not produced by our encoding function.
|
||||||
|
pub fn decode_stream_frame<T, E>(frame: Bytes) -> Option<T>
|
||||||
|
where
|
||||||
|
E: Encoding,
|
||||||
|
T: DeserializeOwned,
|
||||||
|
{
|
||||||
|
let data = frame.as_ref();
|
||||||
|
|
||||||
|
if data.len() < 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let first = data[0];
|
||||||
|
let second = data[1];
|
||||||
|
|
||||||
|
// Require FIN with binary opcode and no RSV bits
|
||||||
|
let fin = first & 0x80 != 0;
|
||||||
|
let opcode = first & 0x0F;
|
||||||
|
let rsv = first & 0x70;
|
||||||
|
if !fin || opcode != 0x02 || rsv != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mask bit must be zero for our framing
|
||||||
|
if second & 0x80 != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut offset = 2usize;
|
||||||
|
let mut payload_len = (second & 0x7F) as usize;
|
||||||
|
|
||||||
|
if payload_len == 126 {
|
||||||
|
if data.len() < offset + 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
payload_len = u16::from_be_bytes([data[offset], data[offset + 1]]) as usize;
|
||||||
|
offset += 2;
|
||||||
|
} else if payload_len == 127 {
|
||||||
|
if data.len() < offset + 8 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut len_bytes = [0u8; 8];
|
||||||
|
len_bytes.copy_from_slice(&data[offset..offset + 8]);
|
||||||
|
let len_u64 = u64::from_be_bytes(len_bytes);
|
||||||
|
|
||||||
|
if len_u64 > usize::MAX as u64 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
payload_len = len_u64 as usize;
|
||||||
|
offset += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.len() < offset + payload_len {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
E::decode(frame.slice(offset..offset + payload_len))
|
||||||
|
}
|
||||||
@@ -307,11 +307,11 @@ impl<I, O, E> Websocket<I, O, E> {
|
|||||||
match msg {
|
match msg {
|
||||||
Message::Text(text) => {
|
Message::Text(text) => {
|
||||||
let e: O =
|
let e: O =
|
||||||
E::from_bytes(text.into()).ok_or_else(WebsocketError::deserialization)?;
|
E::decode(text.into()).ok_or_else(WebsocketError::deserialization)?;
|
||||||
return Ok(e);
|
return Ok(e);
|
||||||
}
|
}
|
||||||
Message::Binary(bytes) => {
|
Message::Binary(bytes) => {
|
||||||
let e: O = E::from_bytes(bytes).ok_or_else(WebsocketError::deserialization)?;
|
let e: O = E::decode(bytes).ok_or_else(WebsocketError::deserialization)?;
|
||||||
return Ok(e);
|
return Ok(e);
|
||||||
}
|
}
|
||||||
Message::Close { code, reason } => {
|
Message::Close { code, reason } => {
|
||||||
@@ -707,13 +707,12 @@ impl<In: DeserializeOwned, Out: Serialize, E: Encoding> TypedWebsocket<In, Out,
|
|||||||
match res {
|
match res {
|
||||||
Ok(res) => match res {
|
Ok(res) => match res {
|
||||||
AxumMessage::Text(utf8_bytes) => {
|
AxumMessage::Text(utf8_bytes) => {
|
||||||
let e: In = E::from_bytes(utf8_bytes.into())
|
let e: In = E::decode(utf8_bytes.into())
|
||||||
.ok_or_else(WebsocketError::deserialization)?;
|
.ok_or_else(WebsocketError::deserialization)?;
|
||||||
return Ok(e);
|
return Ok(e);
|
||||||
}
|
}
|
||||||
AxumMessage::Binary(bytes) => {
|
AxumMessage::Binary(bytes) => {
|
||||||
let e: In =
|
let e: In = E::decode(bytes).ok_or_else(WebsocketError::deserialization)?;
|
||||||
E::from_bytes(bytes).ok_or_else(WebsocketError::deserialization)?;
|
|
||||||
return Ok(e);
|
return Ok(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ pub trait IntoRequest<R = ClientResponse>: Sized {
|
|||||||
|
|
||||||
impl<A, R> IntoRequest<R> for (A,)
|
impl<A, R> IntoRequest<R> for (A,)
|
||||||
where
|
where
|
||||||
A: IntoRequest<R> + 'static,
|
A: IntoRequest<R> + 'static + Send,
|
||||||
{
|
{
|
||||||
fn into_request(
|
fn into_request(
|
||||||
self,
|
self,
|
||||||
@@ -157,9 +157,20 @@ impl<T, E> AssertIsResult for Result<T, E> {}
|
|||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn assert_is_result<T: AssertIsResult>() {}
|
pub fn assert_is_result<T: AssertIsResult>() {}
|
||||||
|
|
||||||
#[diagnostic::on_unimplemented(
|
#[diagnostic::on_unimplemented(message = r#"❌ Invalid Arguments to ServerFn ❌
|
||||||
message = "The arguments to the server function must either be a single `impl FromRequest + IntoRequest` argument, or multiple `DeserializeOwned` arguments."
|
|
||||||
)]
|
The arguments to the server function must be either:
|
||||||
|
|
||||||
|
- a single `impl FromRequest + IntoRequest` argument
|
||||||
|
- or multiple `DeserializeOwned` arguments.
|
||||||
|
|
||||||
|
Did you forget to implement `IntoRequest` or `Deserialize` for one of the arguments?
|
||||||
|
|
||||||
|
`IntoRequest` is a trait that allows payloads to be sent to the server function.
|
||||||
|
|
||||||
|
> See https://dioxuslabs.com/learn/0.7/essentials/fullstack/server_functions for more details.
|
||||||
|
|
||||||
|
"#)]
|
||||||
pub trait AssertCanEncode {}
|
pub trait AssertCanEncode {}
|
||||||
|
|
||||||
pub struct CantEncode;
|
pub struct CantEncode;
|
||||||
@@ -167,5 +178,27 @@ pub struct CantEncode;
|
|||||||
pub struct EncodeIsVerified;
|
pub struct EncodeIsVerified;
|
||||||
impl AssertCanEncode for EncodeIsVerified {}
|
impl AssertCanEncode for EncodeIsVerified {}
|
||||||
|
|
||||||
|
#[diagnostic::on_unimplemented(message = r#"❌ Invalid return type from ServerFn ❌
|
||||||
|
|
||||||
|
The arguments to the server function must be either:
|
||||||
|
|
||||||
|
- a single `impl FromResponse` return type
|
||||||
|
- a single `impl Serialize + DeserializedOwned` return type
|
||||||
|
|
||||||
|
Did you forget to implement `FromResponse` or `DeserializeOwned` for one of the arguments?
|
||||||
|
|
||||||
|
`FromResponse` is a trait that allows payloads to be decoded from the server function response.
|
||||||
|
|
||||||
|
> See https://dioxuslabs.com/learn/0.7/essentials/fullstack/server_functions for more details.
|
||||||
|
|
||||||
|
"#)]
|
||||||
|
pub trait AssertCanDecode {}
|
||||||
|
pub struct CantDecode;
|
||||||
|
pub struct DecodeIsVerified;
|
||||||
|
impl AssertCanDecode for DecodeIsVerified {}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn assert_can_encode(_t: impl AssertCanEncode) {}
|
pub fn assert_can_encode(_t: impl AssertCanEncode) {}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn assert_can_decode(_t: impl AssertCanDecode) {}
|
||||||
Reference in new issue
Block a user