Wait for the suspense boundary above the router to resolve before sending the first streaming chunk (#3891)
* Change the suspense streaming context after suspense is resolved in the router * Wait for the suspense context before streaming the first chunk in ssr * forward more response parts * warn * extract the hydration format into a crate * Fix hydration when no data is sent * forward web and server features * Fix suspense that starts on the client * fix use_server_future on the client * Add documentation for fullstack hooks and protocol * downgrade rust edition * fix clippy * fix tests * fix clippy * Fix pure web compile * undo changes to the fullstack router example * fix formatting
This commit is contained in:
1 parent
4f74ef81d1
commit
84221f55a0
45 files changed
+988
-533
No files matched your search
@@ -25,7 +25,9 @@ generational-box = { workspace = true }
|
||||
# Dioxus + SSR
|
||||
dioxus-ssr = { workspace = true, optional = true }
|
||||
dioxus-isrg = { workspace = true, optional = true }
|
||||
dioxus-router = { workspace = true, optional = true }
|
||||
dioxus-router = { workspace = true, features = ["streaming"], optional = true }
|
||||
dioxus-fullstack-hooks = { workspace = true }
|
||||
dioxus-fullstack-protocol = { workspace = true }
|
||||
hyper = { workspace = true, optional = true }
|
||||
http = { workspace = true, optional = true }
|
||||
|
||||
@@ -83,7 +85,7 @@ devtools = ["dioxus-web?/devtools", "dep:dioxus-devtools"]
|
||||
mounted = ["dioxus-web?/mounted"]
|
||||
file_engine = ["dioxus-web?/file_engine"]
|
||||
document = ["dioxus-web?/document"]
|
||||
web = ["dep:dioxus-web", "dep:web-sys"]
|
||||
web = ["dep:dioxus-web", "dep:web-sys", "dioxus-fullstack-hooks/web"]
|
||||
desktop = ["dep:dioxus-desktop", "server_fn/reqwest", "dioxus_server_macro/reqwest"]
|
||||
mobile = ["dep:dioxus-mobile", "server_fn/reqwest", "dioxus_server_macro/reqwest"]
|
||||
default-tls = ["server_fn/default-tls"]
|
||||
@@ -103,6 +105,7 @@ axum = [
|
||||
server = [
|
||||
"server_fn/ssr",
|
||||
"dioxus_server_macro/server",
|
||||
"dioxus-fullstack-hooks/server",
|
||||
"dep:tokio",
|
||||
"dep:tokio-util",
|
||||
"dep:tokio-stream",
|
||||
|
||||
@@ -202,8 +202,7 @@ async fn handle_server_fns_inner(
|
||||
}
|
||||
|
||||
// apply the response parts from the server context to the response
|
||||
let mut res_options = server_context.response_parts_mut();
|
||||
res.headers_mut().extend(res_options.headers.drain());
|
||||
server_context.send_response(&mut res);
|
||||
|
||||
Ok(res)
|
||||
} else {
|
||||
@@ -237,16 +236,6 @@ pub(crate) fn add_server_context(
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_request_parts_to_response<B>(
|
||||
headers: hyper::header::HeaderMap,
|
||||
response: &mut axum::response::Response<B>,
|
||||
) {
|
||||
let mut_headers = response.headers_mut();
|
||||
for (key, value) in headers.iter() {
|
||||
mut_headers.insert(key, value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// State used by [`render_handler`] to render a dioxus component with axum
|
||||
#[derive(Clone)]
|
||||
pub struct RenderHandleState {
|
||||
@@ -367,8 +356,7 @@ pub async fn render_handler(
|
||||
Ok((freshness, rx)) => {
|
||||
let mut response = axum::response::Html::from(Body::from_stream(rx)).into_response();
|
||||
freshness.write(response.headers_mut());
|
||||
let headers = server_context.response_parts().headers.clone();
|
||||
apply_request_parts_to_response(headers, &mut response);
|
||||
server_context.send_response(&mut response);
|
||||
Result::<http::Response<axum::body::Body>, StatusCode>::Ok(response)
|
||||
}
|
||||
Err(SSRError::Incremental(e)) => {
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
pub mod server;
|
||||
use dioxus_fullstack_protocol::SerializeContextEntry;
|
||||
#[cfg(feature = "server")]
|
||||
pub use server::ServerDocument;
|
||||
#[cfg(all(feature = "web", feature = "document"))]
|
||||
pub mod web;
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn head_element_hydration_entry() -> SerializeContextEntry<bool> {
|
||||
dioxus_fullstack_protocol::serialize_context().create_entry()
|
||||
}
|
||||
@@ -64,8 +64,8 @@ impl ServerDocument {
|
||||
// We only serialize the head elements if the web document feature is enabled
|
||||
#[cfg(feature = "document")]
|
||||
{
|
||||
let serialize = crate::html_storage::serialize_context();
|
||||
serialize.push(&!self.0.borrow().streaming, std::panic::Location::caller());
|
||||
super::head_element_hydration_entry()
|
||||
.insert(&!self.0.borrow().streaming, std::panic::Location::caller());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
use dioxus_lib::{document::*, prelude::queue_effect};
|
||||
use dioxus_web::WebDocument;
|
||||
|
||||
use super::head_element_hydration_entry;
|
||||
|
||||
fn head_element_written_on_server() -> bool {
|
||||
dioxus_web::take_server_data()
|
||||
head_element_hydration_entry()
|
||||
.get()
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod server_cached;
|
||||
pub mod server_future;
|
||||
@@ -1,53 +0,0 @@
|
||||
use dioxus_lib::prelude::use_hook;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
/// This allows you to send data from the server to the client. The data is serialized into the HTML on the server and hydrated on the client.
|
||||
///
|
||||
/// When you run this function on the client, you need to be careful to insure the order you run it initially is the same order you run it 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_lib::prelude::*;
|
||||
/// use dioxus_fullstack::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 {
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
let serialize = crate::html_storage::serialize_context();
|
||||
let data = value();
|
||||
serialize.push(&data, location);
|
||||
data
|
||||
}
|
||||
#[cfg(all(not(feature = "server"), feature = "web"))]
|
||||
{
|
||||
dioxus_web::take_server_data()
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(value)
|
||||
}
|
||||
#[cfg(not(any(feature = "server", feature = "web")))]
|
||||
{
|
||||
value()
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
use dioxus_lib::prelude::*;
|
||||
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,
|
||||
{
|
||||
#[cfg(feature = "server")]
|
||||
let serialize_context = crate::html_storage::use_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
|
||||
#[cfg(feature = "server")]
|
||||
let server_storage_entry = 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(
|
||||
dioxus_web::take_server_data::<T>(),
|
||||
)))
|
||||
});
|
||||
|
||||
let resource = use_resource(move || {
|
||||
#[cfg(feature = "server")]
|
||||
let serialize_context = serialize_context.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(Some(o))) => return o,
|
||||
|
||||
// The data is still pending from the server. Don't try to resolve it on the client
|
||||
Some(Ok(None)) => 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")]
|
||||
serialize_context.insert(server_storage_entry, &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)
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
#![cfg(feature = "server")]
|
||||
|
||||
use dioxus_lib::prelude::{has_context, provide_context, use_hook};
|
||||
use serde::Serialize;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
pub(crate) mod serialize;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct SerializeContext {
|
||||
data: Rc<RefCell<HTMLData>>,
|
||||
}
|
||||
|
||||
impl SerializeContext {
|
||||
/// 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(crate) fn create_entry(&self) -> usize {
|
||||
self.data.borrow_mut().create_entry()
|
||||
}
|
||||
|
||||
/// Insert data into an entry that was created with [`Self::create_entry`]
|
||||
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);
|
||||
}
|
||||
|
||||
/// Push resolved data into the serialized server data
|
||||
pub(crate) fn push<T: Serialize>(
|
||||
&self,
|
||||
data: &T,
|
||||
location: &'static std::panic::Location<'static>,
|
||||
) {
|
||||
self.data.borrow_mut().push(data, location);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn use_serialize_context() -> SerializeContext {
|
||||
use_hook(serialize_context)
|
||||
}
|
||||
|
||||
pub(crate) fn serialize_context() -> SerializeContext {
|
||||
has_context().unwrap_or_else(|| provide_context(SerializeContext::default()))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct HTMLData {
|
||||
/// 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 HTMLData {
|
||||
/// 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.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: Serialize>(
|
||||
&mut self,
|
||||
id: usize,
|
||||
value: &T,
|
||||
location: &'static std::panic::Location<'static>,
|
||||
) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
/// Push resolved data into the serialized server data
|
||||
fn push<T: Serialize>(&mut self, data: &T, location: &'static std::panic::Location<'static>) {
|
||||
let mut serialized = Vec::new();
|
||||
ciborium::into_writer(data, &mut serialized).unwrap();
|
||||
self.data.push(Some(serialized));
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.debug_types
|
||||
.push(Some(std::any::type_name::<T>().to_string()));
|
||||
self.debug_locations.push(Some(location.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Extend this data with the data from another [`HTMLData`]
|
||||
pub(crate) fn extend(&mut self, other: &Self) {
|
||||
self.data.extend_from_slice(&other.data);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.debug_types.extend_from_slice(&other.debug_types);
|
||||
self.debug_locations
|
||||
.extend_from_slice(&other.debug_locations);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
use base64::Engine;
|
||||
use dioxus_lib::prelude::dioxus_core::DynamicNode;
|
||||
use dioxus_lib::prelude::{has_context, ErrorContext, ScopeId, SuspenseContext, VNode, VirtualDom};
|
||||
|
||||
use super::SerializeContext;
|
||||
|
||||
impl super::HTMLData {
|
||||
/// Walks through the suspense boundary in a depth first order and extracts the data from the context API.
|
||||
/// We use depth first order instead of relying on the order the hooks are called in because during suspense on the server, the order that futures are run in may be non deterministic.
|
||||
pub(crate) fn extract_from_suspense_boundary(vdom: &VirtualDom, scope: ScopeId) -> Self {
|
||||
let mut data = Self::default();
|
||||
data.serialize_errors(vdom, scope);
|
||||
data.take_from_scope(vdom, scope);
|
||||
data
|
||||
}
|
||||
|
||||
/// Get the errors from the suspense boundary
|
||||
fn serialize_errors(&mut self, vdom: &VirtualDom, scope: ScopeId) {
|
||||
// If there is an error boundary on the suspense boundary, grab the error from the context API
|
||||
// and throw it on the client so that it bubbles up to the nearest error boundary
|
||||
let error = vdom.in_runtime(|| {
|
||||
scope
|
||||
.consume_context::<ErrorContext>()
|
||||
.and_then(|error_context| error_context.errors().first().cloned())
|
||||
});
|
||||
self.push(&error, std::panic::Location::caller());
|
||||
}
|
||||
|
||||
fn take_from_scope(&mut self, vdom: &VirtualDom, scope: ScopeId) {
|
||||
vdom.in_runtime(|| {
|
||||
scope.in_runtime(|| {
|
||||
// Grab any serializable server context from this scope
|
||||
let context: Option<SerializeContext> = has_context();
|
||||
if let Some(context) = context {
|
||||
self.extend(&context.data.borrow());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// then continue to any children
|
||||
if let Some(scope) = vdom.get_scope(scope) {
|
||||
// If this is a suspense boundary, move into the children first (even if they are suspended) because that will be run first on the client
|
||||
if let Some(suspense_boundary) =
|
||||
SuspenseContext::downcast_suspense_boundary_from_scope(&vdom.runtime(), scope.id())
|
||||
{
|
||||
if let Some(node) = suspense_boundary.suspended_nodes() {
|
||||
self.take_from_vnode(vdom, &node);
|
||||
}
|
||||
}
|
||||
if let Some(node) = scope.try_root_node() {
|
||||
self.take_from_vnode(vdom, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn take_from_vnode(&mut self, vdom: &VirtualDom, vnode: &VNode) {
|
||||
for (dynamic_node_index, dyn_node) in vnode.dynamic_nodes.iter().enumerate() {
|
||||
match dyn_node {
|
||||
DynamicNode::Component(comp) => {
|
||||
if let Some(scope) = comp.mounted_scope(dynamic_node_index, vnode, vdom) {
|
||||
self.take_from_scope(vdom, scope.id());
|
||||
}
|
||||
}
|
||||
DynamicNode::Fragment(nodes) => {
|
||||
for node in nodes {
|
||||
self.take_from_vnode(vdom, node);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
/// 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);
|
||||
|
||||
let format_js_list_of_strings = |list: &[Option<String>]| {
|
||||
let body = list
|
||||
.iter()
|
||||
.map(|s| match s {
|
||||
Some(s) => format!(r#""{s}""#),
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
/// 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(crate) 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,
|
||||
}
|
||||
@@ -4,16 +4,8 @@
|
||||
#![deny(missing_docs)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use once_cell;
|
||||
|
||||
mod html_storage;
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) type ContextProviders =
|
||||
Arc<Vec<Box<dyn Fn() -> Box<dyn std::any::Any> + Send + Sync + 'static>>>;
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
|
||||
pub mod server;
|
||||
@@ -22,8 +14,6 @@ pub mod server;
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "axum_core")))]
|
||||
pub mod axum_core;
|
||||
|
||||
mod hooks;
|
||||
|
||||
pub mod document;
|
||||
#[cfg(feature = "server")]
|
||||
mod render;
|
||||
@@ -41,8 +31,7 @@ mod server_context;
|
||||
|
||||
/// A prelude of commonly used items in dioxus-fullstack.
|
||||
pub mod prelude {
|
||||
use crate::hooks;
|
||||
pub use hooks::{server_cached::use_server_cached, server_future::use_server_future};
|
||||
pub use dioxus_fullstack_hooks::*;
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
//! A shared pool of renderers for efficient server side rendering.
|
||||
use crate::document::ServerDocument;
|
||||
use crate::html_storage::serialize::SerializedHydrationData;
|
||||
use crate::streaming::{Mount, StreamingRenderer};
|
||||
use dioxus_cli_config::base_path;
|
||||
use dioxus_fullstack_hooks::{StreamingContext, StreamingStatus};
|
||||
use dioxus_fullstack_protocol::{HydrationContext, SerializedHydrationData};
|
||||
use dioxus_interpreter_js::INITIALIZE_STREAMING_JS;
|
||||
use dioxus_isrg::{CachedRender, IncrementalRendererError, RenderFreshness};
|
||||
use dioxus_lib::document::Document;
|
||||
use dioxus_lib::prelude::dioxus_core::DynamicNode;
|
||||
use dioxus_router::prelude::ParseRouteError;
|
||||
use dioxus_ssr::Renderer;
|
||||
use futures_channel::mpsc::Sender;
|
||||
@@ -51,6 +53,10 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn in_root_scope<T>(virtual_dom: &VirtualDom, f: impl FnOnce() -> T) -> T {
|
||||
virtual_dom.in_runtime(|| ScopeId::ROOT.in_runtime(f))
|
||||
}
|
||||
|
||||
/// Errors that can occur during server side rendering before the initial chunk is sent down
|
||||
pub enum SSRError {
|
||||
/// An error from the incremental renderer. This should result in a 500 code
|
||||
@@ -180,7 +186,7 @@ impl SsrRendererPool {
|
||||
let myself = self.clone();
|
||||
let streaming_mode = cfg.streaming_mode;
|
||||
|
||||
let join_handle = spawn_platform(move || async move {
|
||||
let create_render_future = move || async move {
|
||||
let mut virtual_dom = virtual_dom_factory();
|
||||
let document = std::rc::Rc::new(crate::document::server::ServerDocument::default());
|
||||
virtual_dom.provide_root_context(document.clone());
|
||||
@@ -196,27 +202,47 @@ impl SsrRendererPool {
|
||||
} else {
|
||||
history = dioxus_history::MemoryHistory::with_initial_path(&route);
|
||||
}
|
||||
let streaming_context = in_root_scope(&virtual_dom, StreamingContext::new);
|
||||
virtual_dom.provide_root_context(Rc::new(history) as Rc<dyn dioxus_history::History>);
|
||||
virtual_dom.provide_root_context(document.clone() as std::rc::Rc<dyn Document>);
|
||||
virtual_dom.provide_root_context(streaming_context);
|
||||
|
||||
// rebuild the virtual dom, which may call server_context()
|
||||
with_server_context(server_context.clone(), || virtual_dom.rebuild_in_place());
|
||||
// rebuild the virtual dom
|
||||
virtual_dom.rebuild_in_place();
|
||||
|
||||
// If streaming is disabled, wait for the virtual dom to finish all suspense work
|
||||
// before rendering anything
|
||||
if streaming_mode == StreamingMode::Disabled {
|
||||
ProvideServerContext::new(virtual_dom.wait_for_suspense(), server_context.clone())
|
||||
.await
|
||||
virtual_dom.wait_for_suspense().await;
|
||||
}
|
||||
// Otherwise, just wait for the streaming context to signal the initial chunk is ready
|
||||
else {
|
||||
loop {
|
||||
// Check if the router has finished and set the streaming context to finished
|
||||
let streaming_context_finished =
|
||||
in_root_scope(&virtual_dom, || streaming_context.current_status())
|
||||
== StreamingStatus::InitialChunkCommitted;
|
||||
// Or if this app isn't using the router and has finished suspense
|
||||
let suspense_finished = !virtual_dom.suspended_tasks_remaining();
|
||||
if streaming_context_finished || suspense_finished {
|
||||
break;
|
||||
}
|
||||
|
||||
// Wait for new async work that runs during suspense (mainly use_server_futures)
|
||||
virtual_dom.wait_for_suspense_work().await;
|
||||
|
||||
// Do that async work
|
||||
virtual_dom.render_suspense_immediate().await;
|
||||
}
|
||||
}
|
||||
|
||||
// check if there are any errors
|
||||
let errors = with_server_context(server_context.clone(), || {
|
||||
virtual_dom.in_runtime(|| {
|
||||
let error_context: ErrorContext = ScopeId::APP
|
||||
.consume_context()
|
||||
.expect("The root should be under an error boundary");
|
||||
let errors = error_context.errors();
|
||||
errors.to_vec()
|
||||
})
|
||||
let errors = virtual_dom.in_runtime(|| {
|
||||
let error_context: ErrorContext = ScopeId::APP
|
||||
.consume_context()
|
||||
.expect("The root should be under an error boundary");
|
||||
let errors = error_context.errors();
|
||||
errors.to_vec()
|
||||
});
|
||||
if errors.is_empty() {
|
||||
// If routing was successful, we can return a 200 status and render into the stream
|
||||
@@ -282,16 +308,8 @@ impl SsrRendererPool {
|
||||
|
||||
// After the initial render, we need to resolve suspense
|
||||
while virtual_dom.suspended_tasks_remaining() {
|
||||
ProvideServerContext::new(
|
||||
virtual_dom.wait_for_suspense_work(),
|
||||
server_context.clone(),
|
||||
)
|
||||
.await;
|
||||
let resolved_suspense_nodes = ProvideServerContext::new(
|
||||
virtual_dom.render_suspense_immediate(),
|
||||
server_context.clone(),
|
||||
)
|
||||
.await;
|
||||
virtual_dom.wait_for_suspense_work().await;
|
||||
let resolved_suspense_nodes = virtual_dom.render_suspense_immediate().await;
|
||||
|
||||
// Just rerender the resolved nodes
|
||||
for scope in resolved_suspense_nodes {
|
||||
@@ -369,6 +387,10 @@ impl SsrRendererPool {
|
||||
|
||||
renderer.reset_render_components();
|
||||
myself.renderers.write().unwrap().push(renderer);
|
||||
};
|
||||
|
||||
let join_handle = spawn_platform(move || {
|
||||
ProvideServerContext::new(create_render_future(), server_context)
|
||||
});
|
||||
|
||||
// Wait for the initial result which determines the status code
|
||||
@@ -464,13 +486,83 @@ fn start_capturing_errors(suspense_scope: ScopeId) {
|
||||
fn serialize_server_data(virtual_dom: &VirtualDom, scope: ScopeId) -> SerializedHydrationData {
|
||||
// After we replace the placeholder in the dom with javascript, we need to send down the resolved data so that the client can hydrate the node
|
||||
// Extract any data we serialized for hydration (from server futures)
|
||||
let html_data =
|
||||
crate::html_storage::HTMLData::extract_from_suspense_boundary(virtual_dom, scope);
|
||||
let html_data = extract_from_suspense_boundary(virtual_dom, scope);
|
||||
|
||||
// serialize the server state into a base64 string
|
||||
html_data.serialized()
|
||||
}
|
||||
|
||||
/// Walks through the suspense boundary in a depth first order and extracts the data from the context API.
|
||||
/// We use depth first order instead of relying on the order the hooks are called in because during suspense on the server, the order that futures are run in may be non deterministic.
|
||||
pub(crate) fn extract_from_suspense_boundary(
|
||||
vdom: &VirtualDom,
|
||||
scope: ScopeId,
|
||||
) -> HydrationContext {
|
||||
let data = HydrationContext::default();
|
||||
serialize_errors(&data, vdom, scope);
|
||||
take_from_scope(&data, vdom, scope);
|
||||
data
|
||||
}
|
||||
|
||||
/// Get the errors from the suspense boundary
|
||||
fn serialize_errors(context: &HydrationContext, vdom: &VirtualDom, scope: ScopeId) {
|
||||
// If there is an error boundary on the suspense boundary, grab the error from the context API
|
||||
// and throw it on the client so that it bubbles up to the nearest error boundary
|
||||
let error = vdom.in_runtime(|| {
|
||||
scope
|
||||
.consume_context::<ErrorContext>()
|
||||
.and_then(|error_context| error_context.errors().first().cloned())
|
||||
});
|
||||
context
|
||||
.error_entry()
|
||||
.insert(&error, std::panic::Location::caller());
|
||||
}
|
||||
|
||||
fn take_from_scope(context: &HydrationContext, vdom: &VirtualDom, scope: ScopeId) {
|
||||
vdom.in_runtime(|| {
|
||||
scope.in_runtime(|| {
|
||||
// Grab any serializable server context from this scope
|
||||
let other: Option<HydrationContext> = has_context();
|
||||
if let Some(other) = other {
|
||||
context.extend(&other);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// then continue to any children
|
||||
if let Some(scope) = vdom.get_scope(scope) {
|
||||
// If this is a suspense boundary, move into the children first (even if they are suspended) because that will be run first on the client
|
||||
if let Some(suspense_boundary) =
|
||||
SuspenseContext::downcast_suspense_boundary_from_scope(&vdom.runtime(), scope.id())
|
||||
{
|
||||
if let Some(node) = suspense_boundary.suspended_nodes() {
|
||||
take_from_vnode(context, vdom, &node);
|
||||
}
|
||||
}
|
||||
if let Some(node) = scope.try_root_node() {
|
||||
take_from_vnode(context, vdom, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn take_from_vnode(context: &HydrationContext, vdom: &VirtualDom, vnode: &VNode) {
|
||||
for (dynamic_node_index, dyn_node) in vnode.dynamic_nodes.iter().enumerate() {
|
||||
match dyn_node {
|
||||
DynamicNode::Component(comp) => {
|
||||
if let Some(scope) = comp.mounted_scope(dynamic_node_index, vnode, vdom) {
|
||||
take_from_scope(context, vdom, scope.id());
|
||||
}
|
||||
}
|
||||
DynamicNode::Fragment(nodes) => {
|
||||
for node in nodes {
|
||||
take_from_vnode(context, vdom, node);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// State used in server side rendering. This utilizes a pool of [`dioxus_ssr::Renderer`]s to cache static templates between renders.
|
||||
#[derive(Clone)]
|
||||
pub struct SSRState {
|
||||
|
||||
@@ -8,7 +8,9 @@ use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::ContextProviders;
|
||||
#[allow(unused)]
|
||||
pub(crate) type ContextProviders =
|
||||
Arc<Vec<Box<dyn Fn() -> Box<dyn std::any::Any> + Send + Sync + 'static>>>;
|
||||
|
||||
/// A ServeConfig is used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus-ssr`].
|
||||
#[derive(Clone, Default)]
|
||||
@@ -486,6 +488,8 @@ pub enum StreamingMode {
|
||||
pub struct ServeConfig {
|
||||
pub(crate) index: IndexHtml,
|
||||
pub(crate) incremental: Option<dioxus_isrg::IncrementalRendererConfig>,
|
||||
// This is used in the axum integration
|
||||
#[allow(unused)]
|
||||
pub(crate) context_providers: ContextProviders,
|
||||
pub(crate) streaming_mode: StreamingMode,
|
||||
}
|
||||
|
||||
@@ -23,9 +23,10 @@ type SendSyncAnyMap = std::collections::HashMap<std::any::TypeId, ContextType>;
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct DioxusServerContext {
|
||||
shared_context: std::sync::Arc<RwLock<SendSyncAnyMap>>,
|
||||
response_parts: std::sync::Arc<RwLock<http::response::Parts>>,
|
||||
shared_context: Arc<RwLock<SendSyncAnyMap>>,
|
||||
response_parts: Arc<RwLock<http::response::Parts>>,
|
||||
pub(crate) parts: Arc<RwLock<http::request::Parts>>,
|
||||
response_sent: Arc<std::sync::atomic::AtomicBool>,
|
||||
}
|
||||
|
||||
enum ContextType {
|
||||
@@ -46,11 +47,12 @@ impl ContextType {
|
||||
impl Default for DioxusServerContext {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
shared_context: std::sync::Arc::new(RwLock::new(HashMap::new())),
|
||||
response_parts: std::sync::Arc::new(RwLock::new(
|
||||
shared_context: Arc::new(RwLock::new(HashMap::new())),
|
||||
response_parts: Arc::new(RwLock::new(
|
||||
http::response::Response::new(()).into_parts().0,
|
||||
)),
|
||||
parts: std::sync::Arc::new(RwLock::new(http::request::Request::new(()).into_parts().0)),
|
||||
parts: Arc::new(RwLock::new(http::request::Request::new(()).into_parts().0)),
|
||||
response_sent: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,6 +71,7 @@ mod server_fn_impl {
|
||||
response_parts: std::sync::Arc::new(RwLock::new(
|
||||
http::response::Response::new(()).into_parts().0,
|
||||
)),
|
||||
response_sent: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +84,7 @@ mod server_fn_impl {
|
||||
response_parts: std::sync::Arc::new(RwLock::new(
|
||||
http::response::Response::new(()).into_parts().0,
|
||||
)),
|
||||
response_sent: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +196,14 @@ mod server_fn_impl {
|
||||
/// }
|
||||
/// ```
|
||||
pub fn response_parts_mut(&self) -> RwLockWriteGuard<'_, http::response::Parts> {
|
||||
if self
|
||||
.response_sent
|
||||
.load(std::sync::atomic::Ordering::Relaxed)
|
||||
{
|
||||
tracing::error!("Attempted to modify the request after the first frame of the response has already been sent. \
|
||||
You can read the response, but modifying the response will not change the response that the client has already received. \
|
||||
Try modifying the response before the suspense boundary above the router is resolved.");
|
||||
}
|
||||
self.response_parts.write()
|
||||
}
|
||||
|
||||
@@ -261,6 +273,22 @@ mod server_fn_impl {
|
||||
pub async fn extract<M, T: FromServerContext<M>>(&self) -> Result<T, T::Rejection> {
|
||||
T::from_request(self).await
|
||||
}
|
||||
|
||||
/// Copy the response parts to a response and mark this server context as sent
|
||||
#[cfg(feature = "axum")]
|
||||
pub(crate) fn send_response<B>(&self, response: &mut http::response::Response<B>) {
|
||||
self.response_sent
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let parts = self.response_parts.read();
|
||||
|
||||
let mut_headers = response.headers_mut();
|
||||
for (key, value) in parts.headers.iter() {
|
||||
mut_headers.insert(key, value.clone());
|
||||
}
|
||||
*response.status_mut() = parts.status;
|
||||
*response.version_mut() = parts.version;
|
||||
response.extensions_mut().extend(parts.extensions.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
//! </script>
|
||||
//! ```
|
||||
|
||||
use dioxus_fullstack_protocol::SerializedHydrationData;
|
||||
use futures_channel::mpsc::Sender;
|
||||
|
||||
use std::{
|
||||
@@ -33,8 +34,6 @@ use std::{
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use crate::html_storage::serialize::SerializedHydrationData;
|
||||
|
||||
/// Sections are identified by a unique id based on the suspense path. We only track the path of suspense boundaries because the client may render different components than the server.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct MountPath {
|
||||
@@ -127,10 +126,10 @@ impl<E> StreamingRenderer<E> {
|
||||
// 2. The serialized data required to hydrate those components
|
||||
// 3. (in debug mode) The type names of the serialized data
|
||||
// 4. (in debug mode) The locations of the serialized data
|
||||
let raw_data = resolved_data.data;
|
||||
write!(
|
||||
into,
|
||||
r#"</div><script>window.dx_hydrate([{id}], "{raw_data}""#
|
||||
r#"</div><script>window.dx_hydrate([{id}], "{}""#,
|
||||
resolved_data.data
|
||||
)?;
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user