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
@@ -22,6 +22,7 @@ dioxus-interpreter-js = { workspace = true, features = [
|
||||
"minimal_bindings",
|
||||
"webonly",
|
||||
] }
|
||||
dioxus-fullstack-protocol = { workspace = true, features = ["web"], optional = true }
|
||||
generational-box = { workspace = true }
|
||||
|
||||
js-sys = "0.3.70"
|
||||
@@ -39,7 +40,6 @@ serde_json = { version = "1.0", optional = true }
|
||||
serde = { version = "1.0", optional = true }
|
||||
serde-wasm-bindgen = { version = "0.5.0", optional = true }
|
||||
|
||||
ciborium = { workspace = true, optional = true }
|
||||
async-trait = { version = "0.1.58", optional = true }
|
||||
|
||||
[dependencies.web-sys]
|
||||
@@ -86,7 +86,7 @@ lazy-js-bundle = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["mounted", "file_engine", "devtools", "document"]
|
||||
hydrate = ["web-sys/Comment", "ciborium", "dep:serde"]
|
||||
hydrate = ["web-sys/Comment", "dep:serde", "dep:dioxus-fullstack-protocol"]
|
||||
mounted = [
|
||||
"web-sys/Element",
|
||||
"dioxus-html/mounted",
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::io::Cursor;
|
||||
|
||||
use dioxus_core::CapturedError;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
thread_local! {
|
||||
static SERVER_DATA: RefCell<Option<HTMLDataCursor>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Try to take the next item from the server data cursor. This will only be set during the first run of a component before hydration.
|
||||
/// This will return `None` if no data was pushed for this instance or if serialization fails
|
||||
// TODO: evan better docs
|
||||
#[track_caller]
|
||||
pub fn take_server_data<T: DeserializeOwned>() -> Result<Option<T>, TakeDataError> {
|
||||
SERVER_DATA.with_borrow(|data| match data.as_ref() {
|
||||
Some(data) => data.take(),
|
||||
None => Err(TakeDataError::DataNotAvailable),
|
||||
})
|
||||
}
|
||||
|
||||
/// Run a closure with the server data
|
||||
pub(crate) fn with_server_data<O>(server_data: HTMLDataCursor, f: impl FnOnce() -> O) -> O {
|
||||
// Set the server data that will be used during hydration
|
||||
set_server_data(server_data);
|
||||
let out = f();
|
||||
// Hydrating the suspense node **should** eat all the server data, but just in case, remove it
|
||||
remove_server_data();
|
||||
out
|
||||
}
|
||||
|
||||
fn set_server_data(data: HTMLDataCursor) {
|
||||
SERVER_DATA.with_borrow_mut(|server_data| *server_data = Some(data));
|
||||
}
|
||||
|
||||
fn remove_server_data() {
|
||||
SERVER_DATA.with_borrow_mut(|server_data| server_data.take());
|
||||
}
|
||||
|
||||
/// Data that is deserialized from the server during hydration
|
||||
pub(crate) struct HTMLDataCursor {
|
||||
error: Option<CapturedError>,
|
||||
data: Vec<Option<Vec<u8>>>,
|
||||
#[cfg(debug_assertions)]
|
||||
debug_types: Option<Vec<String>>,
|
||||
#[cfg(debug_assertions)]
|
||||
debug_locations: Option<Vec<String>>,
|
||||
index: Cell<usize>,
|
||||
}
|
||||
|
||||
impl HTMLDataCursor {
|
||||
pub(crate) fn from_serialized(
|
||||
data: &[u8],
|
||||
debug_types: Option<Vec<String>>,
|
||||
debug_locations: Option<Vec<String>>,
|
||||
) -> Self {
|
||||
let deserialized = ciborium::from_reader(Cursor::new(data)).unwrap();
|
||||
Self::new(deserialized, debug_types, debug_locations)
|
||||
}
|
||||
|
||||
/// Get the error if there is one
|
||||
pub(crate) fn error(&self) -> Option<CapturedError> {
|
||||
self.error.clone()
|
||||
}
|
||||
|
||||
fn new(
|
||||
data: Vec<Option<Vec<u8>>>,
|
||||
#[allow(unused)] debug_types: Option<Vec<String>>,
|
||||
#[allow(unused)] debug_locations: Option<Vec<String>>,
|
||||
) -> Self {
|
||||
let mut myself = Self {
|
||||
index: Cell::new(0),
|
||||
error: None,
|
||||
data,
|
||||
#[cfg(debug_assertions)]
|
||||
debug_types,
|
||||
#[cfg(debug_assertions)]
|
||||
debug_locations,
|
||||
};
|
||||
|
||||
// The first item is always an error if it exists
|
||||
let error = myself
|
||||
.take::<Option<CapturedError>>()
|
||||
.ok()
|
||||
.flatten()
|
||||
.flatten();
|
||||
|
||||
myself.error = error;
|
||||
|
||||
myself
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn take<T: DeserializeOwned>(&self) -> Result<Option<T>, TakeDataError> {
|
||||
let current = self.index.get();
|
||||
if current >= 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(),
|
||||
current
|
||||
);
|
||||
return Err(TakeDataError::DataNotAvailable);
|
||||
}
|
||||
let bytes = self.data[current].as_ref();
|
||||
self.index.set(current + 1);
|
||||
match bytes {
|
||||
Some(bytes) => match ciborium::from_reader(Cursor::new(bytes)) {
|
||||
Ok(x) => Ok(Some(x)),
|
||||
Err(err) => {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let debug_type = self
|
||||
.debug_types
|
||||
.as_ref()
|
||||
.and_then(|types| types.get(current));
|
||||
let debug_locations = self
|
||||
.debug_locations
|
||||
.as_ref()
|
||||
.and_then(|locations| locations.get(current));
|
||||
|
||||
if let (Some(debug_type), 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 => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TakeDataError {}
|
||||
@@ -4,11 +4,10 @@
|
||||
//! 3. Register a callback for dx_hydrate(id, data) that takes some new data, reruns the suspense boundary with that new data and then rehydrates the node
|
||||
|
||||
use crate::dom::WebsysDom;
|
||||
use crate::with_server_data;
|
||||
use crate::HTMLDataCursor;
|
||||
use dioxus_core::prelude::*;
|
||||
use dioxus_core::AttributeValue;
|
||||
use dioxus_core::{DynamicNode, ElementId};
|
||||
use dioxus_fullstack_protocol::HydrationContext;
|
||||
use futures_channel::mpsc::UnboundedReceiver;
|
||||
use std::fmt::Write;
|
||||
use RehydrationError::*;
|
||||
@@ -146,12 +145,12 @@ impl WebsysDom {
|
||||
#[cfg(not(debug_assertions))]
|
||||
let debug_locations = None;
|
||||
|
||||
let server_data = HTMLDataCursor::from_serialized(&data, debug_types, debug_locations);
|
||||
let server_data = HydrationContext::from_serialized(&data, debug_types, debug_locations);
|
||||
// If the server serialized an error into the suspense boundary, throw it on the client so that it bubbles up to the nearest error boundary
|
||||
if let Some(error) = server_data.error() {
|
||||
if let Some(error) = server_data.error_entry().get().ok().flatten() {
|
||||
dom.in_runtime(|| id.throw_error(error));
|
||||
}
|
||||
with_server_data(server_data, || {
|
||||
server_data.in_context(|| {
|
||||
// rerun the scope with the new data
|
||||
SuspenseBoundaryProps::resolve_suspense(
|
||||
id,
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
#[cfg(feature = "hydrate")]
|
||||
mod deserialize;
|
||||
#[cfg(feature = "hydrate")]
|
||||
mod hydrate;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub use deserialize::*;
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[allow(unused)]
|
||||
pub use hydrate::*;
|
||||
|
||||
@@ -81,6 +81,8 @@ pub async fn run(mut virtual_dom: VirtualDom, web_config: Config) -> ! {
|
||||
if should_hydrate {
|
||||
#[cfg(feature = "hydrate")]
|
||||
{
|
||||
use dioxus_fullstack_protocol::HydrationContext;
|
||||
|
||||
websys_dom.skip_mutations = true;
|
||||
// Get the initial hydration data from the client
|
||||
#[wasm_bindgen::prelude::wasm_bindgen(inline_js = r#"
|
||||
@@ -113,12 +115,12 @@ pub async fn run(mut virtual_dom: VirtualDom, web_config: Config) -> ! {
|
||||
let debug_locations = None;
|
||||
|
||||
let server_data =
|
||||
HTMLDataCursor::from_serialized(&hydration_data, debug_types, debug_locations);
|
||||
HydrationContext::from_serialized(&hydration_data, debug_types, debug_locations);
|
||||
// If the server serialized an error into the root suspense boundary, throw it into the root scope
|
||||
if let Some(error) = server_data.error() {
|
||||
if let Some(error) = server_data.error_entry().get().ok().flatten() {
|
||||
virtual_dom.in_runtime(|| dioxus_core::ScopeId::APP.throw_error(error));
|
||||
}
|
||||
with_server_data(server_data, || {
|
||||
server_data.in_context(|| {
|
||||
virtual_dom.rebuild(&mut websys_dom);
|
||||
});
|
||||
websys_dom.skip_mutations = false;
|
||||
|
||||
Reference in new issue
Block a user