redirects, middleware, Transport, set/capture error status (#4751)

* wip: redirect

* redirect example

* fix file upload not in a form

* wip

* deny missing cfgs

* wip: add router fallback but it's not working

* fix suspense bug after rebuilds that queued work

* more cleanup

* add test for error codes

* add helpful comment

* bring back middleware annotation

* fix cfg

* add server feature

* fix doctest

* just disable liveview test for now

* fix liveview

* dont send init

* cache more playwright!

* add more pkgs to cache
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-10-10 21:43:57 -07:00
1 parent 8a81475ba8
commit 9243ef4128
35 files changed
+935 -800

No files matched your search

+17 -51
View File
@@ -1,6 +1,5 @@
use crate::transport::SerializeContextEntry;
use crate::{transport::SerializeContextEntry, Transportable};
use dioxus_core::use_hook;
use serde::{de::DeserializeOwned, Serialize};
/// This allows you to send data from the server to the client *during hydration*.
/// - When compiled as server, the closure is ran and the resulting data is serialized on the server and sent to the client.
@@ -24,26 +23,35 @@ use serde::{de::DeserializeOwned, Serialize};
/// }
/// ```
#[track_caller]
pub fn use_server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
server_fn: impl Fn() -> O,
) -> O {
pub fn use_server_cached<O, M>(server_fn: impl Fn() -> O) -> O
where
O: Transportable<M> + Clone,
M: 'static,
{
let location = std::panic::Location::caller();
use_hook(|| server_cached(server_fn, location))
}
pub(crate) fn server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
pub(crate) fn server_cached<O, M>(
value: impl FnOnce() -> O,
#[allow(unused)] location: &'static std::panic::Location<'static>,
) -> O {
let serialize = crate::serialize_context();
) -> O
where
O: Transportable<M> + Clone,
M: 'static,
{
let serialize = crate::transport::serialize_context();
#[allow(unused)]
let entry: SerializeContextEntry<O> = serialize.create_entry();
#[cfg(feature = "server")]
{
let data = value();
entry.insert(&data, location);
data
}
#[cfg(all(not(feature = "server"), feature = "web"))]
{
match entry.get() {
@@ -51,51 +59,9 @@ pub(crate) fn server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
Err(_) => value(),
}
}
#[cfg(not(any(feature = "server", feature = "web")))]
{
value()
}
}
// pub fn use_server_cached<O, M>(server_fn: impl Fn() -> O) -> O
// where
// O: Transportable<M> + Clone,
// M: 'static,
// {
// let location = std::panic::Location::caller();
// use_hook(|| server_cached(server_fn, location))
// }
// pub(crate) fn server_cached<O, M>(
// value: impl FnOnce() -> O,
// #[allow(unused)] location: &'static std::panic::Location<'static>,
// ) -> O
// where
// O: Transportable<M> + Clone,
// M: 'static,
// {
// let serialize = crate::transport::serialize_context();
// #[allow(unused)]
// let entry: SerializeContextEntry<O> = serialize.create_entry();
// #[cfg(feature = "server")]
// {
// let data = value();
// entry.insert(&data, location);
// data
// }
// #[cfg(all(not(feature = "server"), feature = "web"))]
// {
// match entry.get() {
// Ok(value) => value,
// Err(_) => value(),
// }
// }
// #[cfg(not(any(feature = "server", feature = "web")))]
// {
// value()
// }
// }
+13 -304
View File
@@ -1,15 +1,13 @@
use crate::Transportable;
use dioxus_core::{suspend, use_hook, RenderError};
use dioxus_hooks::*;
use dioxus_signals::ReadableExt;
use serde::{de::DeserializeOwned, Serialize};
use std::future::Future;
/// Runs a future and returns a result with a resource if the future is finished or a suspended error if it is still running. The future may run on the server *during hydration*.
/// - When compiled as server, the closure's future is ran to completion and the resulting data is serialized on the server and sent to the client.
/// - When compiled as web client, the data is deserialized from the server if already available, otherwise runs on the client. Data is usually only available if this hook exists in a component during hydration.
/// - When otherwise compiled, the closure is run directly with no serialization.
/// Runs a future with a manual list of dependencies and returns a resource with the result if the future is finished or a suspended error if it is still running.
///
/// On the server, this will wait until the future is resolved before continuing to render. Thus, this blocks other subsequent server hooks. The result is cached.
///
/// On the server, this will wait until the future is resolved before continuing to render. When the future is resolved, the result will be serialized into the page and hydrated on the client without rerunning the future.
///
///
/// <div class="warning">
@@ -59,19 +57,21 @@ use std::future::Future;
/// }
/// }
/// ```
#[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
#[track_caller]
pub fn use_server_future<T, F>(
pub fn use_server_future<T, F, M>(
mut future: impl FnMut() -> F + 'static,
) -> Result<Resource<T>, RenderError>
where
T: Serialize + DeserializeOwned + 'static,
F: Future<Output = T> + 'static,
T: Transportable<M>,
M: 'static,
{
let serialize_context = use_hook(crate::serialize_context);
let serialize_context = use_hook(crate::transport::serialize_context);
// We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client
#[allow(unused)]
let storage_entry: crate::SerializeContextEntry<T> =
let storage_entry: crate::transport::SerializeContextEntry<T> =
use_hook(|| serialize_context.create_entry());
#[cfg(feature = "server")]
@@ -100,7 +100,9 @@ where
Some(Ok(o)) => return o,
// The data is still pending from the server. Don't try to resolve it on the client
Some(Err(crate::TakeDataError::DataPending)) => std::future::pending::<()>().await,
Some(Err(crate::transport::TakeDataError::DataPending)) => {
std::future::pending::<()>().await
}
// The data was not available on the server, rerun the future
Some(Err(_)) => {}
@@ -135,296 +137,3 @@ where
Ok(resource)
}
// use dioxus_core::{suspend, use_hook, RenderError};
// use dioxus_hooks::*;
// use dioxus_signals::ReadableExt;
// use serde::{de::DeserializeOwned, Serialize};
// use std::future::Future;
// /// Runs a future with a manual list of dependencies and returns a resource with the result if the future is finished or a suspended error if it is still running.
// ///
// ///
// /// On the server, this will wait until the future is resolved before continuing to render. When the future is resolved, the result will be serialized into the page and hydrated on the client without rerunning the future.
// ///
// ///
// /// <div class="warning">
// ///
// /// Unlike [`use_resource`] dependencies are only tracked inside the function that spawns the async block, not the async block itself.
// ///
// /// ```rust, no_run
// /// # use dioxus::prelude::*;
// /// // ❌ The future inside of use_server_future is not reactive
// /// let id = use_signal(|| 0);
// /// use_server_future(move || {
// /// async move {
// /// // But the future is not reactive which means that the future will not subscribe to any reads here
// /// println!("{id}");
// /// }
// /// });
// /// // ✅ The closure that creates the future for use_server_future is reactive
// /// let id = use_signal(|| 0);
// /// use_server_future(move || {
// /// // The closure itself is reactive which means the future will subscribe to any signals you read here
// /// let cloned_id = id();
// /// async move {
// /// // But the future is not reactive which means that the future will not subscribe to any reads here
// /// println!("{cloned_id}");
// /// }
// /// });
// /// ```
// ///
// /// </div>
// ///
// /// # Example
// ///
// /// ```rust, no_run
// /// # use dioxus::prelude::*;
// /// # async fn fetch_article(id: u32) -> String { unimplemented!() }
// /// use dioxus::prelude::*;
// ///
// /// fn App() -> Element {
// /// let mut article_id = use_signal(|| 0);
// /// // `use_server_future` will spawn a task that runs on the server and serializes the result to send to the client.
// /// // The future will rerun any time the
// /// // Since we bubble up the suspense with `?`, the server will wait for the future to resolve before rendering
// /// let article = use_server_future(move || fetch_article(article_id()))?;
// ///
// /// rsx! {
// /// "{article().unwrap()}"
// /// }
// /// }
// /// ```
// #[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
// #[track_caller]
// pub fn use_server_future<T, F>(
// mut future: impl FnMut() -> F + 'static,
// ) -> Result<Resource<T>, RenderError>
// where
// T: Serialize + DeserializeOwned + 'static,
// F: Future<Output = T> + 'static,
// {
// let serialize_context = use_hook(crate::serialize_context);
// // We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client
// #[allow(unused)]
// let storage_entry: crate::SerializeContextEntry<T> =
// use_hook(|| serialize_context.create_entry());
// #[cfg(feature = "server")]
// let caller = std::panic::Location::caller();
// // If this is the first run and we are on the web client, the data might be cached
// #[cfg(feature = "web")]
// let initial_web_result =
// use_hook(|| std::rc::Rc::new(std::cell::RefCell::new(Some(storage_entry.get()))));
// let resource = use_resource(move || {
// #[cfg(feature = "server")]
// let storage_entry = storage_entry.clone();
// let user_fut = future();
// #[cfg(feature = "web")]
// let initial_web_result = initial_web_result.clone();
// #[allow(clippy::let_and_return)]
// async move {
// // If this is the first run and we are on the web client, the data might be cached
// #[cfg(feature = "web")]
// match initial_web_result.take() {
// // The data was deserialized successfully from the server
// Some(Ok(o)) => return o,
// // The data is still pending from the server. Don't try to resolve it on the client
// Some(Err(crate::TakeDataError::DataPending)) => std::future::pending::<()>().await,
// // The data was not available on the server, rerun the future
// Some(Err(_)) => {}
// // This isn't the first run, so we don't need do anything
// None => {}
// }
// // Otherwise just run the future itself
// let out = user_fut.await;
// // If this is the first run and we are on the server, cache the data in the slot we reserved for it
// #[cfg(feature = "server")]
// storage_entry.insert(&out, caller);
// out
// }
// });
// // On the first run, force this task to be polled right away in case its value is ready
// use_hook(|| {
// let _ = resource.task().poll_now();
// });
// // Suspend if the value isn't ready
// if resource.state().cloned() == UseResourceState::Pending {
// let task = resource.task();
// if !task.paused() {
// return Err(suspend(task).unwrap_err());
// }
// }
// Ok(resource)
// }
// // use dioxus_core::{suspend, use_hook, RenderError};
// // use dioxus_hooks::*;
// // use dioxus_signals::ReadableExt;
// // use std::future::Future;
// // // use crate::Transportable;
// // /// Runs a future with a manual list of dependencies and returns a resource with the result if the future is finished or a suspended error if it is still running.
// // ///
// // ///
// // /// On the server, this will wait until the future is resolved before continuing to render. When the future is resolved, the result will be serialized into the page and hydrated on the client without rerunning the future.
// // ///
// // ///
// // /// <div class="warning">
// // ///
// // /// Unlike [`use_resource`] dependencies are only tracked inside the function that spawns the async block, not the async block itself.
// // ///
// // /// ```rust, no_run
// // /// # use dioxus::prelude::*;
// // /// // ❌ The future inside of use_server_future is not reactive
// // /// let id = use_signal(|| 0);
// // /// use_server_future(move || {
// // /// async move {
// // /// // But the future is not reactive which means that the future will not subscribe to any reads here
// // /// println!("{id}");
// // /// }
// // /// });
// // /// // ✅ The closure that creates the future for use_server_future is reactive
// // /// let id = use_signal(|| 0);
// // /// use_server_future(move || {
// // /// // The closure itself is reactive which means the future will subscribe to any signals you read here
// // /// let cloned_id = id();
// // /// async move {
// // /// // But the future is not reactive which means that the future will not subscribe to any reads here
// // /// println!("{cloned_id}");
// // /// }
// // /// });
// // /// ```
// // ///
// // /// </div>
// // ///
// // /// # Example
// // ///
// // /// ```rust, no_run
// // /// # use dioxus::prelude::*;
// // /// # async fn fetch_article(id: u32) -> String { unimplemented!() }
// // /// use dioxus::prelude::*;
// // ///
// // /// fn App() -> Element {
// // /// let mut article_id = use_signal(|| 0);
// // /// // `use_server_future` will spawn a task that runs on the server and serializes the result to send to the client.
// // /// // The future will rerun any time the
// // /// // Since we bubble up the suspense with `?`, the server will wait for the future to resolve before rendering
// // /// let article = use_server_future(move || fetch_article(article_id()))?;
// // ///
// // /// rsx! {
// // /// "{article().unwrap()}"
// // /// }
// // /// }
// // /// ```
// // #[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
// // #[track_caller]
// // pub fn use_server_future<T, F, M>(
// // future: impl FnMut() -> F + 'static,
// // ) -> Result<Resource<T>, RenderError>
// // where
// // F: Future<Output = T> + 'static,
// // T: Transportable<M>,
// // M: 'static,
// // {
// // let resource = use_server_future_unsuspended(future);
// // // Suspend if the value isn't ready
// // if resource.state().cloned() == UseResourceState::Pending {
// // let task = resource.task();
// // if !task.paused() {
// // return Err(suspend(task).unwrap_err());
// // }
// // }
// // Ok(resource)
// // }
// // /// Like [`use_server_future`] but does not suspend if the future is still running.
// // ///
// // /// You need to manage suspending yourself by checking the resource state.
// // pub fn use_server_future_unsuspended<T, F, M>(
// // mut future: impl FnMut() -> F + 'static,
// // ) -> Resource<T>
// // where
// // F: Future<Output = T> + 'static,
// // T: Transportable<M>,
// // M: 'static,
// // {
// // let serialize_context = use_hook(crate::transport::serialize_context);
// // // We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client
// // #[allow(unused)]
// // let storage_entry: crate::transport::SerializeContextEntry<T> =
// // use_hook(|| serialize_context.create_entry());
// // #[cfg(feature = "server")]
// // let caller = std::panic::Location::caller();
// // // If this is the first run and we are on the web client, the data might be cached
// // #[cfg(feature = "web")]
// // let initial_web_result =
// // use_hook(|| std::rc::Rc::new(std::cell::RefCell::new(Some(storage_entry.get()))));
// // let resource = use_resource(move || {
// // #[cfg(feature = "server")]
// // let storage_entry = storage_entry.clone();
// // let user_fut = future();
// // #[cfg(feature = "web")]
// // let initial_web_result = initial_web_result.clone();
// // #[allow(clippy::let_and_return)]
// // async move {
// // // If this is the first run and we are on the web client, the data might be cached
// // #[cfg(feature = "web")]
// // match initial_web_result.take() {
// // // The data was deserialized successfully from the server
// // Some(Ok(o)) => return o,
// // // The data is still pending from the server. Don't try to resolve it on the client
// // Some(Err(crate::transport::TakeDataError::DataPending)) => {
// // std::future::pending::<()>().await
// // }
// // // The data was not available on the server, rerun the future
// // Some(Err(_)) => {}
// // // This isn't the first run, so we don't need do anything
// // None => {}
// // }
// // // Otherwise just run the future itself
// // let out = user_fut.await;
// // // If this is the first run and we are on the server, cache the data in the slot we reserved for it
// // #[cfg(feature = "server")]
// // storage_entry.insert(&out, caller);
// // out
// // }
// // });
// // // On the first run, force this task to be polled right away in case its value is ready
// // use_hook(|| {
// // let _ = resource.task().poll_now();
// // });
// // resource
// // }
+94 -24
View File
@@ -1,42 +1,39 @@
use crate::ServerFnError;
use crate::{HttpError, ServerFnError};
use axum_core::{extract::FromRequest, response::IntoResponse};
use dioxus_core::try_consume_context;
use dioxus_core::{try_consume_context, CapturedError};
use dioxus_signals::{ReadableExt, Signal, WritableExt};
use http::request::Parts;
use http::StatusCode;
use std::{cell::RefCell, rc::Rc};
/// The status of the streaming response
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum StreamingStatus {
/// The initial chunk is still being rendered. The http response parts can still be modified at this point.
RenderingInitialChunk,
/// The initial chunk has been committed and the response is now streaming. The http response parts
/// have already been sent to the client and can no longer be modified.
InitialChunkCommitted,
}
/// The context dioxus fullstack provides for the status of streaming responses on the server
/// The context provided by dioxus fullstack for server-side rendering.
///
/// This context will only be set on the server during a streaming response.
#[derive(Clone, Debug)]
pub struct StreamingContext {
pub struct FullstackContext {
current_status: Signal<StreamingStatus>,
request_headers: Rc<RefCell<http::request::Parts>>,
route_http_status: Signal<HttpError>,
}
impl PartialEq for StreamingContext {
impl PartialEq for FullstackContext {
fn eq(&self, other: &Self) -> bool {
self.current_status == other.current_status
&& Rc::ptr_eq(&self.request_headers, &other.request_headers)
}
}
impl StreamingContext {
impl FullstackContext {
/// Create a new streaming context. You should not need to call this directly. Dioxus fullstack will
/// provide this context for you.
pub fn new(parts: Parts) -> Self {
Self {
current_status: Signal::new(StreamingStatus::RenderingInitialChunk),
request_headers: Rc::new(RefCell::new(parts)),
route_http_status: Signal::new(HttpError {
status: http::StatusCode::OK,
message: None,
}),
}
}
@@ -52,7 +49,7 @@ impl StreamingContext {
/// Get the current status of the streaming response. This method is reactive and will cause
/// the current reactive context to rerun when the status changes.
pub fn current_status(&self) -> StreamingStatus {
pub fn streaming_state(&self) -> StreamingStatus {
*self.current_status.read()
}
@@ -65,7 +62,7 @@ impl StreamingContext {
/// since it's assumed that rendering the app is done under a `GET` request.
pub async fn extract<T: FromRequest<(), M>, M>() -> Result<T, ServerFnError> {
let this = Self::current()
.ok_or_else(|| ServerFnError::new("No StreamingContext found".to_string()))?;
.ok_or_else(|| ServerFnError::new("No FullstackContext found".to_string()))?;
let parts = this.request_headers.borrow_mut().clone();
let request =
@@ -79,18 +76,66 @@ impl StreamingContext {
}
}
/// Get the current `StreamingContext` if it exists. This will return `None` if called on the client
/// Get the current `FullstackContext` if it exists. This will return `None` if called on the client
/// or outside of a streaming response on the server.
pub fn current() -> Option<Self> {
if let Some(rt) = dioxus_core::Runtime::try_current() {
let id = rt.try_current_scope_id()?;
if let Some(ctx) = rt.consume_context::<StreamingContext>(id) {
if let Some(ctx) = rt.consume_context::<FullstackContext>(id) {
return Some(ctx);
}
}
None
}
/// Get the current HTTP status for the route. This will default to 200 OK, but can be modified
/// by calling `FullstackContext::commit_error_status` with an error.
pub fn current_http_status(&self) -> HttpError {
self.route_http_status.read().clone()
}
pub fn set_current_http_status(&mut self, status: HttpError) {
self.route_http_status.set(status);
}
/// Set the current HTTP status for the route. This will be used when committing the response
/// to the client.
pub fn commit_http_status(status: StatusCode, message: Option<String>) {
if let Some(mut ctx) = Self::current() {
ctx.set_current_http_status(HttpError { status, message });
}
}
/// Commit the CapturedError as the current HTTP status for the route.
/// This will attempt to downcast the error to known types and set the appropriate
/// status code. If the error type is unknown, it will default to
/// `StatusCode::INTERNAL_SERVER_ERROR`.
pub fn commit_error_status(error: impl Into<CapturedError>) -> HttpError {
let error = error.into();
let status = status_code_from_error(&error);
let http_error = HttpError {
status,
message: Some(error.to_string()),
};
if let Some(mut ctx) = Self::current() {
ctx.set_current_http_status(http_error.clone());
}
http_error
}
}
/// The status of the streaming response
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum StreamingStatus {
/// The initial chunk is still being rendered. The http response parts can still be modified at this point.
RenderingInitialChunk,
/// The initial chunk has been committed and the response is now streaming. The http response parts
/// have already been sent to the client and can no longer be modified.
InitialChunkCommitted,
}
/// Commit the initial chunk of the response. This will be called automatically if you are using the
@@ -113,7 +158,7 @@ impl StreamingContext {
/// ```
pub fn commit_initial_chunk() {
crate::history::finalize_route();
if let Some(mut streaming) = try_consume_context::<StreamingContext>() {
if let Some(mut streaming) = try_consume_context::<FullstackContext>() {
streaming.commit_initial_chunk();
}
}
@@ -141,9 +186,34 @@ pub fn commit_initial_chunk() {
/// }
/// ```
pub fn current_status() -> StreamingStatus {
if let Some(streaming) = try_consume_context::<StreamingContext>() {
streaming.current_status()
if let Some(streaming) = try_consume_context::<FullstackContext>() {
streaming.streaming_state()
} else {
StreamingStatus::InitialChunkCommitted
}
}
/// Convert a `CapturedError` into an appropriate HTTP status code.
///
/// This will attempt to downcast the error to known types and return a corresponding status code.
/// If the error type is unknown, it will default to `StatusCode::INTERNAL_SERVER_ERROR`.
pub fn status_code_from_error(error: &CapturedError) -> StatusCode {
if let Some(err) = error.downcast_ref::<ServerFnError>() {
match err {
ServerFnError::ServerError {
code: Some(code), ..
} => return StatusCode::from_u16(*code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
_ => return StatusCode::INTERNAL_SERVER_ERROR,
}
}
if let Some(err) = error.downcast_ref::<StatusCode>() {
return *err;
}
if let Some(err) = error.downcast_ref::<HttpError>() {
return err.status;
}
StatusCode::INTERNAL_SERVER_ERROR
}
+103 -142
View File
@@ -3,12 +3,8 @@
use base64::Engine;
use dioxus_core::CapturedError;
use serde::Serialize;
use std::{cell::RefCell, io::Cursor, rc::Rc};
// use base64::Engine;
// use dioxus_core::{CapturedError, Error};
// use serde::{de::DeserializeOwned, Deserialize, Serialize};
// use std::{cell::RefCell, io::Cursor, rc::Rc, sync::Arc};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{cell::RefCell, io::Cursor, rc::Rc, sync::Arc};
#[cfg(feature = "web")]
thread_local! {
@@ -88,8 +84,7 @@ impl HydrationContext {
})
}
// pub(crate) fn insert<T: Transportable<M>, M: 'static>(
pub(crate) fn insert<T: Serialize>(
pub(crate) fn insert<T: Transportable<M>, M: 'static>(
&self,
id: usize,
value: &T,
@@ -98,8 +93,7 @@ impl HydrationContext {
self.data.borrow_mut().insert(id, value, location);
}
// pub(crate) fn get<T: Transportable<M>, M: 'static>(
pub(crate) fn get<T: serde::de::DeserializeOwned>(
pub(crate) fn get<T: Transportable<M>, M: 'static>(
&self,
id: usize,
) -> Result<T, TakeDataError> {
@@ -134,35 +128,20 @@ impl<T> Clone for SerializeContextEntry<T> {
impl<T> SerializeContextEntry<T> {
/// Insert data into an entry that was created with [`HydrationContext::create_entry`]
pub fn insert(&self, value: &T, location: &'static std::panic::Location<'static>)
pub fn insert<M: 'static>(self, value: &T, location: &'static std::panic::Location<'static>)
where
T: Serialize,
T: Transportable<M>,
{
self.context.insert(self.index, value, location);
}
/// Grab the data from the serialize context
pub fn get(&self) -> Result<T, TakeDataError>
pub fn get<M: 'static>(&self) -> Result<T, TakeDataError>
where
T: serde::de::DeserializeOwned,
T: Transportable<M>,
{
self.context.get(self.index)
}
// /// Insert data into an entry that was created with [`HydrationContext::create_entry`]
// pub fn insert<M: 'static>(self, value: &T, location: &'static std::panic::Location<'static>)
// where
// T: Transportable<M>,
// {
// self.context.insert(self.index, value, location);
// }
// /// Grab the data from the serialize context
// pub fn get<M: 'static>(&self) -> Result<T, TakeDataError>
// where
// T: Transportable<M>,
// {
// self.context.get(self.index)
// }
}
/// Check if the client is currently rendering a component for hydration. Always returns true on the server.
@@ -276,16 +255,13 @@ impl HTMLData {
}
/// Insert data into an entry that was created with [`Self::create_entry`]
// fn insert<T: Transportable<M>, M: 'static>(
fn insert<T: Serialize>(
fn insert<T: Transportable<M>, M: 'static>(
&mut self,
id: usize,
value: &T,
#[allow(unused)] location: &'static std::panic::Location<'static>,
) {
// let serialized = value.transport_to_bytes();
let mut serialized = Vec::new();
ciborium::into_writer(value, &mut serialized).unwrap();
let serialized = value.transport_to_bytes();
self.data[id] = Some(serialized);
#[cfg(debug_assertions)]
{
@@ -295,8 +271,7 @@ impl HTMLData {
}
/// Get the data from the serialize context
// fn get<T: Transportable<M>, M: 'static>(&self, index: usize) -> Result<T, TakeDataError> {
fn get<T: serde::de::DeserializeOwned>(&self, index: usize) -> Result<T, TakeDataError> {
fn get<T: Transportable<M>, M: 'static>(&self, index: usize) -> Result<T, TakeDataError> {
if index >= self.data.len() {
tracing::trace!(
"Tried to take more data than was available, len: {}, index: {}; This is normal if the server function was started on the client, but may indicate a bug if the server function result should be deserialized from the server",
@@ -307,8 +282,7 @@ impl HTMLData {
}
let bytes = self.data[index].as_ref();
match bytes {
Some(bytes) => match ciborium::from_reader(Cursor::new(bytes)) {
// Some(bytes) => match T::transport_from_bytes(bytes) {
Some(bytes) => match T::transport_from_bytes(bytes) {
Ok(x) => Ok(x),
Err(err) => {
#[cfg(debug_assertions)]
@@ -453,119 +427,106 @@ pub fn head_element_hydration_entry() -> SerializeContextEntry<bool> {
serialize_context().create_entry()
}
// /// An error that can occur when trying to take data from the server
// #[derive(thiserror::Error, Debug)]
// pub enum TakeDataError {
// /// Deserializing the data failed
// #[error("DeserializationError: {0}")]
// DeserializationError(ciborium::de::Error<std::io::Error>),
/// A `Transportable` type can be safely transported from the server to the client, and be used for
/// hydration. Not all types can sensibly be transported, but many can. This trait makes it possible
/// to customize how types are transported which helps for non-serializable types like `dioxus_core::Error`.
///
/// By default, all types that implement `Serialize` and `DeserializeOwned` are transportable.
///
/// You can also implement `Transportable` for `Result<T, dioxus_core::Error>` where `T` is
/// `Serialize` and `DeserializeOwned` to allow transporting results that may contain errors.
///
/// Note that transporting a `Result<T, dioxus_core::Error>` will lose various aspects of the original
/// `dioxus_core::Error` such as backtraces and source errors, but will preserve the error message.
pub trait Transportable<M = ()>: 'static {
/// Serialize the type to a byte vector for transport
fn transport_to_bytes(&self) -> Vec<u8>;
// /// No data was available
// #[error("DataNotAvailable")]
// DataNotAvailable,
/// Deserialize the type from a byte slice
fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
where
Self: Sized;
}
// /// The server serialized a placeholder for the data, but it isn't available yet
// #[error("DataPending")]
// DataPending,
// }
impl<T> Transportable<()> for T
where
T: Serialize + DeserializeOwned + 'static,
{
fn transport_to_bytes(&self) -> Vec<u8> {
let mut serialized = Vec::new();
ciborium::into_writer(self, &mut serialized).unwrap();
serialized
}
// /// A `Transportable` type can be safely transported from the server to the client, and be used for
// /// hydration. Not all types can sensibly be transported, but many can. This trait makes it possible
// /// to customize how types are transported which helps for non-serializable types like `dioxus_core::Error`.
// ///
// /// By default, all types that implement `Serialize` and `DeserializeOwned` are transportable.
// ///
// /// You can also implement `Transportable` for `Result<T, dioxus_core::Error>` where `T` is
// /// `Serialize` and `DeserializeOwned` to allow transporting results that may contain errors.
// ///
// /// Note that transporting a `Result<T, dioxus_core::Error>` will lose various aspects of the original
// /// `dioxus_core::Error` such as backtraces and source errors, but will preserve the error message.
// pub trait Transportable<M = ()>: 'static {
// /// Serialize the type to a byte vector for transport
// fn transport_to_bytes(&self) -> Vec<u8>;
fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
where
Self: Sized,
{
ciborium::from_reader(Cursor::new(bytes))
}
}
// /// Deserialize the type from a byte slice
// fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
// where
// Self: Sized;
// }
#[derive(Serialize, Deserialize)]
struct TransportResultErr<T> {
error: Result<T, CapturedError>,
}
// impl<T> Transportable<()> for T
// where
// T: Serialize + DeserializeOwned + 'static,
// {
// fn transport_to_bytes(&self) -> Vec<u8> {
// let mut serialized = Vec::new();
// ciborium::into_writer(self, &mut serialized).unwrap();
// serialized
// }
#[doc(hidden)]
pub struct TransportViaErrMarker;
// fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
// where
// Self: Sized,
// {
// ciborium::from_reader(Cursor::new(bytes))
// }
// }
impl<T> Transportable<TransportViaErrMarker> for Result<T, dioxus_core::Error>
where
T: Serialize + DeserializeOwned + 'static,
{
fn transport_to_bytes(&self) -> Vec<u8> {
let err = TransportResultErr {
error: self
.as_ref()
.map_err(|e| CapturedError::from_display(e.to_string())),
};
// #[derive(Serialize, Deserialize)]
// struct TransportResultErr<T> {
// error: Result<T, CapturedError>,
// }
let mut serialized = Vec::new();
ciborium::into_writer(&err, &mut serialized).unwrap();
serialized
}
// #[doc(hidden)]
// pub struct TransportViaErrMarker;
fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
where
Self: Sized,
{
let err: TransportResultErr<T> = ciborium::from_reader(Cursor::new(bytes))?;
match err.error {
Ok(value) => Ok(Ok(value)),
Err(captured) => Ok(Err(dioxus_core::Error::msg(captured.to_string()))),
}
}
}
// impl<T> Transportable<TransportViaErrMarker> for Result<T, dioxus_core::Error>
// where
// T: Serialize + DeserializeOwned + 'static,
// {
// fn transport_to_bytes(&self) -> Vec<u8> {
// let err = TransportResultErr {
// error: self
// .as_ref()
// .map_err(|e| CapturedError::from_display(e.to_string())),
// };
#[doc(hidden)]
pub struct TransportCapturedError;
#[derive(Serialize, Deserialize)]
struct TransportError {
error: String,
}
// let mut serialized = Vec::new();
// ciborium::into_writer(&err, &mut serialized).unwrap();
// serialized
// }
impl Transportable<TransportCapturedError> for CapturedError {
fn transport_to_bytes(&self) -> Vec<u8> {
let err = TransportError {
error: self.to_string(),
};
// fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
// where
// Self: Sized,
// {
// let err: TransportResultErr<T> = ciborium::from_reader(Cursor::new(bytes))?;
// match err.error {
// Ok(value) => Ok(Ok(value)),
// Err(captured) => Ok(Err(dioxus_core::Error::msg(captured.to_string()))),
// }
// }
// }
let mut serialized = Vec::new();
ciborium::into_writer(&err, &mut serialized).unwrap();
serialized
}
// #[doc(hidden)]
// pub struct TransportCapturedError;
// #[derive(Serialize, Deserialize)]
// struct TransportError {
// error: String,
// }
// impl Transportable<TransportCapturedError> for CapturedError {
// fn transport_to_bytes(&self) -> Vec<u8> {
// let err = TransportError {
// error: self.to_string(),
// };
// let mut serialized = Vec::new();
// ciborium::into_writer(&err, &mut serialized).unwrap();
// serialized
// }
// fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
// where
// Self: Sized,
// {
// let err: TransportError = ciborium::from_reader(Cursor::new(bytes))?;
// Ok(CapturedError(Arc::new(Error::msg::<String>(err.error))))
// }
// }
fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
where
Self: Sized,
{
let err: TransportError = ciborium::from_reader(Cursor::new(bytes))?;
Ok(CapturedError(Arc::new(dioxus_core::Error::msg::<String>(
err.error,
))))
}
}