use crate::dom::WebsysDom;
use crate::with_server_data;
use crate::HTMLDataCursor;
use dioxus_core::prelude::*;
use dioxus_core::AttributeValue;
use dioxus_core::{DynamicNode, ElementId};
use futures_channel::mpsc::UnboundedReceiver;
use std::fmt::Write;
use RehydrationError::*;
use super::SuspenseMessage;
#[derive(Debug)]
#[non_exhaustive]
pub(crate) enum RehydrationError {
VNodeNotInitialized,
SuspenseHydrationIdNotFound,
ElementNotFound,
}
#[derive(Debug)]
struct SuspenseHydrationIdsNode {
scope_id: ScopeId,
children: Vec<SuspenseHydrationIdsNode>,
}
impl SuspenseHydrationIdsNode {
fn new(scope_id: ScopeId) -> Self {
Self {
scope_id,
children: Vec::new(),
}
}
fn traverse(&self, path: &[u32]) -> Option<&Self> {
match path {
[] => Some(self),
[id, rest @ ..] => self.children.get(*id as usize)?.traverse(rest),
}
}
fn traverse_mut(&mut self, path: &[u32]) -> Option<&mut Self> {
match path {
[] => Some(self),
[id, rest @ ..] => self.children.get_mut(*id as usize)?.traverse_mut(rest),
}
}
}
#[derive(Default, Debug)]
pub(crate) struct SuspenseHydrationIds {
children: Vec<SuspenseHydrationIdsNode>,
current_path: Vec<u32>,
}
impl SuspenseHydrationIds {
fn add_suspense_boundary(&mut self, id: ScopeId) {
match self.current_path.as_slice() {
[] => {
self.children.push(SuspenseHydrationIdsNode::new(id));
}
[first_index, rest @ ..] => {
let child_node = self.children[*first_index as usize]
.traverse_mut(rest)
.unwrap();
child_node.children.push(SuspenseHydrationIdsNode::new(id));
}
}
}
fn get_suspense_boundary(&self, path: &[u32]) -> Option<ScopeId> {
let root = self.children.get(path[0] as usize)?;
root.traverse(&path[1..]).map(|node| node.scope_id)
}
}
impl WebsysDom {
pub fn rehydrate_streaming(&mut self, message: SuspenseMessage, dom: &mut VirtualDom) {
if let Err(err) = self.rehydrate_streaming_inner(message, dom) {
tracing::error!("Rehydration failed. {:?}", err);
}
}
fn rehydrate_streaming_inner(
&mut self,
message: SuspenseMessage,
dom: &mut VirtualDom,
) -> Result<(), RehydrationError> {
let SuspenseMessage {
suspense_path,
data,
#[cfg(debug_assertions)]
debug_types,
#[cfg(debug_assertions)]
debug_locations,
} = message;
let document = web_sys::window().unwrap().document().unwrap();
let resolved_suspense_id = path_to_resolved_suspense_id(&suspense_path);
let resolved_suspense_element = document
.get_element_by_id(&resolved_suspense_id)
.ok_or(RehydrationError::ElementNotFound)?;
let id = self
.suspense_hydration_ids
.get_suspense_boundary(&suspense_path)
.ok_or(RehydrationError::SuspenseHydrationIdNotFound)?;
let mut current_child = resolved_suspense_element.first_child();
let mut children = Vec::new();
while let Some(node) = current_child {
children.push(node.clone());
current_child = node.next_sibling();
self.interpreter.base().push_root(node);
}
#[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 let Some(error) = server_data.error() {
dom.in_runtime(|| id.throw_error(error));
}
with_server_data(server_data, || {
SuspenseBoundaryProps::resolve_suspense(
id,
dom,
self,
|to| {
to.skip_mutations = true;
},
children.len(),
);
self.skip_mutations = false;
});
self.flush_edits();
resolved_suspense_element.remove();
let Some(root_scope) = dom.get_scope(id) else {
return Ok(());
};
self.suspense_hydration_ids
.current_path
.clone_from(&suspense_path);
self.start_hydration_at_scope(root_scope, dom, children)?;
Ok(())
}
fn start_hydration_at_scope(
&mut self,
scope: &ScopeState,
dom: &VirtualDom,
under: Vec<web_sys::Node>,
) -> Result<(), RehydrationError> {
let mut ids = Vec::new();
let mut to_mount = Vec::new();
self.rehydrate_scope(scope, dom, &mut ids, &mut to_mount)?;
self.interpreter.base().hydrate(ids, under);
#[cfg(feature = "mounted")]
for id in to_mount {
self.send_mount_event(id);
}
Ok(())
}
pub fn rehydrate(
&mut self,
vdom: &VirtualDom,
) -> Result<UnboundedReceiver<SuspenseMessage>, RehydrationError> {
let (mut tx, rx) = futures_channel::mpsc::unbounded();
let closure =
move |path: Vec<u32>,
data: js_sys::Uint8Array,
#[allow(unused)] debug_types: Option<Vec<String>>,
#[allow(unused)] debug_locations: Option<Vec<String>>| {
let data = data.to_vec();
_ = tx.start_send(SuspenseMessage {
suspense_path: path,
data,
#[cfg(debug_assertions)]
debug_types,
#[cfg(debug_assertions)]
debug_locations,
});
};
let closure = wasm_bindgen::closure::Closure::new(closure);
dioxus_interpreter_js::minimal_bindings::register_rehydrate_chunk_for_streaming_debug(
&closure,
);
closure.forget();
self.start_hydration_at_scope(vdom.base_scope(), vdom, vec![self.root.clone()])?;
Ok(rx)
}
fn rehydrate_scope(
&mut self,
scope: &ScopeState,
dom: &VirtualDom,
ids: &mut Vec<u32>,
to_mount: &mut Vec<ElementId>,
) -> Result<(), RehydrationError> {
if let Some(suspense) =
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime(), scope.id())
{
if suspense.has_suspended_tasks() {
self.suspense_hydration_ids
.add_suspense_boundary(scope.id());
}
}
self.rehydrate_vnode(dom, scope.root_node(), ids, to_mount)
}
fn rehydrate_vnode(
&mut self,
dom: &VirtualDom,
vnode: &VNode,
ids: &mut Vec<u32>,
to_mount: &mut Vec<ElementId>,
) -> Result<(), RehydrationError> {
for (i, root) in vnode.template.roots.iter().enumerate() {
self.rehydrate_template_node(
dom,
vnode,
root,
ids,
to_mount,
Some(vnode.mounted_root(i, dom).ok_or(VNodeNotInitialized)?),
)?;
}
Ok(())
}
fn rehydrate_template_node(
&mut self,
dom: &VirtualDom,
vnode: &VNode,
node: &TemplateNode,
ids: &mut Vec<u32>,
to_mount: &mut Vec<ElementId>,
root_id: Option<ElementId>,
) -> Result<(), RehydrationError> {
match node {
TemplateNode::Element {
children, attrs, ..
} => {
let mut mounted_id = root_id;
for attr in *attrs {
if let dioxus_core::TemplateAttribute::Dynamic { id } = attr {
let attributes = &*vnode.dynamic_attrs[*id];
let id = vnode
.mounted_dynamic_attribute(*id, dom)
.ok_or(VNodeNotInitialized)?;
for attribute in attributes {
let value = &attribute.value;
mounted_id = Some(id);
if let AttributeValue::Listener(_) = value {
if attribute.name == "onmounted" {
to_mount.push(id);
}
}
}
}
}
if let Some(id) = mounted_id {
ids.push(id.0 as u32);
}
if !children.is_empty() {
for child in *children {
self.rehydrate_template_node(dom, vnode, child, ids, to_mount, None)?;
}
}
}
TemplateNode::Dynamic { id } => self.rehydrate_dynamic_node(
dom,
&vnode.dynamic_nodes[*id],
*id,
vnode,
ids,
to_mount,
)?,
TemplateNode::Text { .. } => {
if let Some(id) = root_id {
ids.push(id.0 as u32);
}
}
}
Ok(())
}
fn rehydrate_dynamic_node(
&mut self,
dom: &VirtualDom,
dynamic: &DynamicNode,
dynamic_node_index: usize,
vnode: &VNode,
ids: &mut Vec<u32>,
to_mount: &mut Vec<ElementId>,
) -> Result<(), RehydrationError> {
match dynamic {
dioxus_core::DynamicNode::Text(_) | dioxus_core::DynamicNode::Placeholder(_) => {
ids.push(
vnode
.mounted_dynamic_node(dynamic_node_index, dom)
.ok_or(VNodeNotInitialized)?
.0 as u32,
);
}
dioxus_core::DynamicNode::Component(comp) => {
let scope = comp
.mounted_scope(dynamic_node_index, vnode, dom)
.ok_or(VNodeNotInitialized)?;
self.rehydrate_scope(scope, dom, ids, to_mount)?;
}
dioxus_core::DynamicNode::Fragment(fragment) => {
for vnode in fragment {
self.rehydrate_vnode(dom, vnode, ids, to_mount)?;
}
}
}
Ok(())
}
}
fn write_comma_separated(id: &[u32], into: &mut String) {
let mut iter = id.iter();
if let Some(first) = iter.next() {
write!(into, "{first}").unwrap();
}
for id in iter {
write!(into, ",{id}").unwrap();
}
}
fn path_to_resolved_suspense_id(path: &[u32]) -> String {
let mut resolved_suspense_id_formatted = String::from("ds-");
write_comma_separated(path, &mut resolved_suspense_id_formatted);
resolved_suspense_id_formatted.push_str("-r");
resolved_suspense_id_formatted
}