Fix hydration of suspense fragments that contain text nodes (#3287)
* Fix hydration of suspense fragments that contain text nodes * fix release builds * disable link deduplication * restore meta tag deduplication * Add a test for the failed hydration case * make changes to the document trait non-breaking * remove info log from server future hook * restore default implementation - this was also a breaking change * fix semver breakage * clean up server_future * Cleanup unused code * fix clippy --------- Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
1 parent
57d637184f
commit
d41ab3a756
33 files changed
+644
-222
No files matched your search
@@ -11,6 +11,7 @@ thread_local! {
|
||||
/// 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(),
|
||||
@@ -40,13 +41,21 @@ fn remove_server_data() {
|
||||
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]) -> Self {
|
||||
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)
|
||||
Self::new(deserialized, debug_types, debug_locations)
|
||||
}
|
||||
|
||||
/// Get the error if there is one
|
||||
@@ -54,11 +63,19 @@ impl HTMLDataCursor {
|
||||
self.error.clone()
|
||||
}
|
||||
|
||||
fn new(data: Vec<Option<Vec<u8>>>) -> Self {
|
||||
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,
|
||||
index: Cell::new(0),
|
||||
#[cfg(debug_assertions)]
|
||||
debug_types,
|
||||
#[cfg(debug_assertions)]
|
||||
debug_locations,
|
||||
};
|
||||
|
||||
// The first item is always an error if it exists
|
||||
@@ -73,6 +90,7 @@ impl HTMLDataCursor {
|
||||
myself
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn take<T: DeserializeOwned>(&self) -> Result<Option<T>, TakeDataError> {
|
||||
let current = self.index.get();
|
||||
if current >= self.data.len() {
|
||||
@@ -88,9 +106,33 @@ impl HTMLDataCursor {
|
||||
match bytes {
|
||||
Some(bytes) => match ciborium::from_reader(Cursor::new(bytes)) {
|
||||
Ok(x) => Ok(Some(x)),
|
||||
Err(e) => {
|
||||
tracing::error!("Error deserializing data: {:?}", e);
|
||||
Err(TakeDataError::DeserializationError(e))
|
||||
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),
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
//! 2. As we render the virtual dom initially, keep track of the server ids of the suspense boundaries
|
||||
//! 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 std::fmt::Write;
|
||||
|
||||
use crate::dom::WebsysDom;
|
||||
use crate::with_server_data;
|
||||
use crate::HTMLDataCursor;
|
||||
@@ -12,6 +10,7 @@ use dioxus_core::prelude::*;
|
||||
use dioxus_core::AttributeValue;
|
||||
use dioxus_core::{DynamicNode, ElementId};
|
||||
use futures_channel::mpsc::UnboundedReceiver;
|
||||
use std::fmt::Write;
|
||||
use RehydrationError::*;
|
||||
|
||||
use super::SuspenseMessage;
|
||||
@@ -113,6 +112,10 @@ impl WebsysDom {
|
||||
let SuspenseMessage {
|
||||
suspense_path,
|
||||
data,
|
||||
#[cfg(debug_assertions)]
|
||||
debug_types,
|
||||
#[cfg(debug_assertions)]
|
||||
debug_locations,
|
||||
} = message;
|
||||
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
@@ -138,7 +141,12 @@ impl WebsysDom {
|
||||
self.interpreter.base().push_root(node);
|
||||
}
|
||||
|
||||
let server_data = HTMLDataCursor::from_serialized(&data);
|
||||
#[cfg(not(debug_assertions))]
|
||||
let debug_types = None;
|
||||
#[cfg(not(debug_assertions))]
|
||||
let debug_locations = None;
|
||||
|
||||
let server_data = HTMLDataCursor::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() {
|
||||
dom.in_runtime(|| id.throw_error(error));
|
||||
@@ -205,15 +213,25 @@ impl WebsysDom {
|
||||
vdom: &VirtualDom,
|
||||
) -> Result<UnboundedReceiver<SuspenseMessage>, RehydrationError> {
|
||||
let (mut tx, rx) = futures_channel::mpsc::unbounded();
|
||||
let closure = move |path: Vec<u32>, data: js_sys::Uint8Array| {
|
||||
let data = data.to_vec();
|
||||
_ = tx.start_send(SuspenseMessage {
|
||||
suspense_path: path,
|
||||
data,
|
||||
});
|
||||
};
|
||||
let closure =
|
||||
move |path: Vec<u32>,
|
||||
data: js_sys::Uint8Array,
|
||||
#[allow(unused)] debug_types: Option<Vec<String>>,
|
||||
#[allow(unused)] debug_locations: Option<Vec<String>>| {
|
||||
let data = data.to_vec();
|
||||
_ = tx.start_send(SuspenseMessage {
|
||||
suspense_path: path,
|
||||
data,
|
||||
#[cfg(debug_assertions)]
|
||||
debug_types,
|
||||
#[cfg(debug_assertions)]
|
||||
debug_locations,
|
||||
});
|
||||
};
|
||||
let closure = wasm_bindgen::closure::Closure::new(closure);
|
||||
dioxus_interpreter_js::minimal_bindings::register_rehydrate_chunk_for_streaming(&closure);
|
||||
dioxus_interpreter_js::minimal_bindings::register_rehydrate_chunk_for_streaming_debug(
|
||||
&closure,
|
||||
);
|
||||
closure.forget();
|
||||
|
||||
// Rehydrate the root scope that was rendered on the server. We will likely run into suspense boundaries.
|
||||
|
||||
@@ -18,4 +18,12 @@ pub(crate) struct SuspenseMessage {
|
||||
#[cfg(feature = "hydrate")]
|
||||
/// The data to hydrate the suspense boundary with
|
||||
data: Vec<u8>,
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[cfg(debug_assertions)]
|
||||
/// The type names of the data
|
||||
debug_types: Option<Vec<String>>,
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[cfg(debug_assertions)]
|
||||
/// The location of the data in the source code
|
||||
debug_locations: Option<Vec<String>>,
|
||||
}
|
||||
Reference in new issue
Block a user