codify "platform" into resolution logic, server-fn/fullstack overhaul (#4596)
overhaul dioxus fullstack using a new implementation of server_fn This PR is a complete rewrite of dioxus fullstack designed to improve compatibility with axum and to fix a number of compilation errors based on feature flag unification plumbing through macros.
This commit is contained in:
1 parent
8421c3aef0
commit
f2b1c6d97f
424 files changed
+17284
-6624
No files matched your search
@@ -0,0 +1,42 @@
|
||||
[package]
|
||||
name = "dioxus-fullstack-core"
|
||||
authors = ["Jonathan Kelley", "Evan Almloff"]
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
description = "Hooks for serializing futures, values in dioxus-fullstack and other utilities"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
keywords = ["web", "gui", "server"]
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
dioxus-core = { workspace = true, features = ["serialize"]}
|
||||
dioxus-signals = { workspace = true }
|
||||
dioxus-hooks = { workspace = true }
|
||||
futures-channel = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
dioxus-history = { workspace = true }
|
||||
dioxus-document = { workspace = true }
|
||||
ciborium = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
axum-core = { workspace = true }
|
||||
http = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
inventory = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
generational-box = { workspace = true }
|
||||
futures-util = { workspace = true, features = ["std"] }
|
||||
|
||||
[features]
|
||||
web = []
|
||||
server = []
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus-fullstack = { workspace = true }
|
||||
dioxus = { workspace = true, features = ["fullstack"] }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
@@ -0,0 +1,35 @@
|
||||
# Dioxus Fullstack Core
|
||||
|
||||
Dioxus-fullstack-core provides types, traits, hooks, contexts for [`dioxus-fullstack`](https://crates.io/crates/dioxus-fullstack). Libraries that need to integrate with dioxus-fullstack should rely on this crate instead of the full-fledged renderer for quicker build times.
|
||||
|
||||
## Usage
|
||||
|
||||
To start using this crate, you can run the following command:
|
||||
|
||||
```bash
|
||||
cargo add dioxus-fullstack-hooks
|
||||
```
|
||||
|
||||
Then you can use hooks like `use_server_future` in your components:
|
||||
|
||||
```rust
|
||||
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()}"
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_article(id: u32) -> String {
|
||||
format!("Article {}", id)
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,57 @@
|
||||
//! On the client, we use the [`FullstackWebDocument`] implementation to render the head for any elements that were not rendered on the server.
|
||||
|
||||
use dioxus_document::*;
|
||||
|
||||
fn head_element_written_on_server() -> bool {
|
||||
crate::transport::head_element_hydration_entry()
|
||||
.get()
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// A document provider for fullstack web clients
|
||||
#[derive(Clone)]
|
||||
pub struct FullstackWebDocument<D> {
|
||||
document: D,
|
||||
}
|
||||
|
||||
impl<D> From<D> for FullstackWebDocument<D> {
|
||||
fn from(document: D) -> Self {
|
||||
Self { document }
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Document> Document for FullstackWebDocument<D> {
|
||||
fn eval(&self, js: String) -> Eval {
|
||||
self.document.eval(js)
|
||||
}
|
||||
|
||||
/// Set the title of the document
|
||||
fn set_title(&self, title: String) {
|
||||
self.document.set_title(title);
|
||||
}
|
||||
|
||||
/// Create a new meta tag in the head
|
||||
fn create_meta(&self, props: MetaProps) {
|
||||
self.document.create_meta(props);
|
||||
}
|
||||
|
||||
/// Create a new script tag in the head
|
||||
fn create_script(&self, props: ScriptProps) {
|
||||
self.document.create_script(props);
|
||||
}
|
||||
|
||||
/// Create a new style tag in the head
|
||||
fn create_style(&self, props: StyleProps) {
|
||||
self.document.create_style(props);
|
||||
}
|
||||
|
||||
/// Create a new link tag in the head
|
||||
fn create_link(&self, props: LinkProps) {
|
||||
self.document.create_link(props);
|
||||
}
|
||||
|
||||
fn create_head_component(&self) -> bool {
|
||||
!head_element_written_on_server()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
use axum_core::response::IntoResponse;
|
||||
use http::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::HttpError;
|
||||
|
||||
/// A default result type for server functions, which can either be successful or contain an error. The [`ServerFnResult`] type
|
||||
/// is a convenient alias for a `Result` type that uses [`ServerFnError`] as the error type.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use dioxus::prelude::*;
|
||||
///
|
||||
/// #[server]
|
||||
/// async fn parse_number(number: String) -> Result<f32> {
|
||||
/// let parsed_number: f32 = number.parse()?;
|
||||
/// Ok(parsed_number)
|
||||
/// }
|
||||
/// ```
|
||||
pub type ServerFnResult<T = ()> = std::result::Result<T, ServerFnError>;
|
||||
|
||||
/// The error type for the server function system. This enum encompasses all possible errors that can occur
|
||||
/// during the registration, invocation, and processing of server functions.
|
||||
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ServerFnError {
|
||||
/// Occurs when there is an error while actually running the function on the server.
|
||||
///
|
||||
/// The `details` field can optionally contain additional structured information about the error.
|
||||
/// When passing typed errors from the server to the client, the `details` field contains the serialized
|
||||
/// representation of the error.
|
||||
#[error("error running server function: {message} (details: {details:#?})")]
|
||||
ServerError {
|
||||
message: String,
|
||||
|
||||
/// Optional HTTP status code associated with the error.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
code: Option<u16>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
details: Option<serde_json::Value>,
|
||||
},
|
||||
|
||||
/// Occurs on the client if there is a network error while trying to run function on server.
|
||||
#[error("error reaching server to call server function: {0} ")]
|
||||
Request(RequestError),
|
||||
|
||||
/// Occurs on the client if there is an error while trying to read the response body as a stream.
|
||||
#[error("error reading response body stream: {0}")]
|
||||
StreamError(String),
|
||||
|
||||
/// Error while trying to register the server function (only occurs in case of poisoned RwLock).
|
||||
#[error("error while trying to register the server function: {0}")]
|
||||
Registration(String),
|
||||
|
||||
/// Occurs on the client if trying to use an unsupported `HTTP` method when building a request.
|
||||
#[error("error trying to build `HTTP` method request: {0}")]
|
||||
UnsupportedRequestMethod(String),
|
||||
|
||||
/// Occurs when there is an error while actually running the middleware on the server.
|
||||
#[error("error running middleware: {0}")]
|
||||
MiddlewareError(String),
|
||||
|
||||
/// Occurs on the client if there is an error deserializing the server's response.
|
||||
#[error("error deserializing server function results: {0}")]
|
||||
Deserialization(String),
|
||||
|
||||
/// Occurs on the client if there is an error serializing the server function arguments.
|
||||
#[error("error serializing server function arguments: {0}")]
|
||||
Serialization(String),
|
||||
|
||||
/// Occurs on the server if there is an error deserializing one of the arguments that's been sent.
|
||||
#[error("error deserializing server function arguments: {0}")]
|
||||
Args(String),
|
||||
|
||||
/// Occurs on the server if there's a missing argument.
|
||||
#[error("missing argument {0}")]
|
||||
MissingArg(String),
|
||||
|
||||
/// Occurs on the server if there is an error creating an HTTP response.
|
||||
#[error("error creating response {0}")]
|
||||
Response(String),
|
||||
}
|
||||
|
||||
impl ServerFnError {
|
||||
/// Create a new server error (status code 500) with a message.
|
||||
pub fn new(f: impl ToString) -> Self {
|
||||
ServerFnError::ServerError {
|
||||
message: f.to_string(),
|
||||
details: None,
|
||||
code: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for ServerFnError {
|
||||
fn from(value: anyhow::Error) -> Self {
|
||||
ServerFnError::ServerError {
|
||||
message: value.to_string(),
|
||||
details: None,
|
||||
code: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ServerFnError {
|
||||
fn from(value: serde_json::Error) -> Self {
|
||||
ServerFnError::Deserialization(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ServerFnError> for http::StatusCode {
|
||||
fn from(value: ServerFnError) -> Self {
|
||||
match value {
|
||||
ServerFnError::ServerError { code, .. } => match code {
|
||||
Some(code) => http::StatusCode::from_u16(code)
|
||||
.unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR),
|
||||
None => http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
ServerFnError::Request(err) => match err {
|
||||
RequestError::Status(_, code) => http::StatusCode::from_u16(code)
|
||||
.unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR),
|
||||
_ => http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
ServerFnError::StreamError(_)
|
||||
| ServerFnError::Registration(_)
|
||||
| ServerFnError::UnsupportedRequestMethod(_)
|
||||
| ServerFnError::MiddlewareError(_)
|
||||
| ServerFnError::Deserialization(_)
|
||||
| ServerFnError::Serialization(_)
|
||||
| ServerFnError::Args(_)
|
||||
| ServerFnError::MissingArg(_)
|
||||
| ServerFnError::Response(_) => http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RequestError> for ServerFnError {
|
||||
fn from(value: RequestError) -> Self {
|
||||
ServerFnError::Request(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HttpError> for ServerFnError {
|
||||
fn from(value: HttpError) -> Self {
|
||||
ServerFnError::ServerError {
|
||||
message: value.message.unwrap_or_else(|| {
|
||||
value
|
||||
.status
|
||||
.canonical_reason()
|
||||
.unwrap_or("Unknown error")
|
||||
.to_string()
|
||||
}),
|
||||
code: Some(value.status.as_u16()),
|
||||
details: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for ServerFnError {
|
||||
fn into_response(self) -> axum_core::response::Response {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// An error type representing issues that can occur while making requests.
|
||||
///
|
||||
/// This is made to paper over the reqwest::Error type which we don't want to export here and
|
||||
/// is limited in many ways.
|
||||
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RequestError {
|
||||
/// An error occurred when building the request.
|
||||
#[error("error building request: {0}")]
|
||||
Builder(String),
|
||||
|
||||
/// An error occurred when serializing the request body.
|
||||
#[error("error serializing request body: {0}")]
|
||||
Serialization(String),
|
||||
|
||||
/// An error occurred when following a redirect.
|
||||
#[error("error following redirect: {0}")]
|
||||
Redirect(String),
|
||||
|
||||
/// An error occurred when receiving a non-2xx status code.
|
||||
#[error("error receiving status code: {0} ({1})")]
|
||||
Status(String, u16),
|
||||
|
||||
/// An error occurred when a request times out.
|
||||
#[error("error timing out: {0}")]
|
||||
Timeout(String),
|
||||
|
||||
/// An error occurred when sending a request.
|
||||
#[error("error sending request: {0}")]
|
||||
Request(String),
|
||||
|
||||
/// An error occurred when upgrading a connection.
|
||||
#[error("error upgrading connection: {0}")]
|
||||
Connect(String),
|
||||
|
||||
/// An error occurred when there is a request or response body error.
|
||||
#[error("request or response body error: {0}")]
|
||||
Body(String),
|
||||
|
||||
/// An error occurred when decoding the response body.
|
||||
#[error("error decoding response body: {0}")]
|
||||
Decode(String),
|
||||
}
|
||||
|
||||
impl RequestError {
|
||||
pub fn status(&self) -> Option<StatusCode> {
|
||||
match self {
|
||||
RequestError::Status(_, code) => Some(StatusCode::from_u16(*code).ok()?),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status_code(&self) -> Option<u16> {
|
||||
match self {
|
||||
RequestError::Status(_, code) => Some(*code),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//! Contains a hydration compatible error boundary context.
|
||||
|
||||
use crate::serialize_context;
|
||||
use dioxus_core::{spawn_isomorphic, CapturedError, ErrorContext, ReactiveContext};
|
||||
use futures_util::StreamExt;
|
||||
|
||||
/// Initializes an error boundary context that is compatible with hydration.
|
||||
pub fn init_error_boundary() -> ErrorContext {
|
||||
let initial_error = serialize_context().create_entry::<Option<CapturedError>>();
|
||||
let (rx_context, mut rx) = ReactiveContext::new();
|
||||
let errors = ErrorContext::new(None);
|
||||
if let Ok(Some(err)) = initial_error.get() {
|
||||
errors.insert_error(err);
|
||||
}
|
||||
rx_context.run_in(|| {
|
||||
errors.error();
|
||||
});
|
||||
spawn_isomorphic({
|
||||
let errors = errors.clone();
|
||||
async move {
|
||||
if rx.next().await.is_some() {
|
||||
rx_context.run_in(|| {
|
||||
initial_error.insert(&errors.error(), std::panic::Location::caller())
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
errors
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
//! A history provider for fullstack apps that is compatible with hydration.
|
||||
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use crate::transport::{is_hydrating, SerializeContextEntry};
|
||||
use dioxus_core::{provide_context, queue_effect, schedule_update, try_consume_context};
|
||||
use dioxus_history::{history, provide_history_context, History};
|
||||
|
||||
// If we are currently in a scope and this is the first run then queue a rerender
|
||||
// for after hydration
|
||||
fn match_hydration<O>(
|
||||
during_hydration: impl FnOnce() -> O,
|
||||
after_hydration: impl FnOnce() -> O,
|
||||
) -> O {
|
||||
if is_hydrating() {
|
||||
let update = schedule_update();
|
||||
queue_effect(move || update());
|
||||
during_hydration()
|
||||
} else {
|
||||
after_hydration()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct ResolvedRouteContext {
|
||||
route: String,
|
||||
}
|
||||
|
||||
pub(crate) fn finalize_route() {
|
||||
// This may run in tests without the full hydration context set up, if it does, then just
|
||||
// return without modifying the context
|
||||
let Some(entry) = try_consume_context::<RouteEntry>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let entry = entry
|
||||
.entry
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.expect("Failed to get initial route from hydration context");
|
||||
|
||||
if cfg!(feature = "server") {
|
||||
let history = history();
|
||||
let route = history.current_route();
|
||||
entry.insert(&route, std::panic::Location::caller());
|
||||
provide_context(ResolvedRouteContext { route });
|
||||
} else if cfg!(feature = "web") {
|
||||
let initial_route = entry
|
||||
.get()
|
||||
.expect("Failed to get initial route from hydration context");
|
||||
provide_context(ResolvedRouteContext {
|
||||
route: initial_route,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide the fullstack history context. This interacts with the hydration context so it must
|
||||
/// be called in the same order on the client and server after the hydration context is created
|
||||
pub fn provide_fullstack_history_context<H: History + 'static>(history: H) {
|
||||
let entry = crate::transport::serialize_context().create_entry();
|
||||
provide_context(RouteEntry {
|
||||
entry: Rc::new(RefCell::new(Some(entry.clone()))),
|
||||
});
|
||||
provide_history_context(Rc::new(FullstackHistory::new(history)));
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RouteEntry {
|
||||
entry: Rc<RefCell<Option<SerializeContextEntry<String>>>>,
|
||||
}
|
||||
|
||||
/// A history provider for fullstack apps that is compatible with hydration.
|
||||
#[derive(Clone)]
|
||||
struct FullstackHistory<H> {
|
||||
history: H,
|
||||
}
|
||||
|
||||
impl<H> FullstackHistory<H> {
|
||||
/// Create a new `FullstackHistory` with the given history.
|
||||
pub fn new(history: H) -> Self {
|
||||
Self { history }
|
||||
}
|
||||
|
||||
/// Get the initial route of the history.
|
||||
fn initial_route(&self) -> String
|
||||
where
|
||||
H: History,
|
||||
{
|
||||
match try_consume_context::<ResolvedRouteContext>() {
|
||||
Some(context) => context.route,
|
||||
None => self.history.current_route(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: History> History for FullstackHistory<H> {
|
||||
fn current_prefix(&self) -> Option<String> {
|
||||
self.history.current_prefix()
|
||||
}
|
||||
|
||||
fn can_go_back(&self) -> bool {
|
||||
match_hydration(|| false, || self.history.can_go_back())
|
||||
}
|
||||
|
||||
fn can_go_forward(&self) -> bool {
|
||||
match_hydration(|| false, || self.history.can_go_forward())
|
||||
}
|
||||
|
||||
fn external(&self, url: String) -> bool {
|
||||
self.history.external(url)
|
||||
}
|
||||
|
||||
fn updater(&self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
|
||||
self.history.updater(callback)
|
||||
}
|
||||
|
||||
fn include_prevent_default(&self) -> bool {
|
||||
self.history.include_prevent_default()
|
||||
}
|
||||
|
||||
fn current_route(&self) -> String {
|
||||
match_hydration(|| self.initial_route(), || self.history.current_route())
|
||||
}
|
||||
|
||||
fn go_back(&self) {
|
||||
self.history.go_back();
|
||||
}
|
||||
|
||||
fn go_forward(&self) {
|
||||
self.history.go_forward();
|
||||
}
|
||||
|
||||
fn push(&self, route: String) {
|
||||
self.history.push(route);
|
||||
}
|
||||
|
||||
fn replace(&self, path: String) {
|
||||
self.history.replace(path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
use axum_core::response::IntoResponse;
|
||||
use http::StatusCode;
|
||||
use std::fmt;
|
||||
|
||||
/// An error type that wraps an HTTP status code and optional message.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct HttpError {
|
||||
pub status: StatusCode,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
impl HttpError {
|
||||
pub fn new<M: Into<String>>(status: StatusCode, message: M) -> Self {
|
||||
HttpError {
|
||||
status,
|
||||
message: Some(message.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn err<T>(status: StatusCode, message: impl Into<String>) -> Result<T, Self> {
|
||||
Err(HttpError::new(status, message))
|
||||
}
|
||||
|
||||
// --- 4xx Client Errors ---
|
||||
pub fn bad_request<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::BAD_REQUEST, message)
|
||||
}
|
||||
pub fn unauthorized<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::UNAUTHORIZED, message)
|
||||
}
|
||||
pub fn payment_required<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::PAYMENT_REQUIRED, message)
|
||||
}
|
||||
pub fn forbidden<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::FORBIDDEN, message)
|
||||
}
|
||||
pub fn not_found<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::NOT_FOUND, message)
|
||||
}
|
||||
pub fn method_not_allowed<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::METHOD_NOT_ALLOWED, message)
|
||||
}
|
||||
pub fn not_acceptable<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::NOT_ACCEPTABLE, message)
|
||||
}
|
||||
pub fn proxy_auth_required<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::PROXY_AUTHENTICATION_REQUIRED, message)
|
||||
}
|
||||
pub fn request_timeout<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::REQUEST_TIMEOUT, message)
|
||||
}
|
||||
pub fn conflict<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::CONFLICT, message)
|
||||
}
|
||||
pub fn gone<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::GONE, message)
|
||||
}
|
||||
pub fn length_required<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::LENGTH_REQUIRED, message)
|
||||
}
|
||||
pub fn precondition_failed<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::PRECONDITION_FAILED, message)
|
||||
}
|
||||
pub fn payload_too_large<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::PAYLOAD_TOO_LARGE, message)
|
||||
}
|
||||
pub fn uri_too_long<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::URI_TOO_LONG, message)
|
||||
}
|
||||
pub fn unsupported_media_type<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::UNSUPPORTED_MEDIA_TYPE, message)
|
||||
}
|
||||
pub fn im_a_teapot<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::IM_A_TEAPOT, message)
|
||||
}
|
||||
pub fn too_many_requests<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::TOO_MANY_REQUESTS, message)
|
||||
}
|
||||
|
||||
// --- 5xx Server Errors ---
|
||||
pub fn internal_server_error<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::INTERNAL_SERVER_ERROR, message)
|
||||
}
|
||||
pub fn not_implemented<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::NOT_IMPLEMENTED, message)
|
||||
}
|
||||
pub fn bad_gateway<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::BAD_GATEWAY, message)
|
||||
}
|
||||
pub fn service_unavailable<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::SERVICE_UNAVAILABLE, message)
|
||||
}
|
||||
pub fn gateway_timeout<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::GATEWAY_TIMEOUT, message)
|
||||
}
|
||||
pub fn http_version_not_supported<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::HTTP_VERSION_NOT_SUPPORTED, message)
|
||||
}
|
||||
|
||||
// --- 2xx/3xx (rare, but for completeness) ---
|
||||
pub fn ok<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::OK, message)
|
||||
}
|
||||
pub fn created<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::CREATED, message)
|
||||
}
|
||||
pub fn accepted<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::ACCEPTED, message)
|
||||
}
|
||||
pub fn moved_permanently<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::MOVED_PERMANENTLY, message)
|
||||
}
|
||||
pub fn found<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::FOUND, message)
|
||||
}
|
||||
pub fn see_other<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::SEE_OTHER, message)
|
||||
}
|
||||
pub fn not_modified<T>(message: impl Into<String>) -> Result<T, Self> {
|
||||
Self::err(StatusCode::NOT_MODIFIED, message)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for HttpError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self.message {
|
||||
Some(msg) => write!(f, "{}: {}", self.status, msg),
|
||||
None => write!(f, "{}", self.status),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for HttpError {}
|
||||
|
||||
/// Trait to convert errors into HttpError with a given status code.
|
||||
pub trait OrHttpError<T, M>: Sized {
|
||||
fn or_http_error(self, status: StatusCode, message: impl Into<String>) -> Result<T, HttpError>;
|
||||
|
||||
// --- Most common user-facing status codes ---
|
||||
fn or_bad_request(self, message: impl Into<String>) -> Result<T, HttpError> {
|
||||
self.or_http_error(StatusCode::BAD_REQUEST, message)
|
||||
}
|
||||
fn or_unauthorized(self, message: impl Into<String>) -> Result<T, HttpError> {
|
||||
self.or_http_error(StatusCode::UNAUTHORIZED, message)
|
||||
}
|
||||
fn or_forbidden(self, message: impl Into<String>) -> Result<T, HttpError> {
|
||||
self.or_http_error(StatusCode::FORBIDDEN, message)
|
||||
}
|
||||
fn or_not_found(self, message: impl Into<String>) -> Result<T, HttpError> {
|
||||
self.or_http_error(StatusCode::NOT_FOUND, message)
|
||||
}
|
||||
fn or_internal_server_error(self, message: impl Into<String>) -> Result<T, HttpError> {
|
||||
self.or_http_error(StatusCode::INTERNAL_SERVER_ERROR, message)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> OrHttpError<T, ()> for Result<T, E>
|
||||
where
|
||||
E: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
fn or_http_error(self, status: StatusCode, message: impl Into<String>) -> Result<T, HttpError> {
|
||||
self.map_err(|_| HttpError {
|
||||
status,
|
||||
message: Some(message.into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> OrHttpError<T, ()> for Option<T> {
|
||||
fn or_http_error(self, status: StatusCode, message: impl Into<String>) -> Result<T, HttpError> {
|
||||
self.ok_or_else(|| HttpError {
|
||||
status,
|
||||
message: Some(message.into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl OrHttpError<(), ()> for bool {
|
||||
fn or_http_error(
|
||||
self,
|
||||
status: StatusCode,
|
||||
message: impl Into<String>,
|
||||
) -> Result<(), HttpError> {
|
||||
if self {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(HttpError {
|
||||
status,
|
||||
message: Some(message.into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AnyhowMarker;
|
||||
impl<T> OrHttpError<T, AnyhowMarker> for Result<T, anyhow::Error> {
|
||||
fn or_http_error(self, status: StatusCode, message: impl Into<String>) -> Result<T, HttpError> {
|
||||
self.map_err(|_| HttpError {
|
||||
status,
|
||||
message: Some(message.into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for HttpError {
|
||||
fn into_response(self) -> axum_core::response::Response {
|
||||
let body = match &self.message {
|
||||
Some(msg) => msg.clone(),
|
||||
None => self
|
||||
.status
|
||||
.canonical_reason()
|
||||
.unwrap_or("Unknown error")
|
||||
.to_string(),
|
||||
};
|
||||
(self.status, body).into_response()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// #![warn(missing_docs)]
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
pub mod document;
|
||||
pub mod history;
|
||||
|
||||
mod errors;
|
||||
mod loader;
|
||||
mod server_cached;
|
||||
mod server_future;
|
||||
mod streaming;
|
||||
mod transport;
|
||||
|
||||
pub use crate::errors::*;
|
||||
pub use crate::loader::*;
|
||||
pub use crate::server_cached::*;
|
||||
pub use crate::server_future::*;
|
||||
pub use crate::streaming::*;
|
||||
pub use crate::transport::*;
|
||||
|
||||
/// Error types and utilities.
|
||||
#[macro_use]
|
||||
pub mod error;
|
||||
pub use error::*;
|
||||
|
||||
pub mod httperror;
|
||||
pub use httperror::*;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct DioxusServerState {}
|
||||
@@ -0,0 +1,365 @@
|
||||
use dioxus_core::{
|
||||
spawn, use_hook, Callback, IntoAttributeValue, IntoDynNode, ReactiveContext, Subscribers, Task,
|
||||
};
|
||||
use dioxus_core::{CapturedError, RenderError, Result, SuspendedFuture};
|
||||
use dioxus_hooks::{use_callback, use_signal};
|
||||
use dioxus_signals::{
|
||||
read_impls, ReadSignal, Readable, ReadableBoxExt, ReadableExt, ReadableRef, Signal, Writable,
|
||||
WritableExt, WriteLock,
|
||||
};
|
||||
use futures_util::{future, pin_mut, FutureExt, StreamExt};
|
||||
use generational_box::{BorrowResult, UnsyncStorage};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::future::Future;
|
||||
use std::{cell::Cell, ops::Deref, rc::Rc};
|
||||
|
||||
/// A hook to create a resource that loads data asynchronously.
|
||||
///
|
||||
/// This hook takes a closure that returns a future. This future will be executed on both the client
|
||||
/// and the server. The loader will return `Loading` until the future resolves, at which point it will
|
||||
/// return a `Loader<T>`. If the future fails, it will return `Loading::Failed`.
|
||||
///
|
||||
/// After the loader has successfully loaded once, it will never suspend the component again, but will
|
||||
/// instead re-load the value in the background whenever any of its dependencies change.
|
||||
///
|
||||
/// If an error occurs while re-loading, `use_loader` will once again emit a `Loading::Failed` value.
|
||||
/// The `use_loader` hook will never return a suspended state after the initial load.
|
||||
///
|
||||
/// # On the server
|
||||
///
|
||||
/// On the server, this hook will block the rendering of the component (and therefore, the page) until
|
||||
/// the future resolves. Any server futures called by `use_loader` will receive the same request context
|
||||
/// as the component that called `use_loader`.
|
||||
#[allow(clippy::result_large_err)]
|
||||
#[track_caller]
|
||||
pub fn use_loader<F, T, E>(mut future: impl FnMut() -> F + 'static) -> Result<Loader<T>, Loading>
|
||||
where
|
||||
F: Future<Output = Result<T, E>> + 'static,
|
||||
T: 'static + std::cmp::PartialEq + Serialize + DeserializeOwned,
|
||||
E: Into<dioxus_core::Error> + 'static,
|
||||
{
|
||||
let location = std::panic::Location::caller();
|
||||
|
||||
// todo: wire up serialize context into loader
|
||||
// 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
|
||||
// let storage_entry: SerializeContextEntry<T> = use_hook(|| serialize_context.create_entry());
|
||||
// use crate::{transport::SerializeContextEntry, use_server_future, use_server_future_unsuspended};
|
||||
|
||||
let mut err = use_signal(|| None as Option<CapturedError>);
|
||||
let mut value = use_signal(|| None as Option<T>);
|
||||
let mut state = use_signal(|| LoaderState::Pending);
|
||||
let (rc, changed) = use_hook(|| {
|
||||
let (rc, changed) = ReactiveContext::new_with_origin(location);
|
||||
(rc, Rc::new(Cell::new(Some(changed))))
|
||||
});
|
||||
|
||||
let callback = use_callback(move |_| {
|
||||
// Set the state to Pending when the task is restarted
|
||||
state.set(LoaderState::Pending);
|
||||
|
||||
// Create the user's task
|
||||
let fut = rc.reset_and_run_in(&mut future);
|
||||
|
||||
// Spawn a wrapper task that polls the inner future and watches its dependencies
|
||||
spawn(async move {
|
||||
// Move the future here and pin it so we can poll it
|
||||
let fut = fut;
|
||||
pin_mut!(fut);
|
||||
|
||||
// Run each poll in the context of the reactive scope
|
||||
// This ensures the scope is properly subscribed to the future's dependencies
|
||||
let res = future::poll_fn(|cx| {
|
||||
rc.run_in(|| {
|
||||
tracing::trace_span!("polling resource", location = %location)
|
||||
.in_scope(|| fut.poll_unpin(cx))
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
// Map the error to the captured error type so it's cheap to clone and pass out
|
||||
// We go through the anyhow::Error first since it's more useful.
|
||||
let res: Result<T, CapturedError> = res.map_err(|e| {
|
||||
let res: dioxus_core::Error = e.into();
|
||||
res.into()
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(v) => {
|
||||
err.set(None);
|
||||
value.set(Some(v));
|
||||
state.set(LoaderState::Ready);
|
||||
}
|
||||
Err(e) => {
|
||||
err.set(Some(e));
|
||||
state.set(LoaderState::Failed);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let mut task = use_hook(|| Signal::new(callback(())));
|
||||
|
||||
use_hook(|| {
|
||||
let mut changed = changed.take().unwrap();
|
||||
spawn(async move {
|
||||
loop {
|
||||
// Wait for the dependencies to change
|
||||
let _ = changed.next().await;
|
||||
|
||||
// Stop the old task
|
||||
task.write().cancel();
|
||||
|
||||
// Start a new task
|
||||
task.set(callback(()));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let read_value = use_hook(|| value.map(|f| f.as_ref().unwrap()).boxed());
|
||||
|
||||
let handle = LoaderHandle {
|
||||
task,
|
||||
err,
|
||||
callback,
|
||||
state,
|
||||
};
|
||||
|
||||
match &*state.read_unchecked() {
|
||||
LoaderState::Pending => Err(Loading::Pending(handle)),
|
||||
LoaderState::Failed => Err(Loading::Failed(handle)),
|
||||
LoaderState::Ready => Ok(Loader {
|
||||
real_value: value,
|
||||
read_value,
|
||||
error: err,
|
||||
state,
|
||||
task,
|
||||
handle,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// A Loader is a signal that represents a value that is loaded asynchronously.
|
||||
///
|
||||
/// Once a `Loader<T>` has been successfully created from `use_loader`, it can be use like a normal signal of type `T`.
|
||||
///
|
||||
/// When the loader is re-reloading its values, it will no longer suspend its component, making it
|
||||
/// very useful for server-side-rendering.
|
||||
pub struct Loader<T> {
|
||||
/// This is a signal that unwraps the inner value. We can't give it out unless we know the inner value is Some(T)!
|
||||
read_value: ReadSignal<T>,
|
||||
|
||||
/// This is the actual signal. We let the user set this value if they want to, but we can't let them set it to `None`.
|
||||
real_value: Signal<Option<T>>,
|
||||
error: Signal<Option<CapturedError>>,
|
||||
state: Signal<LoaderState>,
|
||||
task: Signal<Task>,
|
||||
handle: LoaderHandle,
|
||||
}
|
||||
|
||||
impl<T: 'static> Loader<T> {
|
||||
/// Get the error that occurred during loading, if any.
|
||||
///
|
||||
/// After initial load, this will return `None` until the next reload fails.
|
||||
pub fn error(&self) -> Option<CapturedError> {
|
||||
self.error.read().as_ref().cloned()
|
||||
}
|
||||
|
||||
/// Restart the loading task.
|
||||
///
|
||||
/// After initial load, this won't suspend the component, but will reload in the background.
|
||||
pub fn restart(&mut self) {
|
||||
self.handle.restart();
|
||||
}
|
||||
|
||||
/// Check if the loader has failed.
|
||||
pub fn is_error(&self) -> bool {
|
||||
self.error.read().is_some() && matches!(*self.state.read(), LoaderState::Failed)
|
||||
}
|
||||
|
||||
/// Cancel the current loading task.
|
||||
pub fn cancel(&self) {
|
||||
self.task.read().cancel();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> Writable for Loader<T> {
|
||||
type WriteMetadata = <Signal<Option<T>> as Writable>::WriteMetadata;
|
||||
|
||||
fn try_write_unchecked(
|
||||
&self,
|
||||
) -> std::result::Result<
|
||||
dioxus_signals::WritableRef<'static, Self>,
|
||||
generational_box::BorrowMutError,
|
||||
>
|
||||
where
|
||||
Self::Target: 'static,
|
||||
{
|
||||
let writer = self.real_value.try_write_unchecked()?;
|
||||
Ok(WriteLock::map(writer, |f: &mut Option<T>| {
|
||||
f.as_mut()
|
||||
.expect("Loader value should be set if the `Loader<T>` exists")
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Readable for Loader<T> {
|
||||
type Target = T;
|
||||
type Storage = UnsyncStorage;
|
||||
|
||||
#[track_caller]
|
||||
fn try_read_unchecked(
|
||||
&self,
|
||||
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
Ok(self.read_value.read_unchecked())
|
||||
}
|
||||
|
||||
/// Get the current value of the signal. **Unlike read, this will not subscribe the current scope to the signal which can cause parts of your UI to not update.**
|
||||
///
|
||||
/// If the signal has been dropped, this will panic.
|
||||
#[track_caller]
|
||||
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
Ok(self.peek_unchecked())
|
||||
}
|
||||
|
||||
fn subscribers(&self) -> Subscribers
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.read_value.subscribers()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoAttributeValue for Loader<T>
|
||||
where
|
||||
T: Clone + IntoAttributeValue + PartialEq + 'static,
|
||||
{
|
||||
fn into_value(self) -> dioxus_core::AttributeValue {
|
||||
self.with(|f| f.clone().into_value())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoDynNode for Loader<T>
|
||||
where
|
||||
T: Clone + IntoDynNode + PartialEq + 'static,
|
||||
{
|
||||
fn into_dyn_node(self) -> dioxus_core::DynamicNode {
|
||||
self().into_dyn_node()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> PartialEq for Loader<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.read_value == other.read_value
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Deref for Loader<T>
|
||||
where
|
||||
T: PartialEq + 'static,
|
||||
{
|
||||
type Target = dyn Fn() -> T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
unsafe { ReadableExt::deref_impl(self) }
|
||||
}
|
||||
}
|
||||
|
||||
read_impls!(Loader<T> where T: PartialEq);
|
||||
|
||||
impl<T> Clone for Loader<T> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Copy for Loader<T> {}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Hash, Eq, Debug)]
|
||||
pub enum LoaderState {
|
||||
/// The loader's future is still running
|
||||
Pending,
|
||||
|
||||
/// The loader's future has completed successfully
|
||||
Ready,
|
||||
|
||||
/// The loader's future has failed and now the loader is in an error state.
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct LoaderHandle {
|
||||
callback: Callback<(), Task>,
|
||||
task: Signal<Task>,
|
||||
err: Signal<Option<CapturedError>>,
|
||||
state: Signal<LoaderState>,
|
||||
}
|
||||
|
||||
impl LoaderHandle {
|
||||
/// Restart the loading task.
|
||||
pub fn restart(&mut self) {
|
||||
self.task.write().cancel();
|
||||
let new_task = self.callback.call(());
|
||||
self.task.set(new_task);
|
||||
}
|
||||
|
||||
/// Get the current state of the loader.
|
||||
pub fn state(&self) -> LoaderState {
|
||||
*self.state.read()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for LoaderHandle {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl Copy for LoaderHandle {}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum Loading {
|
||||
/// The loader is still pending and the component should suspend.
|
||||
Pending(LoaderHandle),
|
||||
|
||||
/// The loader has failed and an error will be returned up the tree.
|
||||
Failed(LoaderHandle),
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Loading {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Loading::Pending(_) => write!(f, "Loading::Pending"),
|
||||
Loading::Failed(_) => write!(f, "Loading::Failed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Loading {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Loading::Pending(_) => write!(f, "Loading is still pending"),
|
||||
Loading::Failed(_) => write!(f, "Loading has failed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a Loading into a RenderError for use with the `?` operator in components
|
||||
impl From<Loading> for RenderError {
|
||||
fn from(val: Loading) -> Self {
|
||||
match val {
|
||||
Loading::Pending(t) => RenderError::Suspended(SuspendedFuture::new(t.task.cloned())),
|
||||
Loading::Failed(err) => RenderError::Error(
|
||||
err.err
|
||||
.cloned()
|
||||
.expect("LoaderHandle in Failed state should always have an error"),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
use crate::transport::SerializeContextEntry;
|
||||
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.
|
||||
/// - 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.
|
||||
///
|
||||
/// The order this function is run on the client needs to be the same order initially run on the server.
|
||||
///
|
||||
/// If Dioxus fullstack cannot find the data on the client, it will run the closure again to get the data.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use dioxus::prelude::*;
|
||||
///
|
||||
/// fn app() -> Element {
|
||||
/// let state1 = use_server_cached(|| {
|
||||
/// 1234
|
||||
/// });
|
||||
///
|
||||
/// unimplemented!()
|
||||
/// }
|
||||
/// ```
|
||||
#[track_caller]
|
||||
pub fn use_server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
|
||||
server_fn: impl Fn() -> O,
|
||||
) -> O {
|
||||
let location = std::panic::Location::caller();
|
||||
use_hook(|| server_cached(server_fn, location))
|
||||
}
|
||||
|
||||
pub(crate) fn server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
|
||||
value: impl FnOnce() -> O,
|
||||
#[allow(unused)] location: &'static std::panic::Location<'static>,
|
||||
) -> O {
|
||||
let serialize = crate::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()
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,429 @@
|
||||
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 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
|
||||
// // }
|
||||
@@ -0,0 +1,106 @@
|
||||
use dioxus_core::try_consume_context;
|
||||
use dioxus_signals::{ReadableExt, Signal, WritableExt};
|
||||
|
||||
/// 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
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct StreamingContext {
|
||||
current_status: Signal<StreamingStatus>,
|
||||
}
|
||||
|
||||
impl Default for StreamingContext {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamingContext {
|
||||
/// Create a new streaming context. You should not need to call this directly. Dioxus fullstack will
|
||||
/// provide this context for you.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
current_status: Signal::new(StreamingStatus::RenderingInitialChunk),
|
||||
}
|
||||
}
|
||||
|
||||
/// Commit the initial chunk of the response. This will be called automatically if you are using the
|
||||
/// dioxus router when the suspense boundary above the router is resolved. Otherwise, you will need
|
||||
/// to call this manually to start the streaming part of the response.
|
||||
///
|
||||
/// Once this method has been called, the http response parts can no longer be modified.
|
||||
pub fn commit_initial_chunk(&mut self) {
|
||||
self.current_status
|
||||
.set(StreamingStatus::InitialChunkCommitted);
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
*self.current_status.read()
|
||||
}
|
||||
}
|
||||
|
||||
/// Commit the initial chunk of the response. This will be called automatically if you are using the
|
||||
/// dioxus router when the suspense boundary above the router is resolved. Otherwise, you will need
|
||||
/// to call this manually to start the streaming part of the response.
|
||||
///
|
||||
/// On the client, this will do nothing.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// # use dioxus::prelude::*;
|
||||
/// # use dioxus_fullstack_core::*;
|
||||
/// # fn Children() -> Element { unimplemented!() }
|
||||
/// fn App() -> Element {
|
||||
/// // This will start streaming immediately after the current render is complete.
|
||||
/// use_hook(commit_initial_chunk);
|
||||
///
|
||||
/// rsx! { Children {} }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn commit_initial_chunk() {
|
||||
crate::history::finalize_route();
|
||||
if let Some(mut streaming) = try_consume_context::<StreamingContext>() {
|
||||
streaming.commit_initial_chunk();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// On the client, this will always return `StreamingStatus::InitialChunkCommitted`.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// # use dioxus::prelude::*;
|
||||
/// # use dioxus_fullstack_core::*;
|
||||
/// #[component]
|
||||
/// fn MetaTitle(title: String) -> Element {
|
||||
/// // If streaming has already started, warn the user that the meta tag will not show
|
||||
/// // up in the initial chunk.
|
||||
/// use_hook(|| {
|
||||
/// if current_status() == StreamingStatus::InitialChunkCommitted {
|
||||
/// dioxus::logger::tracing::warn!("Since `MetaTitle` was rendered after the initial chunk was committed, the meta tag will not show up in the head without javascript enabled.");
|
||||
/// }
|
||||
/// });
|
||||
///
|
||||
/// rsx! { meta { property: "og:title", content: title } }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn current_status() -> StreamingStatus {
|
||||
if let Some(streaming) = try_consume_context::<StreamingContext>() {
|
||||
streaming.current_status()
|
||||
} else {
|
||||
StreamingStatus::InitialChunkCommitted
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
#![warn(missing_docs)]
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
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};
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
thread_local! {
|
||||
static CONTEXT: RefCell<Option<HydrationContext>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Data shared between the frontend and the backend for hydration
|
||||
/// of server functions.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct HydrationContext {
|
||||
#[cfg(feature = "web")]
|
||||
/// Is resolving suspense done on the client
|
||||
suspense_finished: bool,
|
||||
data: Rc<RefCell<HTMLData>>,
|
||||
}
|
||||
|
||||
impl HydrationContext {
|
||||
/// Create a new serialize context from the serialized data
|
||||
pub fn from_serialized(
|
||||
data: &[u8],
|
||||
debug_types: Option<Vec<String>>,
|
||||
debug_locations: Option<Vec<String>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "web")]
|
||||
suspense_finished: false,
|
||||
data: Rc::new(RefCell::new(HTMLData::from_serialized(
|
||||
data,
|
||||
debug_types,
|
||||
debug_locations,
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize the data in the context to be sent to the client
|
||||
pub fn serialized(&self) -> SerializedHydrationData {
|
||||
self.data.borrow().serialized()
|
||||
}
|
||||
|
||||
/// Create a new entry in the data that will be sent to the client without inserting any data. Returns an id that can be used to insert data into the entry once it is ready.
|
||||
pub fn create_entry<T>(&self) -> SerializeContextEntry<T> {
|
||||
let entry_index = self.data.borrow_mut().create_entry();
|
||||
|
||||
SerializeContextEntry {
|
||||
index: entry_index,
|
||||
context: self.clone(),
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the entry for the error in the suspense boundary
|
||||
pub fn error_entry(&self) -> SerializeContextEntry<Option<CapturedError>> {
|
||||
// The first entry is reserved for the error
|
||||
let entry_index = self.data.borrow_mut().create_entry_with_id(0);
|
||||
|
||||
SerializeContextEntry {
|
||||
index: entry_index,
|
||||
context: self.clone(),
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extend this data with the data from another [`HydrationContext`]
|
||||
pub fn extend(&self, other: &Self) {
|
||||
self.data.borrow_mut().extend(&other.data.borrow());
|
||||
}
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
/// Run a closure inside of this context
|
||||
pub fn in_context<T>(&self, f: impl FnOnce() -> T) -> T {
|
||||
CONTEXT.with(|context| {
|
||||
let old = context.borrow().clone();
|
||||
*context.borrow_mut() = Some(self.clone());
|
||||
let result = f();
|
||||
*context.borrow_mut() = old;
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
// pub(crate) fn insert<T: Transportable<M>, M: 'static>(
|
||||
pub(crate) fn insert<T: Serialize>(
|
||||
&self,
|
||||
id: usize,
|
||||
value: &T,
|
||||
location: &'static std::panic::Location<'static>,
|
||||
) {
|
||||
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>(
|
||||
&self,
|
||||
id: usize,
|
||||
) -> Result<T, TakeDataError> {
|
||||
// If suspense is finished on the client, we can assume that the data is available
|
||||
#[cfg(feature = "web")]
|
||||
if self.suspense_finished {
|
||||
return Err(TakeDataError::DataNotAvailable);
|
||||
}
|
||||
self.data.borrow().get(id)
|
||||
}
|
||||
}
|
||||
|
||||
/// An entry into the serialized context. The order entries are created in must be consistent
|
||||
/// between the server and the client.
|
||||
pub struct SerializeContextEntry<T> {
|
||||
/// The index this context will be inserted into inside the serialize context
|
||||
index: usize,
|
||||
/// The context this entry is associated with
|
||||
context: HydrationContext,
|
||||
phantom: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> Clone for SerializeContextEntry<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
index: self.index,
|
||||
context: self.context.clone(),
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>)
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
self.context.insert(self.index, value, location);
|
||||
}
|
||||
|
||||
/// Grab the data from the serialize context
|
||||
pub fn get(&self) -> Result<T, TakeDataError>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
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.
|
||||
pub fn is_hydrating() -> bool {
|
||||
#[cfg(feature = "web")]
|
||||
{
|
||||
// On the client, we can check if the context is set
|
||||
CONTEXT.with(|context| context.borrow().is_some())
|
||||
}
|
||||
#[cfg(not(feature = "web"))]
|
||||
{
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Get or insert the current serialize context. On the client, the hydration context this returns
|
||||
/// will always return `TakeDataError::DataNotAvailable` if hydration of the current chunk is finished.
|
||||
pub fn serialize_context() -> HydrationContext {
|
||||
#[cfg(feature = "web")]
|
||||
// On the client, the hydration logic provides the context in a global
|
||||
if let Some(current_context) = CONTEXT.with(|context| context.borrow().clone()) {
|
||||
current_context
|
||||
} else {
|
||||
// If the context is not set, then suspense is not active
|
||||
HydrationContext {
|
||||
suspense_finished: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "web"))]
|
||||
{
|
||||
// On the server each scope creates the context lazily
|
||||
dioxus_core::has_context()
|
||||
.unwrap_or_else(|| dioxus_core::provide_context(HydrationContext::default()))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct HTMLData {
|
||||
/// The position of the cursor in the data. This is only used on the client
|
||||
pub(crate) cursor: usize,
|
||||
/// The data required for hydration
|
||||
pub data: Vec<Option<Vec<u8>>>,
|
||||
/// The types of each serialized data
|
||||
///
|
||||
/// NOTE: we don't store this in the main data vec because we don't want to include it in
|
||||
/// release mode and we can't assume both the client and server are built with debug assertions
|
||||
/// matching
|
||||
#[cfg(debug_assertions)]
|
||||
pub debug_types: Vec<Option<String>>,
|
||||
/// The locations of each serialized data
|
||||
#[cfg(debug_assertions)]
|
||||
pub debug_locations: Vec<Option<String>>,
|
||||
}
|
||||
|
||||
impl Default for HTMLData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cursor: 1,
|
||||
data: Vec::new(),
|
||||
#[cfg(debug_assertions)]
|
||||
debug_types: Vec::new(),
|
||||
#[cfg(debug_assertions)]
|
||||
debug_locations: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HTMLData {
|
||||
#[allow(unused)]
|
||||
fn from_serialized(
|
||||
data: &[u8],
|
||||
debug_types: Option<Vec<String>>,
|
||||
debug_locations: Option<Vec<String>>,
|
||||
) -> Self {
|
||||
let data = ciborium::from_reader(Cursor::new(data)).unwrap();
|
||||
Self {
|
||||
cursor: 1,
|
||||
data,
|
||||
#[cfg(debug_assertions)]
|
||||
debug_types: debug_types
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(Some)
|
||||
.collect(),
|
||||
#[cfg(debug_assertions)]
|
||||
debug_locations: debug_locations
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(Some)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new entry in the data that will be sent to the client without inserting any data. Returns an id that can be used to insert data into the entry once it is ready.
|
||||
fn create_entry(&mut self) -> usize {
|
||||
let id = self.cursor;
|
||||
self.cursor += 1;
|
||||
self.create_entry_with_id(id)
|
||||
}
|
||||
|
||||
fn create_entry_with_id(&mut self, id: usize) -> usize {
|
||||
while id + 1 > self.data.len() {
|
||||
self.data.push(None);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.debug_types.push(None);
|
||||
self.debug_locations.push(None);
|
||||
}
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
/// Insert data into an entry that was created with [`Self::create_entry`]
|
||||
// fn insert<T: Transportable<M>, M: 'static>(
|
||||
fn insert<T: Serialize>(
|
||||
&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();
|
||||
self.data[id] = Some(serialized);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.debug_types[id] = Some(std::any::type_name::<T>().to_string());
|
||||
self.debug_locations[id] = Some(location.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
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",
|
||||
self.data.len(),
|
||||
index
|
||||
);
|
||||
return Err(TakeDataError::DataNotAvailable);
|
||||
}
|
||||
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) {
|
||||
Ok(x) => Ok(x),
|
||||
Err(err) => {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let debug_type = self.debug_types.get(index);
|
||||
let debug_locations = self.debug_locations.get(index);
|
||||
|
||||
if let (Some(Some(debug_type)), Some(Some(debug_locations))) =
|
||||
(debug_type, debug_locations)
|
||||
{
|
||||
let client_type = std::any::type_name::<T>();
|
||||
let client_location = std::panic::Location::caller();
|
||||
// We we have debug types and a location, we can provide a more helpful error message
|
||||
tracing::error!(
|
||||
"Error deserializing data: {err:?}\n\nThis type was serialized on the server at {debug_locations} with the type name {debug_type}. The client failed to deserialize the type {client_type} at {client_location}.",
|
||||
);
|
||||
return Err(TakeDataError::DeserializationError(err));
|
||||
}
|
||||
}
|
||||
// Otherwise, just log the generic deserialization error
|
||||
tracing::error!("Error deserializing data: {:?}", err);
|
||||
Err(TakeDataError::DeserializationError(err))
|
||||
}
|
||||
},
|
||||
None => Err(TakeDataError::DataPending),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extend this data with the data from another [`HTMLData`]
|
||||
pub(crate) fn extend(&mut self, other: &Self) {
|
||||
// Make sure this vectors error entry exists even if it is empty
|
||||
if self.data.is_empty() {
|
||||
self.data.push(None);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.debug_types.push(None);
|
||||
self.debug_locations.push(None);
|
||||
}
|
||||
}
|
||||
|
||||
let mut other_data_iter = other.data.iter().cloned();
|
||||
#[cfg(debug_assertions)]
|
||||
let mut other_debug_types_iter = other.debug_types.iter().cloned();
|
||||
#[cfg(debug_assertions)]
|
||||
let mut other_debug_locations_iter = other.debug_locations.iter().cloned();
|
||||
|
||||
// Merge the error entry from the other context
|
||||
if let Some(Some(other_error)) = other_data_iter.next() {
|
||||
self.data[0] = Some(other_error.clone());
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.debug_types[0] = other_debug_types_iter.next().unwrap_or(None);
|
||||
self.debug_locations[0] = other_debug_locations_iter.next().unwrap_or(None);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't copy the error from the other context
|
||||
self.data.extend(other_data_iter);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.debug_types.extend(other_debug_types_iter);
|
||||
self.debug_locations.extend(other_debug_locations_iter);
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode data as base64. This is intended to be used in the server to send data to the client.
|
||||
pub(crate) fn serialized(&self) -> SerializedHydrationData {
|
||||
let mut serialized = Vec::new();
|
||||
ciborium::into_writer(&self.data, &mut serialized).unwrap();
|
||||
|
||||
let data = base64::engine::general_purpose::STANDARD.encode(serialized);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let format_js_list_of_strings = |list: &[Option<String>]| {
|
||||
let body = list
|
||||
.iter()
|
||||
.map(|s| match s {
|
||||
Some(s) => {
|
||||
// Escape backslashes, quotes, and newlines
|
||||
let escaped = s
|
||||
.replace(r#"\"#, r#"\\"#)
|
||||
.replace("\n", r#"\n"#)
|
||||
.replace(r#"""#, r#"\""#);
|
||||
|
||||
format!(r#""{escaped}""#)
|
||||
}
|
||||
None => r#""unknown""#.to_string(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
format!("[{}]", body)
|
||||
};
|
||||
|
||||
SerializedHydrationData {
|
||||
data,
|
||||
#[cfg(debug_assertions)]
|
||||
debug_types: format_js_list_of_strings(&self.debug_types),
|
||||
#[cfg(debug_assertions)]
|
||||
debug_locations: format_js_list_of_strings(&self.debug_locations),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Data that was serialized on the server for hydration on the client. This includes
|
||||
/// extra information about the types and sources of the serialized data in debug mode
|
||||
pub struct SerializedHydrationData {
|
||||
/// The base64 encoded serialized data
|
||||
pub data: String,
|
||||
/// A list of the types of each serialized data
|
||||
#[cfg(debug_assertions)]
|
||||
pub debug_types: String,
|
||||
/// A list of the locations of each serialized data
|
||||
#[cfg(debug_assertions)]
|
||||
pub debug_locations: String,
|
||||
}
|
||||
|
||||
/// An error that can occur when trying to take data from the server
|
||||
#[derive(Debug)]
|
||||
pub enum TakeDataError {
|
||||
/// Deserializing the data failed
|
||||
DeserializationError(ciborium::de::Error<std::io::Error>),
|
||||
/// No data was available
|
||||
DataNotAvailable,
|
||||
/// The server serialized a placeholder for the data, but it isn't available yet
|
||||
DataPending,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TakeDataError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::DeserializationError(e) => write!(f, "DeserializationError: {}", e),
|
||||
Self::DataNotAvailable => write!(f, "DataNotAvailable"),
|
||||
Self::DataPending => write!(f, "DataPending"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TakeDataError {}
|
||||
|
||||
/// Create a new entry in the serialize context for the head element hydration
|
||||
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>),
|
||||
|
||||
// /// No data was available
|
||||
// #[error("DataNotAvailable")]
|
||||
// DataNotAvailable,
|
||||
|
||||
// /// The server serialized a placeholder for the data, but it isn't available yet
|
||||
// #[error("DataPending")]
|
||||
// DataPending,
|
||||
// }
|
||||
|
||||
// /// 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>;
|
||||
|
||||
// /// Deserialize the type from a byte slice
|
||||
// fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
|
||||
// where
|
||||
// Self: Sized;
|
||||
// }
|
||||
|
||||
// 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
|
||||
// }
|
||||
|
||||
// fn transport_from_bytes(bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>>
|
||||
// where
|
||||
// Self: Sized,
|
||||
// {
|
||||
// ciborium::from_reader(Cursor::new(bytes))
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[derive(Serialize, Deserialize)]
|
||||
// struct TransportResultErr<T> {
|
||||
// error: Result<T, CapturedError>,
|
||||
// }
|
||||
|
||||
// #[doc(hidden)]
|
||||
// pub struct TransportViaErrMarker;
|
||||
|
||||
// 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())),
|
||||
// };
|
||||
|
||||
// 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: 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()))),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[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))))
|
||||
// }
|
||||
// }
|
||||
Reference in new issue
Block a user