diff --git a/packages/fullstack/src/serve_config.rs b/packages/fullstack/src/serve_config.rs index e2397d54b6..bb1fa485d8 100644 --- a/packages/fullstack/src/serve_config.rs +++ b/packages/fullstack/src/serve_config.rs @@ -314,10 +314,9 @@ pub enum StreamingMode { pub struct ServeConfig { pub(crate) index: IndexHtml, pub(crate) incremental: Option, - pub(crate) streaming_mode: StreamingMode, - #[allow(unused)] pub(crate) context_providers: ContextProviders, + pub(crate) streaming_mode: StreamingMode, } impl LaunchConfig for ServeConfig {} diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index 18d474b00b..cfa62ed189 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -[6449103750905854967, 17669692872757955279, 13069001215487072322, 7552882699631236642, 4823736819599448666, 5444526391971481782, 12156139214887111728, 5052021921702764563, 12925655762638175824, 5638004933879392817] \ No newline at end of file +[6449103750905854967, 17669692872757955279, 13069001215487072322, 3463239101742491625, 10304266762117349606, 5444526391971481782, 12156139214887111728, 5052021921702764563, 12925655762638175824, 5638004933879392817] \ No newline at end of file diff --git a/packages/interpreter/src/js/hydrate.js b/packages/interpreter/src/js/hydrate.js index 5e44cd506f..6a15c24f00 100644 --- a/packages/interpreter/src/js/hydrate.js +++ b/packages/interpreter/src/js/hydrate.js @@ -1 +1 @@ -function register_rehydrate_chunk_for_streaming(callback){window.hydration_callback=callback;for(let i=0;i, js_sys::Uint8Array)>, + ); + + /// Register a callback that that will be called to hydrate a node at the given id with data from the server + /// Contains additional attributes that are used to hydrate the node + /// + /// # Compatibility Note + /// + /// We maintain two versions of this function for backwards compatibility. Both defer to the + /// same underlying implementation. The additional arguments are the debug locations. + pub fn register_rehydrate_chunk_for_streaming_debug( + closure: &Closure< dyn FnMut(Vec, js_sys::Uint8Array, Option>, Option>), >, ); diff --git a/packages/interpreter/src/ts/hydrate.ts b/packages/interpreter/src/ts/hydrate.ts index e9e41c871d..cbea2a761d 100644 --- a/packages/interpreter/src/ts/hydrate.ts +++ b/packages/interpreter/src/ts/hydrate.ts @@ -1,7 +1,16 @@ import "./hydrate_types"; +import { HydrationCallback } from "./hydrate_types"; +// We changed the signature to support debug types and locations but need to keep the old signature +// for backwards compatibility. export function register_rehydrate_chunk_for_streaming( callback: (id: number[], data: Uint8Array) => void +): void { + register_rehydrate_chunk_for_streaming_debug(callback) +} + +export function register_rehydrate_chunk_for_streaming_debug( + callback: HydrationCallback ): void { window.hydration_callback = callback; for (let i = 0; i < window.hydrate_queue.length; i++) { diff --git a/packages/interpreter/src/ts/hydrate_types.ts b/packages/interpreter/src/ts/hydrate_types.ts index ed1ccd531c..8a8469d1a6 100644 --- a/packages/interpreter/src/ts/hydrate_types.ts +++ b/packages/interpreter/src/ts/hydrate_types.ts @@ -1,15 +1,17 @@ -export {}; +export { HydrationCallback }; + +type HydrationCallback = ( + id: number[], + data: Uint8Array, + debug_types: string[] | null, + debug_locations: string[] | null +) => void; declare global { interface Window { hydrate_queue: [number[], Uint8Array, string[] | null, string[] | null][]; hydration_callback: - | null - | (( - id: number[], - data: Uint8Array, - debug_types: string[] | null, - debug_locations: string[] | null - ) => void); + | null + | HydrationCallback; } } diff --git a/packages/web/src/hydration/deserialize.rs b/packages/web/src/hydration/deserialize.rs index 9f9295c6c7..285c00f50c 100644 --- a/packages/web/src/hydration/deserialize.rs +++ b/packages/web/src/hydration/deserialize.rs @@ -79,14 +79,12 @@ impl HTMLDataCursor { }; // The first item is always an error if it exists - let error = myself + myself.error = myself .take::>() .ok() .flatten() .flatten(); - myself.error = error; - myself } @@ -101,42 +99,56 @@ impl HTMLDataCursor { ); return Err(TakeDataError::DataNotAvailable); } + let bytes = self.data[current].as_ref(); + + // Make sure we increment the index regardless of how the deserialization goes 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::(); - 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), + + // If there is no data, return None + let Some(bytes) = bytes else { + return Ok(None); + }; + + ciborium::from_reader(Cursor::new(bytes)) + .map_err(|err| self.make_detailed_deserialize_error::(current, err)) + } + + fn make_detailed_deserialize_error( + &self, + current: usize, + err: ciborium::de::Error, + ) -> TakeDataError { + // Try to provide a *very* helpful error message if we have debug types and locations + #[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::(); + let client_location = std::panic::Location::caller(); + // We we have debug types and a location, we can provide a more helpful error message + // todo(jon): wire this up to devtools with a json emit + tracing::error!( + r#"Error deserializing data: {err:?} +This 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 TakeDataError::DeserializationError(err); + } } + + // Otherwise, in release, just log the generic deserialization error + tracing::error!("Error deserializing data: {:?}", err); + TakeDataError::DeserializationError(err) } } diff --git a/packages/web/src/hydration/hydrate.rs b/packages/web/src/hydration/hydrate.rs index f39f808fc4..158500be98 100644 --- a/packages/web/src/hydration/hydrate.rs +++ b/packages/web/src/hydration/hydrate.rs @@ -229,7 +229,9 @@ impl WebsysDom { }); }; 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. diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 2ebeed57eb..3a6f161d07 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -100,20 +100,20 @@ pub async fn run(mut virtual_dom: VirtualDom, web_config: Config) -> ! { fn get_initial_hydration_debug_types() -> Option>; fn get_initial_hydration_debug_locations() -> Option>; } - let hydration_data = get_initial_hydration_data().to_vec(); - - // If we are running in debug mode, also get the debug types and locations - #[cfg(debug_assertions)] - let debug_types = get_initial_hydration_debug_types(); - #[cfg(not(debug_assertions))] - let debug_types = None; - #[cfg(debug_assertions)] - let debug_locations = get_initial_hydration_debug_locations(); - #[cfg(not(debug_assertions))] - let debug_locations = None; - - let server_data = - HTMLDataCursor::from_serialized(&hydration_data, debug_types, debug_locations); + + let server_data = HTMLDataCursor::from_serialized( + &get_initial_hydration_data().to_vec(), + // If we are running in debug mode, also get the debug types and locations + match cfg!(debug_assertions) { + true => get_initial_hydration_debug_types(), + false => None, + }, + match cfg!(debug_assertions) { + true => get_initial_hydration_debug_locations(), + false => None, + }, + ); + // If the server serialized an error into the root suspense boundary, throw it into the root scope if let Some(error) = server_data.error() { virtual_dom.in_runtime(|| dioxus_core::ScopeId::APP.throw_error(error));