소스 검색

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>
Evan Almloff 5 달 전
부모
커밋
d41ab3a756
33개의 변경된 파일642개의 추가작업 그리고 220개의 파일을 삭제
  1. 11 0
      .cargo/config.toml
  2. 7 8
      packages/core/src/root_wrapper.rs
  3. 46 1
      packages/desktop/src/document.rs
  4. 22 21
      packages/document/src/document.rs
  5. 10 3
      packages/document/src/elements/link.rs
  6. 2 1
      packages/document/src/elements/meta.rs
  7. 15 3
      packages/document/src/elements/script.rs
  8. 36 4
      packages/document/src/elements/style.rs
  9. 17 38
      packages/fullstack/src/document/server.rs
  10. 11 16
      packages/fullstack/src/document/web.rs
  11. 5 2
      packages/fullstack/src/hooks/server_cached.rs
  12. 27 30
      packages/fullstack/src/hooks/server_future.rs
  13. 64 16
      packages/fullstack/src/html_storage/mod.rs
  14. 49 32
      packages/fullstack/src/html_storage/serialize.rs
  15. 20 2
      packages/fullstack/src/render.rs
  16. 22 3
      packages/fullstack/src/streaming.rs
  17. 1 1
      packages/interpreter/src/js/core.js
  18. 1 1
      packages/interpreter/src/js/hash.txt
  19. 1 1
      packages/interpreter/src/js/hydrate.js
  20. 1 1
      packages/interpreter/src/js/initialize_streaming.js
  21. 7 0
      packages/interpreter/src/lib.rs
  22. 19 5
      packages/interpreter/src/ts/core.ts
  23. 9 4
      packages/interpreter/src/ts/hydrate.ts
  24. 12 3
      packages/interpreter/src/ts/hydrate_types.ts
  25. 8 3
      packages/interpreter/src/ts/initialize_streaming.ts
  26. 54 1
      packages/liveview/src/document.rs
  27. 0 0
      packages/playwright-tests/nested-suspense/assets/style.css
  28. 4 0
      packages/playwright-tests/nested-suspense/src/main.rs
  29. 54 1
      packages/web/src/document.rs
  30. 49 7
      packages/web/src/hydration/deserialize.rs
  31. 29 11
      packages/web/src/hydration/hydrate.rs
  32. 8 0
      packages/web/src/hydration/mod.rs
  33. 21 1
      packages/web/src/lib.rs

+ 11 - 0
.cargo/config.toml

@@ -0,0 +1,11 @@
+[profile]
+
+[profile.dioxus-wasm]
+inherits = "dev"
+opt-level = 2
+
+[profile.dioxus-server]
+inherits = "dev"
+
+[profile.dioxus-android]
+inherits = "dev"

+ 7 - 8
packages/core/src/root_wrapper.rs

@@ -13,27 +13,26 @@ pub(crate) fn RootScopeWrapper(props: RootProps<VComponent>) -> Element {
         None,
         None,
         TEMPLATE,
         TEMPLATE,
         Box::new([DynamicNode::Component(
         Box::new([DynamicNode::Component(
-            fc_to_builder(ErrorBoundary)
-                .children(Element::Ok(VNode::new(
+            fc_to_builder(SuspenseBoundary)
+                .fallback(|_| Element::Ok(VNode::placeholder()))
+                .children(Ok(VNode::new(
                     None,
                     None,
                     TEMPLATE,
                     TEMPLATE,
                     Box::new([DynamicNode::Component({
                     Box::new([DynamicNode::Component({
-                        #[allow(unused_imports)]
-                        fc_to_builder(SuspenseBoundary)
-                            .fallback(|_| Element::Ok(VNode::placeholder()))
-                            .children(Ok(VNode::new(
+                        fc_to_builder(ErrorBoundary)
+                            .children(Element::Ok(VNode::new(
                                 None,
                                 None,
                                 TEMPLATE,
                                 TEMPLATE,
                                 Box::new([DynamicNode::Component(props.0)]),
                                 Box::new([DynamicNode::Component(props.0)]),
                                 Box::new([]),
                                 Box::new([]),
                             )))
                             )))
                             .build()
                             .build()
-                            .into_vcomponent(SuspenseBoundary)
+                            .into_vcomponent(ErrorBoundary)
                     })]),
                     })]),
                     Box::new([]),
                     Box::new([]),
                 )))
                 )))
                 .build()
                 .build()
-                .into_vcomponent(ErrorBoundary),
+                .into_vcomponent(SuspenseBoundary),
         )]),
         )]),
         Box::new([]),
         Box::new([]),
     ))
     ))

+ 46 - 1
packages/desktop/src/document.rs

@@ -1,4 +1,8 @@
-use dioxus_document::{Document, Eval, EvalError, Evaluator};
+use dioxus_core::prelude::queue_effect;
+use dioxus_document::{
+    create_element_in_head, Document, Eval, EvalError, Evaluator, LinkProps, MetaProps,
+    ScriptProps, StyleProps,
+};
 use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
 use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
 
 
 use crate::{query::Query, DesktopContext};
 use crate::{query::Query, DesktopContext};
@@ -7,6 +11,7 @@ use crate::{query::Query, DesktopContext};
 pub const NATIVE_EVAL_JS: &str = include_str!("./js/native_eval.js");
 pub const NATIVE_EVAL_JS: &str = include_str!("./js/native_eval.js");
 
 
 /// Represents the desktop-target's provider of evaluators.
 /// Represents the desktop-target's provider of evaluators.
+#[derive(Clone)]
 pub struct DesktopDocument {
 pub struct DesktopDocument {
     pub(crate) desktop_ctx: DesktopContext,
     pub(crate) desktop_ctx: DesktopContext,
 }
 }
@@ -25,6 +30,46 @@ impl Document for DesktopDocument {
     fn set_title(&self, title: String) {
     fn set_title(&self, title: String) {
         self.desktop_ctx.window.set_title(&title);
         self.desktop_ctx.window.set_title(&title);
     }
     }
+
+    /// Create a new meta tag in the head
+    fn create_meta(&self, props: MetaProps) {
+        let myself = self.clone();
+        queue_effect(move || {
+            myself.eval(create_element_in_head("meta", &props.attributes(), None));
+        });
+    }
+
+    /// Create a new script tag in the head
+    fn create_script(&self, props: ScriptProps) {
+        let myself = self.clone();
+        queue_effect(move || {
+            myself.eval(create_element_in_head(
+                "script",
+                &props.attributes(),
+                props.script_contents().ok(),
+            ));
+        });
+    }
+
+    /// Create a new style tag in the head
+    fn create_style(&self, props: StyleProps) {
+        let myself = self.clone();
+        queue_effect(move || {
+            myself.eval(create_element_in_head(
+                "style",
+                &props.attributes(),
+                props.style_contents().ok(),
+            ));
+        });
+    }
+
+    /// Create a new link tag in the head
+    fn create_link(&self, props: LinkProps) {
+        let myself = self.clone();
+        queue_effect(move || {
+            myself.eval(create_element_in_head("link", &props.attributes(), None));
+        });
+    }
 }
 }
 
 
 /// Represents a desktop-target's JavaScript evaluator.
 /// Represents a desktop-target's JavaScript evaluator.

+ 22 - 21
packages/document/src/document.rs

@@ -30,7 +30,10 @@ fn format_attributes(attributes: &[(&str, String)]) -> String {
     formatted
     formatted
 }
 }
 
 
-fn create_element_in_head(
+/// Create a new element in the head with javascript through the [`Document::eval`] method
+///
+/// This can be used to implement the head element creation logic for most [`Document`] implementations.
+pub fn create_element_in_head(
     tag: &str,
     tag: &str,
     attributes: &[(&str, String)],
     attributes: &[(&str, String)],
     children: Option<String>,
     children: Option<String>,
@@ -74,6 +77,8 @@ pub trait Document: 'static {
         attributes: &[(&str, String)],
         attributes: &[(&str, String)],
         contents: Option<String>,
         contents: Option<String>,
     ) {
     ) {
+        // This default implementation remains to make the trait compatible with the 0.6 version, but it should not be used
+        // The element should only be created inside an effect so it is not called while the component is suspended
         self.eval(create_element_in_head(name, attributes, contents));
         self.eval(create_element_in_head(name, attributes, contents));
     }
     }
 
 
@@ -86,30 +91,13 @@ pub trait Document: 'static {
     /// Create a new script tag in the head
     /// Create a new script tag in the head
     fn create_script(&self, props: ScriptProps) {
     fn create_script(&self, props: ScriptProps) {
         let attributes = props.attributes();
         let attributes = props.attributes();
-        match (&props.src, props.script_contents()) {
-            // The script has inline contents, render it as a script tag
-            (_, Ok(contents)) => self.create_head_element("script", &attributes, Some(contents)),
-            // The script has a src, render it as a script tag without a body
-            (Some(_), _) => self.create_head_element("script", &attributes, None),
-            // The script has neither contents nor src, log an error
-            (None, Err(err)) => err.log("Script"),
-        }
+        self.create_head_element("script", &attributes, props.script_contents().ok());
     }
     }
 
 
     /// Create a new style tag in the head
     /// Create a new style tag in the head
     fn create_style(&self, props: StyleProps) {
     fn create_style(&self, props: StyleProps) {
-        let mut attributes = props.attributes();
-        match (&props.href, props.style_contents()) {
-            // The style has inline contents, render it as a style tag
-            (_, Ok(contents)) => self.create_head_element("style", &attributes, Some(contents)),
-            // The style has a src, render it as a link tag
-            (Some(_), _) => {
-                attributes.push(("type", "text/css".into()));
-                self.create_head_element("link", &attributes, None)
-            }
-            // The style has neither contents nor src, log an error
-            (None, Err(err)) => err.log("Style"),
-        };
+        let attributes = props.attributes();
+        self.create_head_element("style", &attributes, props.style_contents().ok());
     }
     }
 
 
     /// Create a new link tag in the head
     /// Create a new link tag in the head
@@ -117,6 +105,13 @@ pub trait Document: 'static {
         let attributes = props.attributes();
         let attributes = props.attributes();
         self.create_head_element("link", &attributes, None);
         self.create_head_element("link", &attributes, None);
     }
     }
+
+    /// Check if we should create a new head component at all. If it returns false, the head component will be skipped.
+    ///
+    /// This runs once per head component and is used to hydrate head components in fullstack.
+    fn create_head_component(&self) -> bool {
+        true
+    }
 }
 }
 
 
 /// A document that does nothing
 /// A document that does nothing
@@ -148,4 +143,10 @@ impl Document for NoOpDocument {
         }
         }
         Eval::new(owner.insert(Box::new(NoOpEvaluator)))
         Eval::new(owner.insert(Box::new(NoOpEvaluator)))
     }
     }
+
+    fn set_title(&self, _: String) {}
+    fn create_meta(&self, _: MetaProps) {}
+    fn create_script(&self, _: ScriptProps) {}
+    fn create_style(&self, _: StyleProps) {}
+    fn create_link(&self, _: LinkProps) {}
 }
 }

+ 10 - 3
packages/document/src/elements/link.rs

@@ -25,7 +25,8 @@ pub struct LinkProps {
 }
 }
 
 
 impl LinkProps {
 impl LinkProps {
-    pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
+    /// Get all the attributes for the link tag
+    pub fn attributes(&self) -> Vec<(&'static str, String)> {
         let mut attributes = Vec::new();
         let mut attributes = Vec::new();
         if let Some(rel) = &self.rel {
         if let Some(rel) = &self.rel {
             attributes.push(("rel", rel.clone()));
             attributes.push(("rel", rel.clone()));
@@ -104,12 +105,18 @@ pub fn Link(props: LinkProps) -> Element {
     use_update_warning(&props, "Link {}");
     use_update_warning(&props, "Link {}");
 
 
     use_hook(|| {
     use_hook(|| {
+        let document = document();
+        let mut insert_link = document.create_head_component();
         if let Some(href) = &props.href {
         if let Some(href) = &props.href {
             if !should_insert_link(href) {
             if !should_insert_link(href) {
-                return;
+                insert_link = false;
             }
             }
         }
         }
-        let document = document();
+
+        if !insert_link {
+            return;
+        }
+
         document.create_link(props);
         document.create_link(props);
     });
     });
 
 

+ 2 - 1
packages/document/src/elements/meta.rs

@@ -16,7 +16,8 @@ pub struct MetaProps {
 }
 }
 
 
 impl MetaProps {
 impl MetaProps {
-    pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
+    /// Get all the attributes for the meta tag
+    pub fn attributes(&self) -> Vec<(&'static str, String)> {
         let mut attributes = Vec::new();
         let mut attributes = Vec::new();
         if let Some(property) = &self.property {
         if let Some(property) = &self.property {
             attributes.push(("property", property.clone()));
             attributes.push(("property", property.clone()));

+ 15 - 3
packages/document/src/elements/script.rs

@@ -22,7 +22,8 @@ pub struct ScriptProps {
 }
 }
 
 
 impl ScriptProps {
 impl ScriptProps {
-    pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
+    /// Get all the attributes for the script tag
+    pub fn attributes(&self) -> Vec<(&'static str, String)> {
         let mut attributes = Vec::new();
         let mut attributes = Vec::new();
         if let Some(defer) = &self.defer {
         if let Some(defer) = &self.defer {
             attributes.push(("defer", defer.to_string()));
             attributes.push(("defer", defer.to_string()));
@@ -90,13 +91,24 @@ pub fn Script(props: ScriptProps) -> Element {
     use_update_warning(&props, "Script {}");
     use_update_warning(&props, "Script {}");
 
 
     use_hook(|| {
     use_hook(|| {
+        let document = document();
+        let mut insert_script = document.create_head_component();
         if let Some(src) = &props.src {
         if let Some(src) = &props.src {
             if !should_insert_script(src) {
             if !should_insert_script(src) {
-                return;
+                insert_script = false;
             }
             }
         }
         }
 
 
-        let document = document();
+        if !insert_script {
+            return;
+        }
+
+        // Make sure the props are in a valid form - they must either have a source or children
+        if let (None, Err(err)) = (&props.src, props.script_contents()) {
+            // If the script has neither contents nor src, log an error
+            err.log("Script")
+        }
+
         document.create_script(props);
         document.create_script(props);
     });
     });
 
 

+ 36 - 4
packages/document/src/elements/style.rs

@@ -17,7 +17,8 @@ pub struct StyleProps {
 }
 }
 
 
 impl StyleProps {
 impl StyleProps {
-    pub(crate) fn attributes(&self) -> Vec<(&'static str, String)> {
+    /// Get all the attributes for the style tag
+    pub fn attributes(&self) -> Vec<(&'static str, String)> {
         let mut attributes = Vec::new();
         let mut attributes = Vec::new();
         if let Some(href) = &self.href {
         if let Some(href) = &self.href {
             attributes.push(("href", href.clone()));
             attributes.push(("href", href.clone()));
@@ -71,13 +72,44 @@ pub fn Style(props: StyleProps) -> Element {
     use_update_warning(&props, "Style {}");
     use_update_warning(&props, "Style {}");
 
 
     use_hook(|| {
     use_hook(|| {
+        let document = document();
+        let mut insert_style = document.create_head_component();
         if let Some(href) = &props.href {
         if let Some(href) = &props.href {
             if !should_insert_style(href) {
             if !should_insert_style(href) {
-                return;
+                insert_style = false;
             }
             }
         }
         }
-        let document = document();
-        document.create_style(props);
+        if !insert_style {
+            return;
+        }
+        let mut attributes = props.attributes();
+        match (&props.href, props.style_contents()) {
+            // The style has inline contents, render it as a style tag
+            (_, Ok(_)) => document.create_style(props),
+            // The style has a src, render it as a link tag
+            (Some(_), _) => {
+                attributes.push(("type", "text/css".into()));
+                document.create_link(LinkProps {
+                    media: props.media,
+                    title: props.title,
+                    r#type: Some("text/css".to_string()),
+                    additional_attributes: props.additional_attributes,
+                    href: props.href,
+                    rel: None,
+                    disabled: None,
+                    r#as: None,
+                    sizes: None,
+                    crossorigin: None,
+                    referrerpolicy: None,
+                    fetchpriority: None,
+                    hreflang: None,
+                    integrity: None,
+                    blocking: None,
+                });
+            }
+            // The style has neither contents nor src, log an error
+            (None, Err(err)) => err.log("Style"),
+        };
     });
     });
 
 
     VNode::empty()
     VNode::empty()

+ 17 - 38
packages/fullstack/src/document/server.rs

@@ -59,12 +59,13 @@ impl ServerDocument {
 
 
     /// Write the head element into the serialized context for hydration
     /// Write the head element into the serialized context for hydration
     /// We write true if the head element was written to the DOM during server side rendering
     /// We write true if the head element was written to the DOM during server side rendering
+    #[track_caller]
     pub(crate) fn serialize_for_hydration(&self) {
     pub(crate) fn serialize_for_hydration(&self) {
         // We only serialize the head elements if the web document feature is enabled
         // We only serialize the head elements if the web document feature is enabled
         #[cfg(feature = "document")]
         #[cfg(feature = "document")]
         {
         {
             let serialize = crate::html_storage::serialize_context();
             let serialize = crate::html_storage::serialize_context();
-            serialize.push(&!self.0.borrow().streaming);
+            serialize.push(&!self.0.borrow().streaming, std::panic::Location::caller());
         }
         }
     }
     }
 }
 }
@@ -76,13 +77,10 @@ impl Document for ServerDocument {
 
 
     fn set_title(&self, title: String) {
     fn set_title(&self, title: String) {
         self.warn_if_streaming();
         self.warn_if_streaming();
-        self.serialize_for_hydration();
         self.0.borrow_mut().title = Some(title);
         self.0.borrow_mut().title = Some(title);
     }
     }
 
 
     fn create_meta(&self, props: MetaProps) {
     fn create_meta(&self, props: MetaProps) {
-        self.warn_if_streaming();
-        self.serialize_for_hydration();
         self.0.borrow_mut().meta.push(rsx! {
         self.0.borrow_mut().meta.push(rsx! {
             meta {
             meta {
                 name: props.name,
                 name: props.name,
@@ -96,8 +94,6 @@ impl Document for ServerDocument {
     }
     }
 
 
     fn create_script(&self, props: ScriptProps) {
     fn create_script(&self, props: ScriptProps) {
-        self.warn_if_streaming();
-        self.serialize_for_hydration();
         let children = props.script_contents().ok();
         let children = props.script_contents().ok();
         self.0.borrow_mut().script.push(rsx! {
         self.0.borrow_mut().script.push(rsx! {
             script {
             script {
@@ -117,42 +113,19 @@ impl Document for ServerDocument {
     }
     }
 
 
     fn create_style(&self, props: StyleProps) {
     fn create_style(&self, props: StyleProps) {
-        self.warn_if_streaming();
-        self.serialize_for_hydration();
-        match (&props.href, props.style_contents()) {
-            // The style has inline contents, render it as a style tag
-            (_, Ok(contents)) => self.0.borrow_mut().script.push(rsx! {
-                style {
-                    media: props.media,
-                    nonce: props.nonce,
-                    title: props.title,
-                    ..props.additional_attributes,
-                    {contents}
-                }
-            }),
-            // The style has a href, render it as a link tag
-            (Some(_), _) => {
-                self.0.borrow_mut().script.push(rsx! {
-                    link {
-                        rel: "stylesheet",
-                        href: props.href,
-                        media: props.media,
-                        nonce: props.nonce,
-                        title: props.title,
-                        ..props.additional_attributes,
-                    }
-                });
-            }
-            // The style has neither contents nor src, log an error
-            (None, Err(err)) => {
-                err.log("Style");
+        let contents = props.style_contents().ok();
+        self.0.borrow_mut().script.push(rsx! {
+            style {
+                media: props.media,
+                nonce: props.nonce,
+                title: props.title,
+                ..props.additional_attributes,
+                {contents}
             }
             }
-        }
+        })
     }
     }
 
 
     fn create_link(&self, props: LinkProps) {
     fn create_link(&self, props: LinkProps) {
-        self.warn_if_streaming();
-        self.serialize_for_hydration();
         self.0.borrow_mut().link.push(rsx! {
         self.0.borrow_mut().link.push(rsx! {
             link {
             link {
                 rel: props.rel,
                 rel: props.rel,
@@ -172,4 +145,10 @@ impl Document for ServerDocument {
             }
             }
         })
         })
     }
     }
+
+    fn create_head_component(&self) -> bool {
+        self.warn_if_streaming();
+        self.serialize_for_hydration();
+        true
+    }
 }
 }

+ 11 - 16
packages/fullstack/src/document/web.rs

@@ -1,7 +1,7 @@
 #![allow(unused)]
 #![allow(unused)]
 //! On the client, we use the [`WebDocument`] implementation to render the head for any elements that were not rendered on the server.
 //! On the client, we use the [`WebDocument`] implementation to render the head for any elements that were not rendered on the server.
 
 
-use dioxus_lib::document::*;
+use dioxus_lib::{document::*, prelude::queue_effect};
 use dioxus_web::WebDocument;
 use dioxus_web::WebDocument;
 
 
 fn head_element_written_on_server() -> bool {
 fn head_element_written_on_server() -> bool {
@@ -12,6 +12,7 @@ fn head_element_written_on_server() -> bool {
 }
 }
 
 
 /// A document provider for fullstack web clients
 /// A document provider for fullstack web clients
+#[derive(Clone)]
 pub struct FullstackWebDocument;
 pub struct FullstackWebDocument;
 
 
 impl Document for FullstackWebDocument {
 impl Document for FullstackWebDocument {
@@ -19,38 +20,32 @@ impl Document for FullstackWebDocument {
         WebDocument.eval(js)
         WebDocument.eval(js)
     }
     }
 
 
+    /// Set the title of the document
     fn set_title(&self, title: String) {
     fn set_title(&self, title: String) {
-        if head_element_written_on_server() {
-            return;
-        }
         WebDocument.set_title(title);
         WebDocument.set_title(title);
     }
     }
 
 
+    /// Create a new meta tag in the head
     fn create_meta(&self, props: MetaProps) {
     fn create_meta(&self, props: MetaProps) {
-        if head_element_written_on_server() {
-            return;
-        }
         WebDocument.create_meta(props);
         WebDocument.create_meta(props);
     }
     }
 
 
+    /// Create a new script tag in the head
     fn create_script(&self, props: ScriptProps) {
     fn create_script(&self, props: ScriptProps) {
-        if head_element_written_on_server() {
-            return;
-        }
         WebDocument.create_script(props);
         WebDocument.create_script(props);
     }
     }
 
 
+    /// Create a new style tag in the head
     fn create_style(&self, props: StyleProps) {
     fn create_style(&self, props: StyleProps) {
-        if head_element_written_on_server() {
-            return;
-        }
         WebDocument.create_style(props);
         WebDocument.create_style(props);
     }
     }
 
 
+    /// Create a new link tag in the head
     fn create_link(&self, props: LinkProps) {
     fn create_link(&self, props: LinkProps) {
-        if head_element_written_on_server() {
-            return;
-        }
         WebDocument.create_link(props);
         WebDocument.create_link(props);
     }
     }
+
+    fn create_head_component(&self) -> bool {
+        !head_element_written_on_server()
+    }
 }
 }

+ 5 - 2
packages/fullstack/src/hooks/server_cached.rs

@@ -20,20 +20,23 @@ use serde::{de::DeserializeOwned, Serialize};
 ///    unimplemented!()
 ///    unimplemented!()
 /// }
 /// }
 /// ```
 /// ```
+#[track_caller]
 pub fn use_server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
 pub fn use_server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
     server_fn: impl Fn() -> O,
     server_fn: impl Fn() -> O,
 ) -> O {
 ) -> O {
-    use_hook(|| server_cached(server_fn))
+    let location = std::panic::Location::caller();
+    use_hook(|| server_cached(server_fn, location))
 }
 }
 
 
 pub(crate) fn server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
 pub(crate) fn server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
     value: impl FnOnce() -> O,
     value: impl FnOnce() -> O,
+    #[allow(unused)] location: &'static std::panic::Location<'static>,
 ) -> O {
 ) -> O {
     #[cfg(feature = "server")]
     #[cfg(feature = "server")]
     {
     {
         let serialize = crate::html_storage::serialize_context();
         let serialize = crate::html_storage::serialize_context();
         let data = value();
         let data = value();
-        serialize.push(&data);
+        serialize.push(&data, location);
         data
         data
     }
     }
     #[cfg(all(not(feature = "server"), feature = "web"))]
     #[cfg(all(not(feature = "server"), feature = "web"))]

+ 27 - 30
packages/fullstack/src/hooks/server_future.rs

@@ -56,6 +56,7 @@ use std::future::Future;
 /// }
 /// }
 /// ```
 /// ```
 #[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
 #[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
+#[track_caller]
 pub fn use_server_future<T, F>(
 pub fn use_server_future<T, F>(
     mut future: impl FnMut() -> F + 'static,
     mut future: impl FnMut() -> F + 'static,
 ) -> Result<Resource<T>, RenderError>
 ) -> Result<Resource<T>, RenderError>
@@ -65,10 +66,14 @@ where
 {
 {
     #[cfg(feature = "server")]
     #[cfg(feature = "server")]
     let serialize_context = crate::html_storage::use_serialize_context();
     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
     // 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")]
     #[cfg(feature = "server")]
     let server_storage_entry = use_hook(|| serialize_context.create_entry());
     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
     // If this is the first run and we are on the web client, the data might be cached
     #[cfg(feature = "web")]
     #[cfg(feature = "web")]
     let initial_web_result = use_hook(|| {
     let initial_web_result = use_hook(|| {
@@ -80,33 +85,28 @@ where
     let resource = use_resource(move || {
     let resource = use_resource(move || {
         #[cfg(feature = "server")]
         #[cfg(feature = "server")]
         let serialize_context = serialize_context.clone();
         let serialize_context = serialize_context.clone();
+
         let user_fut = future();
         let user_fut = future();
+
         #[cfg(feature = "web")]
         #[cfg(feature = "web")]
         let initial_web_result = initial_web_result.clone();
         let initial_web_result = initial_web_result.clone();
 
 
+        #[allow(clippy::let_and_return)]
         async move {
         async move {
             // If this is the first run and we are on the web client, the data might be cached
             // If this is the first run and we are on the web client, the data might be cached
             #[cfg(feature = "web")]
             #[cfg(feature = "web")]
-            {
-                let initial = initial_web_result.borrow_mut().take();
-                match initial {
-                    // This isn't the first run
-                    None => {}
-                    // This is the first run
-                    Some(first_run) => {
-                        match first_run {
-                            // THe data was deserialized successfully from the server
-                            Ok(Some(o)) => return o,
-                            // The data is still pending from the server. Don't try to resolve it on the client
-                            Ok(None) => {
-                                tracing::trace!("Waiting for server data");
-                                std::future::pending::<()>().await;
-                            }
-                            // The data was not available on the server, rerun the future
-                            Err(_) => {}
-                        }
-                    }
-                }
+            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
             // Otherwise just run the future itself
@@ -114,9 +114,8 @@ where
 
 
             // If this is the first run and we are on the server, cache the data in the slot we reserved for it
             // 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")]
             #[cfg(feature = "server")]
-            serialize_context.insert(server_storage_entry, &out);
+            serialize_context.insert(server_storage_entry, &out, caller);
 
 
-            #[allow(clippy::let_and_return)]
             out
             out
         }
         }
     });
     });
@@ -127,14 +126,12 @@ where
     });
     });
 
 
     // Suspend if the value isn't ready
     // Suspend if the value isn't ready
-    match resource.state().cloned() {
-        UseResourceState::Pending => {
-            let task = resource.task();
-            if !task.paused() {
-                return Err(suspend(task).unwrap_err());
-            }
-            Ok(resource)
+    if resource.state().cloned() == UseResourceState::Pending {
+        let task = resource.task();
+        if !task.paused() {
+            return Err(suspend(task).unwrap_err());
         }
         }
-        _ => Ok(resource),
     }
     }
+
+    Ok(resource)
 }
 }

+ 64 - 16
packages/fullstack/src/html_storage/mod.rs

@@ -1,11 +1,8 @@
-#![allow(unused)]
-use base64::Engine;
-use dioxus_lib::prelude::{has_context, provide_context, use_hook};
-use serialize::serde_to_writable;
-use std::{cell::RefCell, io::Cursor, rc::Rc, sync::atomic::AtomicUsize};
+#![cfg(feature = "server")]
 
 
-use base64::engine::general_purpose::STANDARD;
-use serde::{de::DeserializeOwned, Serialize};
+use dioxus_lib::prelude::{has_context, provide_context, use_hook};
+use serde::Serialize;
+use std::{cell::RefCell, rc::Rc};
 
 
 pub(crate) mod serialize;
 pub(crate) mod serialize;
 
 
@@ -21,13 +18,22 @@ impl SerializeContext {
     }
     }
 
 
     /// Insert data into an entry that was created with [`Self::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) {
-        self.data.borrow_mut().insert(id, value);
+    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
     /// Push resolved data into the serialized server data
-    pub(crate) fn push<T: Serialize>(&self, data: &T) {
-        self.data.borrow_mut().push(data);
+    pub(crate) fn push<T: Serialize>(
+        &self,
+        data: &T,
+        location: &'static std::panic::Location<'static>,
+    ) {
+        self.data.borrow_mut().push(data, location);
     }
     }
 }
 }
 
 
@@ -39,31 +45,73 @@ pub(crate) fn serialize_context() -> SerializeContext {
     has_context().unwrap_or_else(|| provide_context(SerializeContext::default()))
     has_context().unwrap_or_else(|| provide_context(SerializeContext::default()))
 }
 }
 
 
-#[derive(serde::Serialize, serde::Deserialize, Default)]
-#[serde(transparent)]
+#[derive(Default)]
 pub(crate) struct HTMLData {
 pub(crate) struct HTMLData {
+    /// The data required for hydration
     pub data: Vec<Option<Vec<u8>>>,
     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 {
 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.
     /// 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(&mut self) -> usize {
+    fn create_entry(&mut self) -> usize {
         let id = self.data.len();
         let id = self.data.len();
         self.data.push(None);
         self.data.push(None);
+        #[cfg(debug_assertions)]
+        {
+            self.debug_types.push(None);
+            self.debug_locations.push(None);
+        }
         id
         id
     }
     }
 
 
     /// Insert data into an entry that was created with [`Self::create_entry`]
     /// Insert data into an entry that was created with [`Self::create_entry`]
-    pub(crate) fn insert<T: Serialize>(&mut self, id: usize, value: &T) {
+    fn insert<T: Serialize>(
+        &mut self,
+        id: usize,
+        value: &T,
+        location: &'static std::panic::Location<'static>,
+    ) {
         let mut serialized = Vec::new();
         let mut serialized = Vec::new();
         ciborium::into_writer(value, &mut serialized).unwrap();
         ciborium::into_writer(value, &mut serialized).unwrap();
         self.data[id] = Some(serialized);
         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
     /// Push resolved data into the serialized server data
-    pub(crate) fn push<T: Serialize>(&mut self, data: &T) {
+    fn push<T: Serialize>(&mut self, data: &T, location: &'static std::panic::Location<'static>) {
         let mut serialized = Vec::new();
         let mut serialized = Vec::new();
         ciborium::into_writer(data, &mut serialized).unwrap();
         ciborium::into_writer(data, &mut serialized).unwrap();
         self.data.push(Some(serialized));
         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);
+        }
     }
     }
 }
 }

+ 49 - 32
packages/fullstack/src/html_storage/serialize.rs

@@ -1,45 +1,29 @@
-use dioxus_lib::prelude::dioxus_core::DynamicNode;
-use dioxus_lib::prelude::{
-    has_context, try_consume_context, ErrorContext, ScopeId, SuspenseBoundaryProps,
-    SuspenseContext, VNode, VirtualDom,
-};
-use serde::Serialize;
-
-use base64::engine::general_purpose::STANDARD;
 use base64::Engine;
 use base64::Engine;
+use dioxus_lib::prelude::dioxus_core::DynamicNode;
+use dioxus_lib::prelude::{has_context, ErrorContext, ScopeId, SuspenseContext, VNode, VirtualDom};
 
 
 use super::SerializeContext;
 use super::SerializeContext;
 
 
-#[allow(unused)]
-pub(crate) fn serde_to_writable<T: Serialize>(
-    value: &T,
-    write_to: &mut impl std::fmt::Write,
-) -> Result<(), ciborium::ser::Error<std::fmt::Error>> {
-    let mut serialized = Vec::new();
-    ciborium::into_writer(value, &mut serialized).unwrap();
-    write_to.write_str(STANDARD.encode(serialized).as_str())?;
-    Ok(())
-}
-
 impl super::HTMLData {
 impl super::HTMLData {
     /// Walks through the suspense boundary in a depth first order and extracts the data from the context API.
     /// 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.
     /// 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 {
     pub(crate) fn extract_from_suspense_boundary(vdom: &VirtualDom, scope: ScopeId) -> Self {
         let mut data = Self::default();
         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
         // 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
         // and throw it on the client so that it bubbles up to the nearest error boundary
-        let mut error = vdom.in_runtime(|| {
+        let error = vdom.in_runtime(|| {
             scope
             scope
                 .consume_context::<ErrorContext>()
                 .consume_context::<ErrorContext>()
                 .and_then(|error_context| error_context.errors().first().cloned())
                 .and_then(|error_context| error_context.errors().first().cloned())
         });
         });
-        data.push(&error);
-        data.take_from_scope(vdom, scope);
-        data
-    }
-
-    fn take_from_virtual_dom(&mut self, vdom: &VirtualDom) {
-        self.take_from_scope(vdom, ScopeId::ROOT)
+        self.push(&error, std::panic::Location::caller());
     }
     }
 
 
     fn take_from_scope(&mut self, vdom: &VirtualDom, scope: ScopeId) {
     fn take_from_scope(&mut self, vdom: &VirtualDom, scope: ScopeId) {
@@ -48,9 +32,7 @@ impl super::HTMLData {
                 // Grab any serializable server context from this scope
                 // Grab any serializable server context from this scope
                 let context: Option<SerializeContext> = has_context();
                 let context: Option<SerializeContext> = has_context();
                 if let Some(context) = context {
                 if let Some(context) = context {
-                    let borrow = context.data.borrow();
-                    let mut data = borrow.data.iter().cloned();
-                    self.data.extend(data)
+                    self.extend(&context.data.borrow());
                 }
                 }
             });
             });
         });
         });
@@ -91,9 +73,44 @@ impl super::HTMLData {
 
 
     #[cfg(feature = "server")]
     #[cfg(feature = "server")]
     /// Encode data as base64. This is intended to be used in the server to send data to the client.
     /// Encode data as base64. This is intended to be used in the server to send data to the client.
-    pub(crate) fn serialized(&self) -> String {
+    pub(crate) fn serialized(&self) -> SerializedHydrationData {
         let mut serialized = Vec::new();
         let mut serialized = Vec::new();
         ciborium::into_writer(&self.data, &mut serialized).unwrap();
         ciborium::into_writer(&self.data, &mut serialized).unwrap();
-        base64::engine::general_purpose::STANDARD.encode(serialized)
+
+        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,
+}

+ 20 - 2
packages/fullstack/src/render.rs

@@ -1,5 +1,6 @@
 //! A shared pool of renderers for efficient server side rendering.
 //! A shared pool of renderers for efficient server side rendering.
 use crate::document::ServerDocument;
 use crate::document::ServerDocument;
+use crate::html_storage::serialize::SerializedHydrationData;
 use crate::streaming::{Mount, StreamingRenderer};
 use crate::streaming::{Mount, StreamingRenderer};
 use dioxus_cli_config::base_path;
 use dioxus_cli_config::base_path;
 use dioxus_interpreter_js::INITIALIZE_STREAMING_JS;
 use dioxus_interpreter_js::INITIALIZE_STREAMING_JS;
@@ -408,7 +409,7 @@ fn start_capturing_errors(suspense_scope: ScopeId) {
     suspense_scope.in_runtime(provide_error_boundary);
     suspense_scope.in_runtime(provide_error_boundary);
 }
 }
 
 
-fn serialize_server_data(virtual_dom: &VirtualDom, scope: ScopeId) -> String {
+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
     // 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)
     // Extract any data we serialized for hydration (from server futures)
     let html_data =
     let html_data =
@@ -530,10 +531,27 @@ impl FullstackHTMLTemplate {
         // Collect the initial server data from the root node. For most apps, no use_server_futures will be resolved initially, so this will be full on `None`s.
         // Collect the initial server data from the root node. For most apps, no use_server_futures will be resolved initially, so this will be full on `None`s.
         // Sending down those Nones are still important to tell the client not to run the use_server_futures that are already running on the backend
         // Sending down those Nones are still important to tell the client not to run the use_server_futures that are already running on the backend
         let resolved_data = serialize_server_data(virtual_dom, ScopeId::ROOT);
         let resolved_data = serialize_server_data(virtual_dom, ScopeId::ROOT);
+        // We always send down the data required to hydrate components on the client
+        let raw_data = resolved_data.data;
         write!(
         write!(
             to,
             to,
-            r#"<script>window.initial_dioxus_hydration_data="{resolved_data}";</script>"#,
+            r#"<script>window.initial_dioxus_hydration_data="{raw_data}";"#,
         )?;
         )?;
+        #[cfg(debug_assertions)]
+        {
+            // In debug mode, we also send down the type names and locations of the serialized data
+            let debug_types = &resolved_data.debug_types;
+            let debug_locations = &resolved_data.debug_locations;
+            write!(
+                to,
+                r#"window.initial_dioxus_hydration_debug_types={debug_types};"#,
+            )?;
+            write!(
+                to,
+                r#"window.initial_dioxus_hydration_debug_locations={debug_locations};"#,
+            )?;
+        }
+        write!(to, r#"</script>"#,)?;
         to.write_str(&index.post_main)?;
         to.write_str(&index.post_main)?;
 
 
         Ok(())
         Ok(())

+ 22 - 3
packages/fullstack/src/streaming.rs

@@ -33,6 +33,8 @@ use std::{
     sync::{Arc, RwLock},
     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.
 /// 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)]
 #[derive(Clone, Debug, Default)]
 struct MountPath {
 struct MountPath {
@@ -109,7 +111,7 @@ impl<E> StreamingRenderer<E> {
         &self,
         &self,
         id: Mount,
         id: Mount,
         html: impl FnOnce(&mut W) -> std::fmt::Result,
         html: impl FnOnce(&mut W) -> std::fmt::Result,
-        data: impl Display,
+        resolved_data: SerializedHydrationData,
         into: &mut W,
         into: &mut W,
     ) -> std::fmt::Result {
     ) -> std::fmt::Result {
         // Then replace the suspense placeholder with the new content
         // Then replace the suspense placeholder with the new content
@@ -119,10 +121,27 @@ impl<E> StreamingRenderer<E> {
         html(into)?;
         html(into)?;
         // Restore the old path
         // Restore the old path
         *self.current_path.write().unwrap() = old_path;
         *self.current_path.write().unwrap() = old_path;
+        // dx_hydrate accepts 2-4 arguments. The first two are required, the rest are optional
+        // The arguments are:
+        // 1. The id of the nodes we are hydrating under
+        // 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!(
         write!(
             into,
             into,
-            r#"</div><script>window.dx_hydrate([{id}], "{data}")</script>"#
-        )
+            r#"</div><script>window.dx_hydrate([{id}], "{raw_data}""#
+        )?;
+        #[cfg(debug_assertions)]
+        {
+            // In debug mode, we also send down the type names and locations of the serialized data
+            let debug_types = &resolved_data.debug_types;
+            let debug_locations = &resolved_data.debug_locations;
+            write!(into, r#", {debug_types}, {debug_locations}"#,)?;
+        }
+        write!(into, r#")</script>"#)?;
+
+        Ok(())
     }
     }
 
 
     /// Close the stream with an error
     /// Close the stream with an error

+ 1 - 1
packages/interpreter/src/js/core.js

@@ -1 +1 @@
-function setAttributeInner(node,field,value,ns){if(ns==="style"){node.style.setProperty(field,value);return}if(ns){node.setAttributeNS(ns,field,value);return}switch(field){case"value":if(node.value!==value)node.value=value;break;case"initial_value":node.defaultValue=value;break;case"checked":node.checked=truthy(value);break;case"initial_checked":node.defaultChecked=truthy(value);break;case"selected":node.selected=truthy(value);break;case"initial_selected":node.defaultSelected=truthy(value);break;case"dangerous_inner_html":node.innerHTML=value;break;default:if(!truthy(value)&&isBoolAttr(field))node.removeAttribute(field);else node.setAttribute(field,value)}}var truthy=function(val){return val==="true"||val===!0},isBoolAttr=function(field){switch(field){case"allowfullscreen":case"allowpaymentrequest":case"async":case"autofocus":case"autoplay":case"checked":case"controls":case"default":case"defer":case"disabled":case"formnovalidate":case"hidden":case"ismap":case"itemscope":case"loop":case"multiple":case"muted":case"nomodule":case"novalidate":case"open":case"playsinline":case"readonly":case"required":case"reversed":case"selected":case"truespeed":case"webkitdirectory":return!0;default:return!1}};class BaseInterpreter{global;local;root;handler;resizeObserver;intersectionObserver;nodes;stack;templates;m;constructor(){}initialize(root,handler=null){this.global={},this.local={},this.root=root,this.nodes=[root],this.stack=[root],this.templates={},this.handler=handler,root.setAttribute("data-dioxus-id","0")}handleResizeEvent(entry){const target=entry.target;let event=new CustomEvent("resize",{bubbles:!1,detail:entry});target.dispatchEvent(event)}createResizeObserver(element){if(!this.resizeObserver)this.resizeObserver=new ResizeObserver((entries)=>{for(let entry of entries)this.handleResizeEvent(entry)});this.resizeObserver.observe(element)}removeResizeObserver(element){if(this.resizeObserver)this.resizeObserver.unobserve(element)}handleIntersectionEvent(entry){const target=entry.target;let event=new CustomEvent("visible",{bubbles:!1,detail:entry});target.dispatchEvent(event)}createIntersectionObserver(element){if(!this.intersectionObserver)this.intersectionObserver=new IntersectionObserver((entries)=>{for(let entry of entries)this.handleIntersectionEvent(entry)});this.intersectionObserver.observe(element)}removeIntersectionObserver(element){if(this.intersectionObserver)this.intersectionObserver.unobserve(element)}createListener(event_name,element,bubbles){if(event_name=="resize")this.createResizeObserver(element);else if(event_name=="visible")this.createIntersectionObserver(element);if(bubbles)if(this.global[event_name]===void 0)this.global[event_name]={active:1,callback:this.handler},this.root.addEventListener(event_name,this.handler);else this.global[event_name].active++;else{const id=element.getAttribute("data-dioxus-id");if(!this.local[id])this.local[id]={};element.addEventListener(event_name,this.handler)}}removeListener(element,event_name,bubbles){if(event_name=="resize")this.removeResizeObserver(element);else if(event_name=="visible")this.removeIntersectionObserver(element);else if(bubbles)this.removeBubblingListener(event_name);else this.removeNonBubblingListener(element,event_name)}removeBubblingListener(event_name){if(this.global[event_name].active--,this.global[event_name].active===0)this.root.removeEventListener(event_name,this.global[event_name].callback),delete this.global[event_name]}removeNonBubblingListener(element,event_name){const id=element.getAttribute("data-dioxus-id");if(delete this.local[id][event_name],Object.keys(this.local[id]).length===0)delete this.local[id];element.removeEventListener(event_name,this.handler)}removeAllNonBubblingListeners(element){const id=element.getAttribute("data-dioxus-id");delete this.local[id]}getNode(id){return this.nodes[id]}pushRoot(node){this.stack.push(node)}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k<many;k++)root.appendChild(els[k])}loadChild(ptr,len){let node=this.stack[this.stack.length-1],ptr_end=ptr+len;for(;ptr<ptr_end;ptr++){let end=this.m.getUint8(ptr);for(node=node.firstChild;end>0;end--)node=node.nextSibling}return node}saveTemplate(nodes,tmpl_id){this.templates[tmpl_id]=nodes}hydrate_node(hydrateNode,ids){const split=hydrateNode.getAttribute("data-node-hydration").split(","),id=ids[parseInt(split[0])];if(this.nodes[id]=hydrateNode,split.length>1){hydrateNode.listening=split.length-1,hydrateNode.setAttribute("data-dioxus-id",id.toString());for(let j=1;j<split.length;j++){const split2=split[j].split(":"),event_name=split2[0],bubbles=split2[1]==="1";this.createListener(event_name,hydrateNode,bubbles)}}}hydrate(ids,underNodes){for(let i=0;i<underNodes.length;i++){const under=underNodes[i];if(under instanceof HTMLElement){if(under.getAttribute("data-node-hydration"))this.hydrate_node(under,ids);const hydrateNodes=under.querySelectorAll("[data-node-hydration]");for(let i2=0;i2<hydrateNodes.length;i2++)this.hydrate_node(hydrateNodes[i2],ids)}const treeWalker=document.createTreeWalker(under,NodeFilter.SHOW_COMMENT);while(treeWalker.currentNode){const currentNode=treeWalker.currentNode;if(currentNode.nodeType===Node.COMMENT_NODE){const id=currentNode.textContent,placeholderSplit=id.split("placeholder");if(placeholderSplit.length>1){if(this.nodes[ids[parseInt(placeholderSplit[1])]]=currentNode,!treeWalker.nextNode())break;continue}const textNodeSplit=id.split("node-id");if(textNodeSplit.length>1){let next=currentNode.nextSibling;currentNode.remove();let commentAfterText,textNode;if(next.nodeType===Node.COMMENT_NODE){const newText=next.parentElement.insertBefore(document.createTextNode(""),next);commentAfterText=next,textNode=newText}else textNode=next,commentAfterText=textNode.nextSibling;treeWalker.currentNode=commentAfterText,this.nodes[ids[parseInt(textNodeSplit[1])]]=textNode;let exit=!treeWalker.nextNode();if(commentAfterText.remove(),exit)break;continue}}if(!treeWalker.nextNode())break}}}setAttributeInner(node,field,value,ns){setAttributeInner(node,field,value,ns)}}export{BaseInterpreter};
+function setAttributeInner(node,field,value,ns){if(ns==="style"){node.style.setProperty(field,value);return}if(ns){node.setAttributeNS(ns,field,value);return}switch(field){case"value":if(node.value!==value)node.value=value;break;case"initial_value":node.defaultValue=value;break;case"checked":node.checked=truthy(value);break;case"initial_checked":node.defaultChecked=truthy(value);break;case"selected":node.selected=truthy(value);break;case"initial_selected":node.defaultSelected=truthy(value);break;case"dangerous_inner_html":node.innerHTML=value;break;default:if(!truthy(value)&&isBoolAttr(field))node.removeAttribute(field);else node.setAttribute(field,value)}}var truthy=function(val){return val==="true"||val===!0},isBoolAttr=function(field){switch(field){case"allowfullscreen":case"allowpaymentrequest":case"async":case"autofocus":case"autoplay":case"checked":case"controls":case"default":case"defer":case"disabled":case"formnovalidate":case"hidden":case"ismap":case"itemscope":case"loop":case"multiple":case"muted":case"nomodule":case"novalidate":case"open":case"playsinline":case"readonly":case"required":case"reversed":case"selected":case"truespeed":case"webkitdirectory":return!0;default:return!1}};class BaseInterpreter{global;local;root;handler;resizeObserver;intersectionObserver;nodes;stack;templates;m;constructor(){}initialize(root,handler=null){this.global={},this.local={},this.root=root,this.nodes=[root],this.stack=[root],this.templates={},this.handler=handler,root.setAttribute("data-dioxus-id","0")}handleResizeEvent(entry){const target=entry.target;let event=new CustomEvent("resize",{bubbles:!1,detail:entry});target.dispatchEvent(event)}createResizeObserver(element){if(!this.resizeObserver)this.resizeObserver=new ResizeObserver((entries)=>{for(let entry of entries)this.handleResizeEvent(entry)});this.resizeObserver.observe(element)}removeResizeObserver(element){if(this.resizeObserver)this.resizeObserver.unobserve(element)}handleIntersectionEvent(entry){const target=entry.target;let event=new CustomEvent("visible",{bubbles:!1,detail:entry});target.dispatchEvent(event)}createIntersectionObserver(element){if(!this.intersectionObserver)this.intersectionObserver=new IntersectionObserver((entries)=>{for(let entry of entries)this.handleIntersectionEvent(entry)});this.intersectionObserver.observe(element)}removeIntersectionObserver(element){if(this.intersectionObserver)this.intersectionObserver.unobserve(element)}createListener(event_name,element,bubbles){if(event_name=="resize")this.createResizeObserver(element);else if(event_name=="visible")this.createIntersectionObserver(element);if(bubbles)if(this.global[event_name]===void 0)this.global[event_name]={active:1,callback:this.handler},this.root.addEventListener(event_name,this.handler);else this.global[event_name].active++;else{const id=element.getAttribute("data-dioxus-id");if(!this.local[id])this.local[id]={};element.addEventListener(event_name,this.handler)}}removeListener(element,event_name,bubbles){if(event_name=="resize")this.removeResizeObserver(element);else if(event_name=="visible")this.removeIntersectionObserver(element);else if(bubbles)this.removeBubblingListener(event_name);else this.removeNonBubblingListener(element,event_name)}removeBubblingListener(event_name){if(this.global[event_name].active--,this.global[event_name].active===0)this.root.removeEventListener(event_name,this.global[event_name].callback),delete this.global[event_name]}removeNonBubblingListener(element,event_name){const id=element.getAttribute("data-dioxus-id");if(delete this.local[id][event_name],Object.keys(this.local[id]).length===0)delete this.local[id];element.removeEventListener(event_name,this.handler)}removeAllNonBubblingListeners(element){const id=element.getAttribute("data-dioxus-id");delete this.local[id]}getNode(id){return this.nodes[id]}pushRoot(node){this.stack.push(node)}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k<many;k++)root.appendChild(els[k])}loadChild(ptr,len){let node=this.stack[this.stack.length-1],ptr_end=ptr+len;for(;ptr<ptr_end;ptr++){let end=this.m.getUint8(ptr);for(node=node.firstChild;end>0;end--)node=node.nextSibling}return node}saveTemplate(nodes,tmpl_id){this.templates[tmpl_id]=nodes}hydrate_node(hydrateNode,ids){const split=hydrateNode.getAttribute("data-node-hydration").split(","),id=ids[parseInt(split[0])];if(this.nodes[id]=hydrateNode,split.length>1){hydrateNode.listening=split.length-1,hydrateNode.setAttribute("data-dioxus-id",id.toString());for(let j=1;j<split.length;j++){const split2=split[j].split(":"),event_name=split2[0],bubbles=split2[1]==="1";this.createListener(event_name,hydrateNode,bubbles)}}}hydrate(ids,underNodes){for(let i=0;i<underNodes.length;i++){const under=underNodes[i];if(under instanceof HTMLElement){if(under.getAttribute("data-node-hydration"))this.hydrate_node(under,ids);const hydrateNodes=under.querySelectorAll("[data-node-hydration]");for(let i2=0;i2<hydrateNodes.length;i2++)this.hydrate_node(hydrateNodes[i2],ids)}const treeWalker=document.createTreeWalker(under,NodeFilter.SHOW_COMMENT);let nextSibling=under.nextSibling,continueToNextNode=()=>{if(!treeWalker.nextNode())return!1;return treeWalker.currentNode!==nextSibling};while(treeWalker.currentNode){const currentNode=treeWalker.currentNode;if(currentNode.nodeType===Node.COMMENT_NODE){const id=currentNode.textContent,placeholderSplit=id.split("placeholder");if(placeholderSplit.length>1){if(this.nodes[ids[parseInt(placeholderSplit[1])]]=currentNode,!continueToNextNode())break;continue}const textNodeSplit=id.split("node-id");if(textNodeSplit.length>1){let next=currentNode.nextSibling;currentNode.remove();let commentAfterText,textNode;if(next.nodeType===Node.COMMENT_NODE){const newText=next.parentElement.insertBefore(document.createTextNode(""),next);commentAfterText=next,textNode=newText}else textNode=next,commentAfterText=textNode.nextSibling;treeWalker.currentNode=commentAfterText,this.nodes[ids[parseInt(textNodeSplit[1])]]=textNode;let exit=currentNode===under||!continueToNextNode();if(commentAfterText.remove(),exit)break;continue}}if(!continueToNextNode())break}}}setAttributeInner(node,field,value,ns){setAttributeInner(node,field,value,ns)}}export{BaseInterpreter};

+ 1 - 1
packages/interpreter/src/js/hash.txt

@@ -1 +1 @@
-[6449103750905854967, 3846442265490457921, 13069001215487072322, 8716623267269178440, 5336385715226370016, 14456089431355876478, 12156139214887111728, 5052021921702764563, 12925655762638175824, 5638004933879392817]
+[6449103750905854967, 17669692872757955279, 13069001215487072322, 11420464406527728232, 3770103091118609057, 5444526391971481782, 12156139214887111728, 5052021921702764563, 12925655762638175824, 5638004933879392817]

+ 1 - 1
packages/interpreter/src/js/hydrate.js

@@ -1 +1 @@
-function register_rehydrate_chunk_for_streaming(callback){window.hydration_callback=callback;for(let i=0;i<window.hydrate_queue.length;i++){const[id,data]=window.hydrate_queue[i];window.hydration_callback(id,data)}}export{register_rehydrate_chunk_for_streaming};
+function register_rehydrate_chunk_for_streaming(callback){return register_rehydrate_chunk_for_streaming_debug(callback)}function register_rehydrate_chunk_for_streaming_debug(callback){window.hydration_callback=callback;for(let i=0;i<window.hydrate_queue.length;i++){const[id,data,debug_types,debug_locations]=window.hydrate_queue[i];window.hydration_callback(id,data,debug_types,debug_locations)}}export{register_rehydrate_chunk_for_streaming_debug,register_rehydrate_chunk_for_streaming};

+ 1 - 1
packages/interpreter/src/js/initialize_streaming.js

@@ -1 +1 @@
-window.hydrate_queue=[];window.dx_hydrate=(id,data)=>{const decoded=atob(data),bytes=Uint8Array.from(decoded,(c)=>c.charCodeAt(0));if(window.hydration_callback)window.hydration_callback(id,bytes);else window.hydrate_queue.push([id,bytes])};
+window.hydrate_queue=[];window.dx_hydrate=(id,data,debug_types,debug_locations)=>{const decoded=atob(data),bytes=Uint8Array.from(decoded,(c)=>c.charCodeAt(0));if(window.hydration_callback)window.hydration_callback(id,bytes,debug_types,debug_locations);else window.hydrate_queue.push([id,bytes,debug_types,debug_locations])};

+ 7 - 0
packages/interpreter/src/lib.rs

@@ -47,6 +47,13 @@ pub mod minimal_bindings {
         pub fn register_rehydrate_chunk_for_streaming(
         pub fn register_rehydrate_chunk_for_streaming(
             closure: &wasm_bindgen::closure::Closure<dyn FnMut(Vec<u32>, js_sys::Uint8Array)>,
             closure: &wasm_bindgen::closure::Closure<dyn FnMut(Vec<u32>, js_sys::Uint8Array)>,
         );
         );
+
+        /// Register a callback that that will be called to hydrate a node at the given id with data from the server
+        pub fn register_rehydrate_chunk_for_streaming_debug(
+            closure: &wasm_bindgen::closure::Closure<
+                dyn FnMut(Vec<u32>, js_sys::Uint8Array, Option<Vec<String>>, Option<Vec<String>>),
+            >,
+        );
     }
     }
 
 
     #[wasm_bindgen(module = "/src/js/patch_console.js")]
     #[wasm_bindgen(module = "/src/js/patch_console.js")]

+ 19 - 5
packages/interpreter/src/ts/core.ts

@@ -31,7 +31,7 @@ export class BaseInterpreter {
   // sledgehammer is generating this...
   // sledgehammer is generating this...
   m: any;
   m: any;
 
 
-  constructor() { }
+  constructor() {}
 
 
   initialize(root: HTMLElement, handler: EventListener | null = null) {
   initialize(root: HTMLElement, handler: EventListener | null = null) {
     this.global = {};
     this.global = {};
@@ -45,7 +45,7 @@ export class BaseInterpreter {
     this.handler = handler;
     this.handler = handler;
 
 
     // make sure to set the root element's ID so it still registers events
     // make sure to set the root element's ID so it still registers events
-    root.setAttribute('data-dioxus-id', "0");
+    root.setAttribute("data-dioxus-id", "0");
   }
   }
 
 
   handleResizeEvent(entry: ResizeObserverEntry) {
   handleResizeEvent(entry: ResizeObserverEntry) {
@@ -241,6 +241,18 @@ export class BaseInterpreter {
         NodeFilter.SHOW_COMMENT
         NodeFilter.SHOW_COMMENT
       );
       );
 
 
+      let nextSibling = under.nextSibling;
+      // Continue to the next node. Returns false if we should stop traversing because we've reached the end of the children
+      // of the root
+      let continueToNextNode = () => {
+        // stop traversing if there are no more nodes
+        if (!treeWalker.nextNode()) {
+          return false;
+        }
+        // stop traversing if we have reached the next sibling of the root node
+        return treeWalker.currentNode !== nextSibling;
+      };
+
       while (treeWalker.currentNode) {
       while (treeWalker.currentNode) {
         const currentNode = treeWalker.currentNode as ChildNode;
         const currentNode = treeWalker.currentNode as ChildNode;
         if (currentNode.nodeType === Node.COMMENT_NODE) {
         if (currentNode.nodeType === Node.COMMENT_NODE) {
@@ -251,7 +263,7 @@ export class BaseInterpreter {
 
 
           if (placeholderSplit.length > 1) {
           if (placeholderSplit.length > 1) {
             this.nodes[ids[parseInt(placeholderSplit[1])]] = currentNode;
             this.nodes[ids[parseInt(placeholderSplit[1])]] = currentNode;
-            if (!treeWalker.nextNode()) {
+            if (!continueToNextNode()) {
               break;
               break;
             }
             }
             continue;
             continue;
@@ -284,7 +296,9 @@ export class BaseInterpreter {
             }
             }
             treeWalker.currentNode = commentAfterText;
             treeWalker.currentNode = commentAfterText;
             this.nodes[ids[parseInt(textNodeSplit[1])]] = textNode;
             this.nodes[ids[parseInt(textNodeSplit[1])]] = textNode;
-            let exit = !treeWalker.nextNode();
+            // Stop traversing if we started on a comment node (which has no children)
+            // or the next sibling stops the walk
+            let exit = currentNode === under || !continueToNextNode();
             // remove the comment node after the text node
             // remove the comment node after the text node
             commentAfterText.remove();
             commentAfterText.remove();
             if (exit) {
             if (exit) {
@@ -293,7 +307,7 @@ export class BaseInterpreter {
             continue;
             continue;
           }
           }
         }
         }
-        if (!treeWalker.nextNode()) {
+        if (!continueToNextNode()) {
           break;
           break;
         }
         }
       }
       }

+ 9 - 4
packages/interpreter/src/ts/hydrate.ts

@@ -1,11 +1,16 @@
 import "./hydrate_types";
 import "./hydrate_types";
+import { HydrationCallback } from "./hydrate_types";
 
 
-export function register_rehydrate_chunk_for_streaming(
-  callback: (id: number[], data: Uint8Array) => void
+export function register_rehydrate_chunk_for_streaming(callback: HydrationCallback): void {
+  return register_rehydrate_chunk_for_streaming_debug(callback);
+}
+
+export function register_rehydrate_chunk_for_streaming_debug(
+  callback: HydrationCallback
 ): void {
 ): void {
   window.hydration_callback = callback;
   window.hydration_callback = callback;
   for (let i = 0; i < window.hydrate_queue.length; i++) {
   for (let i = 0; i < window.hydrate_queue.length; i++) {
-    const [id, data] = window.hydrate_queue[i];
-    window.hydration_callback(id, data);
+    const [id, data, debug_types, debug_locations] = window.hydrate_queue[i];
+    window.hydration_callback(id, data, debug_types, debug_locations);
   }
   }
 }
 }

+ 12 - 3
packages/interpreter/src/ts/hydrate_types.ts

@@ -1,8 +1,17 @@
-export {};
+export { };
+
+export type HydrationCallback = (
+  id: number[],
+  data: Uint8Array,
+  debug_types: string[] | null,
+  debug_locations: string[] | null
+) => void;
 
 
 declare global {
 declare global {
   interface Window {
   interface Window {
-    hydrate_queue: [number[], Uint8Array][];
-    hydration_callback: null | ((id: number[], data: Uint8Array) => void);
+    hydrate_queue: [number[], Uint8Array, string[] | null, string[] | null][];
+    hydration_callback:
+    | null
+    | HydrationCallback;
   }
   }
 }
 }

+ 8 - 3
packages/interpreter/src/ts/initialize_streaming.ts

@@ -4,13 +4,18 @@ import "./hydrate_types";
 window.hydrate_queue = [];
 window.hydrate_queue = [];
 
 
 // @ts-ignore
 // @ts-ignore
-window.dx_hydrate = (id: number[], data: string) => {
+window.dx_hydrate = (
+  id: number[],
+  data: string,
+  debug_types: string[] | null,
+  debug_locations: string[] | null
+) => {
   // First convert the base64 encoded string to a Uint8Array
   // First convert the base64 encoded string to a Uint8Array
   const decoded = atob(data);
   const decoded = atob(data);
   const bytes = Uint8Array.from(decoded, (c) => c.charCodeAt(0));
   const bytes = Uint8Array.from(decoded, (c) => c.charCodeAt(0));
   if (window.hydration_callback) {
   if (window.hydration_callback) {
-    window.hydration_callback(id, bytes);
+    window.hydration_callback(id, bytes, debug_types, debug_locations);
   } else {
   } else {
-    window.hydrate_queue.push([id, bytes]);
+    window.hydrate_queue.push([id, bytes, debug_types, debug_locations]);
   }
   }
 };
 };

+ 54 - 1
packages/liveview/src/document.rs

@@ -1,5 +1,9 @@
+use dioxus_core::prelude::queue_effect;
 use dioxus_core::ScopeId;
 use dioxus_core::ScopeId;
-use dioxus_document::{Document, Eval, EvalError, Evaluator};
+use dioxus_document::{
+    create_element_in_head, Document, Eval, EvalError, Evaluator, LinkProps, MetaProps,
+    ScriptProps, StyleProps,
+};
 use dioxus_history::History;
 use dioxus_history::History;
 use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
 use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
 use std::rc::Rc;
 use std::rc::Rc;
@@ -75,6 +79,7 @@ pub fn init_document() {
 }
 }
 
 
 /// Reprints the liveview-target's provider of evaluators.
 /// Reprints the liveview-target's provider of evaluators.
+#[derive(Clone)]
 pub struct LiveviewDocument {
 pub struct LiveviewDocument {
     query: QueryEngine,
     query: QueryEngine,
 }
 }
@@ -83,4 +88,52 @@ impl Document for LiveviewDocument {
     fn eval(&self, js: String) -> Eval {
     fn eval(&self, js: String) -> Eval {
         Eval::new(LiveviewEvaluator::create(self.query.clone(), js))
         Eval::new(LiveviewEvaluator::create(self.query.clone(), js))
     }
     }
+
+    /// Set the title of the document
+    fn set_title(&self, title: String) {
+        let myself = self.clone();
+        queue_effect(move || {
+            myself.eval(format!("document.title = {title:?};"));
+        });
+    }
+
+    /// Create a new meta tag in the head
+    fn create_meta(&self, props: MetaProps) {
+        let myself = self.clone();
+        queue_effect(move || {
+            myself.eval(create_element_in_head("meta", &props.attributes(), None));
+        });
+    }
+
+    /// Create a new script tag in the head
+    fn create_script(&self, props: ScriptProps) {
+        let myself = self.clone();
+        queue_effect(move || {
+            myself.eval(create_element_in_head(
+                "script",
+                &props.attributes(),
+                props.script_contents().ok(),
+            ));
+        });
+    }
+
+    /// Create a new style tag in the head
+    fn create_style(&self, props: StyleProps) {
+        let myself = self.clone();
+        queue_effect(move || {
+            myself.eval(create_element_in_head(
+                "style",
+                &props.attributes(),
+                props.style_contents().ok(),
+            ));
+        });
+    }
+
+    /// Create a new link tag in the head
+    fn create_link(&self, props: LinkProps) {
+        let myself = self.clone();
+        queue_effect(move || {
+            myself.eval(create_element_in_head("link", &props.attributes(), None));
+        });
+    }
 }
 }

+ 0 - 0
packages/playwright-tests/nested-suspense/assets/style.css


+ 4 - 0
packages/playwright-tests/nested-suspense/src/main.rs

@@ -23,6 +23,9 @@ fn app() -> Element {
     rsx! {
     rsx! {
         SuspenseBoundary {
         SuspenseBoundary {
             fallback: move |_| rsx! {},
             fallback: move |_| rsx! {},
+            document::Style {
+                href: asset!("/assets/style.css")
+            }
             LoadTitle {}
             LoadTitle {}
         }
         }
         MessageWithLoader { id: 0 }
         MessageWithLoader { id: 0 }
@@ -48,6 +51,7 @@ fn LoadTitle() -> Element {
         .unwrap();
         .unwrap();
 
 
     rsx! {
     rsx! {
+        "title loaded"
         document::Title { "{title.title}" }
         document::Title { "{title.title}" }
     }
     }
 }
 }

+ 54 - 1
packages/web/src/document.rs

@@ -1,5 +1,9 @@
+use dioxus_core::prelude::queue_effect;
 use dioxus_core::ScopeId;
 use dioxus_core::ScopeId;
-use dioxus_document::{Document, Eval, EvalError, Evaluator};
+use dioxus_document::{
+    create_element_in_head, Document, Eval, EvalError, Evaluator, LinkProps, MetaProps,
+    ScriptProps, StyleProps,
+};
 use dioxus_history::History;
 use dioxus_history::History;
 use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
 use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
 use js_sys::Function;
 use js_sys::Function;
@@ -69,11 +73,60 @@ pub fn init_document() {
 }
 }
 
 
 /// The web-target's document provider.
 /// The web-target's document provider.
+#[derive(Clone)]
 pub struct WebDocument;
 pub struct WebDocument;
 impl Document for WebDocument {
 impl Document for WebDocument {
     fn eval(&self, js: String) -> Eval {
     fn eval(&self, js: String) -> Eval {
         Eval::new(WebEvaluator::create(js))
         Eval::new(WebEvaluator::create(js))
     }
     }
+
+    /// Set the title of the document
+    fn set_title(&self, title: String) {
+        let myself = self.clone();
+        queue_effect(move || {
+            myself.eval(format!("document.title = {title:?};"));
+        });
+    }
+
+    /// Create a new meta tag in the head
+    fn create_meta(&self, props: MetaProps) {
+        let myself = self.clone();
+        queue_effect(move || {
+            myself.eval(create_element_in_head("meta", &props.attributes(), None));
+        });
+    }
+
+    /// Create a new script tag in the head
+    fn create_script(&self, props: ScriptProps) {
+        let myself = self.clone();
+        queue_effect(move || {
+            myself.eval(create_element_in_head(
+                "script",
+                &props.attributes(),
+                props.script_contents().ok(),
+            ));
+        });
+    }
+
+    /// Create a new style tag in the head
+    fn create_style(&self, props: StyleProps) {
+        let myself = self.clone();
+        queue_effect(move || {
+            myself.eval(create_element_in_head(
+                "style",
+                &props.attributes(),
+                props.style_contents().ok(),
+            ));
+        });
+    }
+
+    /// Create a new link tag in the head
+    fn create_link(&self, props: LinkProps) {
+        let myself = self.clone();
+        queue_effect(move || {
+            myself.eval(create_element_in_head("link", &props.attributes(), None));
+        });
+    }
 }
 }
 
 
 /// Required to avoid blocking the Rust WASM thread.
 /// Required to avoid blocking the Rust WASM thread.

+ 49 - 7
packages/web/src/hydration/deserialize.rs

@@ -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.
 /// 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
 /// This will return `None` if no data was pushed for this instance or if serialization fails
 // TODO: evan better docs
 // TODO: evan better docs
+#[track_caller]
 pub fn take_server_data<T: DeserializeOwned>() -> Result<Option<T>, TakeDataError> {
 pub fn take_server_data<T: DeserializeOwned>() -> Result<Option<T>, TakeDataError> {
     SERVER_DATA.with_borrow(|data| match data.as_ref() {
     SERVER_DATA.with_borrow(|data| match data.as_ref() {
         Some(data) => data.take(),
         Some(data) => data.take(),
@@ -40,13 +41,21 @@ fn remove_server_data() {
 pub(crate) struct HTMLDataCursor {
 pub(crate) struct HTMLDataCursor {
     error: Option<CapturedError>,
     error: Option<CapturedError>,
     data: Vec<Option<Vec<u8>>>,
     data: Vec<Option<Vec<u8>>>,
+    #[cfg(debug_assertions)]
+    debug_types: Option<Vec<String>>,
+    #[cfg(debug_assertions)]
+    debug_locations: Option<Vec<String>>,
     index: Cell<usize>,
     index: Cell<usize>,
 }
 }
 
 
 impl HTMLDataCursor {
 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();
         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
     /// Get the error if there is one
@@ -54,11 +63,19 @@ impl HTMLDataCursor {
         self.error.clone()
         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 {
         let mut myself = Self {
+            index: Cell::new(0),
             error: None,
             error: None,
             data,
             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
         // The first item is always an error if it exists
@@ -73,6 +90,7 @@ impl HTMLDataCursor {
         myself
         myself
     }
     }
 
 
+    #[track_caller]
     pub fn take<T: DeserializeOwned>(&self) -> Result<Option<T>, TakeDataError> {
     pub fn take<T: DeserializeOwned>(&self) -> Result<Option<T>, TakeDataError> {
         let current = self.index.get();
         let current = self.index.get();
         if current >= self.data.len() {
         if current >= self.data.len() {
@@ -88,9 +106,33 @@ impl HTMLDataCursor {
         match bytes {
         match bytes {
             Some(bytes) => match ciborium::from_reader(Cursor::new(bytes)) {
             Some(bytes) => match ciborium::from_reader(Cursor::new(bytes)) {
                 Ok(x) => Ok(Some(x)),
                 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),
             None => Ok(None),

+ 29 - 11
packages/web/src/hydration/hydrate.rs

@@ -3,8 +3,6 @@
 //! 2. As we render the virtual dom initially, keep track of the server ids of the suspense boundaries
 //! 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
 //! 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::dom::WebsysDom;
 use crate::with_server_data;
 use crate::with_server_data;
 use crate::HTMLDataCursor;
 use crate::HTMLDataCursor;
@@ -12,6 +10,7 @@ use dioxus_core::prelude::*;
 use dioxus_core::AttributeValue;
 use dioxus_core::AttributeValue;
 use dioxus_core::{DynamicNode, ElementId};
 use dioxus_core::{DynamicNode, ElementId};
 use futures_channel::mpsc::UnboundedReceiver;
 use futures_channel::mpsc::UnboundedReceiver;
+use std::fmt::Write;
 use RehydrationError::*;
 use RehydrationError::*;
 
 
 use super::SuspenseMessage;
 use super::SuspenseMessage;
@@ -113,6 +112,10 @@ impl WebsysDom {
         let SuspenseMessage {
         let SuspenseMessage {
             suspense_path,
             suspense_path,
             data,
             data,
+            #[cfg(debug_assertions)]
+            debug_types,
+            #[cfg(debug_assertions)]
+            debug_locations,
         } = message;
         } = message;
 
 
         let document = web_sys::window().unwrap().document().unwrap();
         let document = web_sys::window().unwrap().document().unwrap();
@@ -138,7 +141,12 @@ impl WebsysDom {
             self.interpreter.base().push_root(node);
             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 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() {
             dom.in_runtime(|| id.throw_error(error));
             dom.in_runtime(|| id.throw_error(error));
@@ -205,15 +213,25 @@ impl WebsysDom {
         vdom: &VirtualDom,
         vdom: &VirtualDom,
     ) -> Result<UnboundedReceiver<SuspenseMessage>, RehydrationError> {
     ) -> Result<UnboundedReceiver<SuspenseMessage>, RehydrationError> {
         let (mut tx, rx) = futures_channel::mpsc::unbounded();
         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);
         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();
         closure.forget();
 
 
         // Rehydrate the root scope that was rendered on the server. We will likely run into suspense boundaries.
         // Rehydrate the root scope that was rendered on the server. We will likely run into suspense boundaries.

+ 8 - 0
packages/web/src/hydration/mod.rs

@@ -18,4 +18,12 @@ pub(crate) struct SuspenseMessage {
     #[cfg(feature = "hydrate")]
     #[cfg(feature = "hydrate")]
     /// The data to hydrate the suspense boundary with
     /// The data to hydrate the suspense boundary with
     data: Vec<u8>,
     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>>,
 }
 }

+ 21 - 1
packages/web/src/lib.rs

@@ -88,12 +88,32 @@ pub async fn run(mut virtual_dom: VirtualDom, web_config: Config) -> ! {
                     const decoded = atob(window.initial_dioxus_hydration_data);
                     const decoded = atob(window.initial_dioxus_hydration_data);
                     return Uint8Array.from(decoded, (c) => c.charCodeAt(0))
                     return Uint8Array.from(decoded, (c) => c.charCodeAt(0))
                 }
                 }
+                export function get_initial_hydration_debug_types() {
+                    return window.initial_dioxus_hydration_debug_types;
+                }
+                export function get_initial_hydration_debug_locations() {
+                    return window.initial_dioxus_hydration_debug_locations;
+                }
             "#)]
             "#)]
             extern "C" {
             extern "C" {
                 fn get_initial_hydration_data() -> js_sys::Uint8Array;
                 fn get_initial_hydration_data() -> js_sys::Uint8Array;
+                fn get_initial_hydration_debug_types() -> Option<Vec<String>>;
+                fn get_initial_hydration_debug_locations() -> Option<Vec<String>>;
             }
             }
             let hydration_data = get_initial_hydration_data().to_vec();
             let hydration_data = get_initial_hydration_data().to_vec();
-            let server_data = HTMLDataCursor::from_serialized(&hydration_data);
+
+            // 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);
             // If the server serialized an error into the root suspense boundary, throw it into the root scope
             // 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() {
                 virtual_dom.in_runtime(|| dioxus_core::ScopeId::APP.throw_error(error));
                 virtual_dom.in_runtime(|| dioxus_core::ScopeId::APP.throw_error(error));