Suspense boundaries/out of order streaming/anyhow like error handling (#2365)

* create static site generation helpers in the router crate

* work on integrating static site generation into fullstack

* move ssg into a separate crate

* integrate ssg with the launch builder

* simplify ssg example

* fix static_routes for child routes

* move CLI hot reloading websocket code into dioxus-hot-reload

* fix some unused imports

* use the same hot reloading websocket code for fullstack

* fix fullstack hot reloading

* move cli hot reloading logic into the hot reload crate

* ssg example working with dx serve

* add more examples

* fix clippy

* switch to a result for Element

* fix formatting

* fix hot reload doctest imports

* fix axum imports

* add show method to error context

* implement retaining nodes during suspense

* fix unterminated if statements

* communicate between tasks and suspense boundaries

* make suspense placeholders easier to use

* implement IntoDynNode and IntoVNode for more wrappers

* fix clippy examples

* fix rsx tests

* add streaming html utilities to the ssr package

* unify hydration and non-hydration ssr cache

* fix router with Result Element

* don't run server doc tests

* Fix hot reload websocket doc examples

* simple apps working with fullstack streaming

* fix preloading wasm

* Report errors encountered while streaming

* remove async from incremental renderer

* document new VirtualDom suspense methods

* make streaming work with incremental rendering

* fix static site generation

* document suspense structs

* create closure type; allow async event handlers in props; allow shorthand event handlers

* test forwarding event handlers with the shorthand syntax

* fix clippy

* fix imports in spawn async doctest

* fix empty rsx

* fix async result event handlers

* fix mounting router in multiple places

* Fix task dead cancel race condition

* simplify diffing before adding suspense

* fix binary size increase

* fix attribute diffing

* more diffing fixes

* create minimal fullstack feature

* smaller fullstack bundles

* allow mounting nodes that are already created and creating nodes without mounting them

* fix hot reload feature

* fix replacing components

* don't reclaim virtual nodes

* client side suspense working!

* fix CLI

* slightly smaller fullstack builds

* fix multiple suspended scopes

* fix merge errors

* yield back to tokio every few polls to fix suspending on many tasks at once

* remove logs

* document suspense boundary and update suspense example

* fix ssg

* make streaming optional

* fix some router and core tests

* fix suspense example

* fix serialization with out of order server futures

* add incremental streaming hackernews demo

* fix hackernews demo

* fix root hackernews redirect

* fix formatting

* add tests for suspense cases

* slightly smaller binaries

* slightly smaller

* improve error handling docs

* fix errors example link

* fix doc tests

* remove log file

* fix ssr cache type inference

* remove index.html

* fix ssg render template

* fix assigning ids on elements with dynamic attributes

* add desktop feature to the workspace examples

* remove router static generation example; ssg lives in the dioxus-static-generation package

* add a test for effects during suspense

* only run effects on mounted nodes

* fix multiple suspense roots

* fix node iterator

* fix closures without arguments

* fix dioxus-core readme doctest

* remove suspense logs

* fix scope stack

* fix clippy

* remove unused suspense boundary from hackernews

* assert that launch never returns for better compiler errors

* fix static generation launch function

* fix web renderer

* pass context providers into server functions

* add an example for FromContext

* clean up DioxusRouterExt

* fix server function context

* fix fullstack desktop example

* forward CLI serve settings to fullstack

* re-export serve config at the root of fullstack

* forward env directly instead of using a guard

* just set the port in the CLI for fullstack playwright tests

* fix fullstack dioxus-cli-config feature

* fix launch server merge conflicts

* fix fullstack launch context

* Merge branch 'main' into suspense-2.0

* fix fullstack html data

* remove drop virtual dom feature

* add a comment about only_write_templates binary size workaround

* remove explicit dependencies from use_server_future

* make ErrorContext and SuspenseContext more similar

* Tweak: small tweaks to tomls to make diff smaller

* only rerun components under suspense after the initial placeholders are sent to the client

* add module docs for suspense

* keep track of when suspense boundaries are resolved

* start implementing JS out of order streaming

* fix core tests

* implement the server side of suspense with js

* fix streaming ssr with nested suspense

* move streaming ssr code into fullstack

* revert minification changes

* serialize server future data as the html streams

* start loading scripts wasm immediately instead of defering the script

* very basic nested suspense example working with minimal html updates

* clean up some suspense/error docs

* fix hydrating nested pending server futures

* sort resolved boundaries by height

* Fix disconnecting clients while streaming

* fix static generation crate

* don't insert extra divs when hydrating streamed chunks

* wait to swap in the elements until they are hydrated

* remove inline streaming script

* hackernews partially working

* fix spa mode

* banish the open shadow dom

* fix removing placeholder

* set up streaming playwright test

* run web playwright tests on 9999 to avoid port conflicts with other local servers

* remove suspense nodes if the suspense boundary is replaced before the suspense resolves on the server

* ignore hydration of removed suspense boundaries

* use path based indexing to fix hydrating suspense after parent suspense with child is removed

* re-export dioxus error

* remove resolved suspense divs if the suspense boundary has been removed

* Fix client side initialized server futures

* ignore comment nodes while traversing nodes in core to avoid lists getting swapped out with suspense

* Pass initial hydration data to the client

* hide pre nodes

* don't panic if reclaiming an element fails

* fix scope stack when polling tasks

* improve deserialization out of length message

* Ok(VNode::placeholder()) -> VNode::empty()

* fix typo in rsx usage

* restore testing changes from suspense example

* clean up some logs and comments

* fix playwright tests

* clean up more changes in core

* clean up core tests

* remove anymap dependency

* clean up changes to hooks

* clean up changes in the router, rsx, and web

* revert changes to axum-hello-world

* fix use_server_future

* fix clippy in dioxus-core

* check that the next or previous node exist before checking if we should ignore them

* fix formatting

* fix suspense playwright test

* remove unused suspense code

* add more suspense playwright tests

* add more docs for error boundaries

* fix suspense core tests

* fix ErrorBoundary example

* remove a bunch of debug logging in js

* fix router failure_external_navigation

* use absolute paths in the interpreter build.rs

* strip '\r' while hashing ts files

* add a wrapper with a default error boundary and suspense boundary

* restore hot reloading

* ignore non-ts files when hashing

* sort ts files before hashing them

* fix rsx tests

* fix fullstack doc tests

* fix core tests

* fix axum auth example

* update suspense hydration diagram

* longer playwright build limit

* tiny fixes - spelling, formatting

* update diagram link

* remove comment and template nodes for suspense placeholders

* remove comment nodes as we hydrate text

* simplify hackernews example

* clean up hydrating text nodes

* switch to a separate environment variable for the base path for smaller binaries

* clean up file system html trait

* fix form data

* move streaming code into fullstack

* implement serialize and deserialize for CapturedError

* remove waits in the nested suspense playwright spec

* force sequential fullstack builds for CI

* longer nested suspense delay for CI

* fix --force-sequential flag

* wait to launch server until client build is done

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
Evan AlmloffandJonathan Kelley authored and GitHub committed 2024-07-01 20:50:36 -07:00
1 parent ffa36a67c3
commit 022e4ad203
175 files changed
+7872 -4010

No files matched your search

+24 -1
View File
@@ -29,6 +29,25 @@ pub struct WebsysDom {
#[cfg(feature = "mounted")]
pub(crate) queued_mounted_events: Vec<ElementId>,
// We originally started with a different `WriteMutations` for collecting templates during hydration.
// When profiling the binary size of web applications, this caused a large increase in binary size
// because diffing code in core is generic over the `WriteMutation` object.
//
// The fact that diffing is generic over WriteMutations instead of dynamic dispatch or a vec is nice
// because we can directly write mutations to sledgehammer and avoid the runtime and binary size overhead
// of dynamic dispatch
//
// Instead we now store a flag to see if we should be writing templates at all if hydration is enabled.
// This has a small overhead, but it avoids dynamic dispatch and reduces the binary size
//
// NOTE: running the virtual dom with the `only_write_templates` flag set to true is different from running
// it with no mutation writer because it still assigns ids to nodes, but it doesn't write them to the dom
#[cfg(feature = "hydrate")]
pub(crate) only_write_templates: bool,
#[cfg(feature = "hydrate")]
pub(crate) suspense_hydration_ids: crate::hydration::SuspenseHydrationIds,
}
pub struct UiEvent {
@@ -86,7 +105,7 @@ impl WebsysDom {
.map(|f| f.split_whitespace())
{
prevent_event = prevent_requests
.map(|f| f.trim_start_matches("on"))
.map(|f| f.strip_prefix("on").unwrap_or(f))
.any(|f| f == name);
} else {
prevent_event = false;
@@ -131,6 +150,10 @@ impl WebsysDom {
event_channel,
#[cfg(feature = "mounted")]
queued_mounted_events: Default::default(),
#[cfg(feature = "hydrate")]
only_write_templates: false,
#[cfg(feature = "hydrate")]
suspense_hydration_ids: Default::default(),
}
}
+18 -20
View File
@@ -4,12 +4,12 @@ use dioxus_html::{
point_interaction::{
InteractionElementOffset, InteractionLocation, ModifiersInteraction, PointerInteraction,
},
DragData, FileEngine, FormData, FormValue, HasDragData, HasFileData, HasFormData, HasImageData,
DragData, FormData, FormValue, HasDragData, HasFileData, HasFormData, HasImageData,
HasMouseData, HtmlEventConverter, ImageData, MountedData, PlatformEventData, ScrollData,
};
use js_sys::Array;
use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue};
use web_sys::{Document, DragEvent, Element, Event, MouseEvent};
use web_sys::{Document, Element, Event, MouseEvent};
pub(crate) struct WebEventConverter;
@@ -423,18 +423,15 @@ impl HasFormData for WebFormData {
}
impl HasFileData for WebFormData {
fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
#[cfg(not(feature = "file_engine"))]
let files = None;
#[cfg(feature = "file_engine")]
#[cfg(feature = "file_engine")]
fn files(&self) -> Option<std::sync::Arc<dyn dioxus_html::FileEngine>> {
let files = self
.element
.dyn_ref()
.and_then(|input: &web_sys::HtmlInputElement| {
input.files().and_then(|files| {
#[allow(clippy::arc_with_non_send_sync)]
crate::file_engine::WebFileEngine::new(files).map(|f| {
dioxus_html::WebFileEngine::new(files).map(|f| {
std::sync::Arc::new(f) as std::sync::Arc<dyn dioxus_html::FileEngine>
})
})
@@ -507,20 +504,21 @@ impl InteractionLocation for WebDragData {
}
impl HasFileData for WebDragData {
fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
#[cfg(not(feature = "file_engine"))]
let files = None;
#[cfg(feature = "file_engine")]
let files = self.raw.dyn_ref::<DragEvent>().and_then(|drag_event| {
drag_event.data_transfer().and_then(|dt| {
dt.files().and_then(|files| {
#[allow(clippy::arc_with_non_send_sync)]
crate::file_engine::WebFileEngine::new(files).map(|f| {
std::sync::Arc::new(f) as std::sync::Arc<dyn dioxus_html::FileEngine>
#[cfg(feature = "file_engine")]
fn files(&self) -> Option<std::sync::Arc<dyn dioxus_html::FileEngine>> {
let files = self
.raw
.dyn_ref::<web_sys::DragEvent>()
.and_then(|drag_event| {
drag_event.data_transfer().and_then(|dt| {
dt.files().and_then(|files| {
#[allow(clippy::arc_with_non_send_sync)]
dioxus_html::WebFileEngine::new(files).map(|f| {
std::sync::Arc::new(f) as std::sync::Arc<dyn dioxus_html::FileEngine>
})
})
})
})
});
});
files
}
-131
View File
@@ -1,131 +0,0 @@
use std::any::Any;
use dioxus_html::FileEngine;
use futures_channel::oneshot;
use js_sys::Uint8Array;
use wasm_bindgen::{prelude::Closure, JsCast};
use web_sys::{File, FileList, FileReader};
pub(crate) struct WebFileEngine {
file_reader: FileReader,
file_list: FileList,
}
impl WebFileEngine {
pub fn new(file_list: FileList) -> Option<Self> {
Some(Self {
file_list,
file_reader: FileReader::new().ok()?,
})
}
fn len(&self) -> usize {
self.file_list.length() as usize
}
fn get(&self, index: usize) -> Option<File> {
self.file_list.item(index as u32)
}
fn find(&self, name: &str) -> Option<File> {
(0..self.len())
.filter_map(|i| self.get(i))
.find(|f| f.name() == name)
}
}
#[async_trait::async_trait(?Send)]
impl FileEngine for WebFileEngine {
fn files(&self) -> Vec<String> {
(0..self.len())
.filter_map(|i| self.get(i).map(|f| f.name()))
.collect()
}
async fn file_size(&self, file: &str) -> Option<u64> {
let file = self.find(file)?;
Some(file.size() as u64)
}
// read a file to bytes
async fn read_file(&self, file: &str) -> Option<Vec<u8>> {
let file = self.find(file)?;
let file_reader = self.file_reader.clone();
let (rx, tx) = oneshot::channel();
let on_load: Closure<dyn FnMut()> = Closure::new({
let mut rx = Some(rx);
move || {
let result = file_reader.result();
let _ = rx
.take()
.expect("multiple files read without refreshing the channel")
.send(result);
}
});
self.file_reader
.set_onload(Some(on_load.as_ref().unchecked_ref()));
on_load.forget();
self.file_reader.read_as_array_buffer(&file).ok()?;
if let Ok(Ok(js_val)) = tx.await {
let as_u8_arr = Uint8Array::new(&js_val);
let as_u8_vec = as_u8_arr.to_vec();
Some(as_u8_vec)
} else {
None
}
}
// read a file to string
async fn read_file_to_string(&self, file: &str) -> Option<String> {
let file = self.find(file)?;
let file_reader = self.file_reader.clone();
let (rx, tx) = oneshot::channel();
let on_load: Closure<dyn FnMut()> = Closure::new({
let mut rx = Some(rx);
move || {
let result = file_reader.result();
let _ = rx
.take()
.expect("multiple files read without refreshing the channel")
.send(result);
}
});
self.file_reader
.set_onload(Some(on_load.as_ref().unchecked_ref()));
on_load.forget();
self.file_reader.read_as_text(&file).ok()?;
if let Ok(Ok(js_val)) = tx.await {
js_val.as_string()
} else {
None
}
}
async fn get_native_file(&self, file: &str) -> Option<Box<dyn Any>> {
let file = self.find(file)?;
Some(Box::new(file))
}
}
/// Helper trait for WebFileEngine
#[async_trait::async_trait(?Send)]
pub trait WebFileEngineExt {
/// returns web_sys::File
async fn get_web_file(&self, file: &str) -> Option<web_sys::File>;
}
#[async_trait::async_trait(?Send)]
impl WebFileEngineExt for std::sync::Arc<dyn FileEngine> {
async fn get_web_file(&self, file: &str) -> Option<web_sys::File> {
let native_file = self.get_native_file(file).await?;
let ret = native_file.downcast::<web_sys::File>().ok()?;
Some(*ret)
}
}
+88
View File
@@ -0,0 +1,88 @@
use std::cell::{Cell, RefCell};
use std::io::Cursor;
use serde::de::DeserializeOwned;
thread_local! {
static SERVER_DATA: RefCell<Option<HTMLDataCursor>> = const { RefCell::new(None) };
}
/// Try to take the next item from the server data cursor. This will only be set during the first run of a component before hydration.
/// This will return `None` if no data was pushed for this instance or if serialization fails
// TODO: evan better docs
pub fn take_server_data<T: DeserializeOwned>() -> Result<Option<T>, TakeDataError> {
SERVER_DATA.with_borrow(|data| match data.as_ref() {
Some(data) => data.take(),
None => Err(TakeDataError::DataNotAvailable),
})
}
/// Run a closure with the server data
pub(crate) fn with_server_data<O>(server_data: HTMLDataCursor, f: impl FnOnce() -> O) -> O {
// Set the server data that will be used during hydration
set_server_data(server_data);
let out = f();
// Hydrating the suspense node **should** eat all the server data, but just in case, remove it
remove_server_data();
out
}
fn set_server_data(data: HTMLDataCursor) {
SERVER_DATA.with_borrow_mut(|server_data| *server_data = Some(data));
}
fn remove_server_data() {
SERVER_DATA.with_borrow_mut(|server_data| server_data.take());
}
/// Data that is deserialized from the server during hydration
pub(crate) struct HTMLDataCursor {
data: Vec<Option<Vec<u8>>>,
index: Cell<usize>,
}
impl HTMLDataCursor {
pub(crate) fn from_serialized(data: &[u8]) -> Self {
let deserialized = ciborium::from_reader(Cursor::new(data)).unwrap();
Self::new(deserialized)
}
fn new(data: Vec<Option<Vec<u8>>>) -> Self {
Self {
data,
index: Cell::new(0),
}
}
pub fn take<T: DeserializeOwned>(&self) -> Result<Option<T>, TakeDataError> {
let current = self.index.get();
if current >= self.data.len() {
tracing::trace!(
"Tried to take more data than was available, len: {}, index: {}; This is normal if the server function was started on the client, but may indicate a bug if the server function result should be deserialized from the server",
self.data.len(),
current
);
return Err(TakeDataError::DataNotAvailable);
}
let bytes = self.data[current].as_ref();
self.index.set(current + 1);
match bytes {
Some(bytes) => match ciborium::from_reader(Cursor::new(bytes)) {
Ok(x) => Ok(Some(x)),
Err(e) => {
tracing::error!("Error deserializing data: {:?}", e);
Err(TakeDataError::DeserializationError(e))
}
},
None => Ok(None),
}
}
}
/// An error that can occur when trying to take data from the server
pub enum TakeDataError {
/// Deserializing the data failed
DeserializationError(ciborium::de::Error<std::io::Error>),
/// No data was available
DataNotAvailable,
}
+365
View File
@@ -0,0 +1,365 @@
//! When hydrating streaming components:
//! 1. Just hydrate the template on the outside
//! 2. As we render the virtual dom initially, keep track of the server ids of the suspense boundaries
//! 3. Register a callback for dx_hydrate(id, data) that takes some new data, reruns the suspense boundary with that new data and then rehydrates the node
use std::fmt::Write;
use crate::dom::WebsysDom;
use crate::with_server_data;
use crate::HTMLDataCursor;
use dioxus_core::prelude::*;
use dioxus_core::AttributeValue;
use dioxus_core::{DynamicNode, ElementId};
use futures_channel::mpsc::UnboundedReceiver;
use RehydrationError::*;
use super::SuspenseMessage;
#[derive(Debug)]
#[non_exhaustive]
pub(crate) enum RehydrationError {
/// The client tried to rehydrate a vnode before the dom was built
VNodeNotInitialized,
/// The client tried to rehydrate a suspense boundary that was not mounted on the server
SuspenseHydrationIdNotFound,
/// The client tried to rehydrate a dom id that was not found on the server
ElementNotFound,
}
#[derive(Debug)]
struct SuspenseHydrationIdsNode {
/// The scope id of the suspense boundary
scope_id: ScopeId,
/// Children of this node
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),
}
}
}
/// Streaming hydration happens in waves. The server assigns suspense hydrations ids based on the order
/// the suspense boundaries are discovered in which should be consistent on the client and server.
///
/// This struct keeps track of the order the suspense boundaries are discovered in on the client so we can map the id in the dom to the scope we need to rehydrate.
///
/// Diagram: https://excalidraw.com/#json=4NxmW90g0207Y62lESxfF,vP_Yn6j7k23utq2HZIsuiw
#[derive(Default, Debug)]
pub(crate) struct SuspenseHydrationIds {
/// A dense mapping from traversal order to the scope id of the suspense boundary
/// The suspense boundary may be unmounted if the component was removed after partial hydration on the client
children: Vec<SuspenseHydrationIdsNode>,
current_path: Vec<u32>,
}
impl SuspenseHydrationIds {
/// Add a suspense boundary to the list of suspense boundaries. This should only be called on the root scope after the first rebuild (which happens on the server) and on suspense boundaries that are resolved from the server.
/// Running this on a scope that is only created on the client may cause hydration issues.
fn add_suspense_boundary(&mut self, id: ScopeId) {
match self.current_path.as_slice() {
// This is a root node, add the new node
[] => {
self.children.push(SuspenseHydrationIdsNode::new(id));
}
// This isn't a root node, traverse into children and add the new node
[first_index, rest @ ..] => {
let child_node = self.children[*first_index as usize]
.traverse_mut(rest)
.unwrap();
child_node.children.push(SuspenseHydrationIdsNode::new(id));
}
}
}
/// Get the scope id of the suspense boundary from the id in the dom
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,
} = message;
let document = web_sys::window().unwrap().document().unwrap();
// Before we start rehydrating the suspense boundary we need to check that the suspense boundary exists. It may have been removed on the client.
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)?;
// First convert the dom id into a scope id based on the discovery order of the suspense boundaries.
// This may fail if the id is not parsable, or if the suspense boundary was removed after partial hydration on the client.
let id = self
.suspense_hydration_ids
.get_suspense_boundary(&suspense_path)
.ok_or(RehydrationError::SuspenseHydrationIdNotFound)?;
// Push the new nodes onto the stack
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);
}
with_server_data(HTMLDataCursor::from_serialized(&data), || {
// rerun the scope with the new data
SuspenseBoundaryProps::resolve_suspense(
id,
dom,
self,
|to| {
// Switch to only writing templates
to.only_write_templates = true;
},
children.len(),
);
self.only_write_templates = false;
});
// Flush the mutations that will swap the placeholder nodes with the resolved nodes
self.flush_edits();
// Remove the streaming div
resolved_suspense_element.remove();
let Some(root_scope) = dom.get_scope(id) else {
// If the scope was removed on the client, we may not be able to rehydrate it, but this shouldn't cause an error
return Ok(());
};
// As we hydrate the suspense boundary, set the current path to the path of the suspense boundary
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();
// Recursively rehydrate the nodes under the scope
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| {
let data = data.to_vec();
_ = tx.start_send(SuspenseMessage {
suspense_path: path,
data,
});
};
let closure = wasm_bindgen::closure::Closure::new(closure);
dioxus_interpreter_js::minimal_bindings::register_rehydrate_chunk_for_streaming(&closure);
closure.forget();
// Rehydrate the root scope that was rendered on the server. We will likely run into suspense boundaries.
// Any suspense boundaries we run into are stored for hydration later.
self.start_hydration_at_scope(vdom.base_scope(), vdom, vec![self.root.clone().into()])?;
Ok(rx)
}
fn rehydrate_scope(
&mut self,
scope: &ScopeState,
dom: &VirtualDom,
ids: &mut Vec<u32>,
to_mount: &mut Vec<ElementId>,
) -> Result<(), RehydrationError> {
// If this scope is a suspense boundary that is pending, add it to the list of pending suspense boundaries
if let Some(suspense) = SuspenseBoundaryProps::downcast_from_scope(scope) {
if suspense.suspended() {
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.get().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
}
+21
View File
@@ -0,0 +1,21 @@
#[cfg(feature = "hydrate")]
mod deserialize;
#[cfg(feature = "hydrate")]
mod hydrate;
#[cfg(feature = "hydrate")]
pub use deserialize::*;
#[cfg(feature = "hydrate")]
#[allow(unused)]
pub use hydrate::*;
/// The message sent from the server to the client to hydrate a suspense boundary
#[derive(Debug)]
pub(crate) struct SuspenseMessage {
#[cfg(feature = "hydrate")]
/// The path to the suspense boundary. Each element in the path is an index into the children of the suspense boundary (or the root node) in the order they are first created
suspense_path: Vec<u32>,
#[cfg(feature = "hydrate")]
/// The data to hydrate the suspense boundary with
data: Vec<u8>,
}
+84 -27
View File
@@ -20,11 +20,10 @@
//! To purview the examples, check of the root Dioxus crate - the examples in this crate are mostly meant to provide
//! validation of websys-specific features and not the general use of Dioxus.
use std::rc::Rc;
use std::{panic, rc::Rc};
pub use crate::cfg::Config;
#[cfg(feature = "file_engine")]
pub use crate::file_engine::WebFileEngineExt;
use crate::hydration::SuspenseMessage;
use dioxus_core::VirtualDom;
use futures_util::{pin_mut, select, FutureExt, StreamExt};
@@ -39,14 +38,12 @@ pub use event::*;
#[cfg(feature = "eval")]
mod eval;
#[cfg(feature = "file_engine")]
mod file_engine;
#[cfg(all(feature = "hot_reload", debug_assertions))]
mod hot_reload;
#[cfg(feature = "hydrate")]
mod rehydrate;
mod hydration;
#[allow(unused)]
pub use hydration::*;
/// Runs the app as a future that can be scheduled around the main thread.
///
@@ -58,7 +55,7 @@ mod rehydrate;
/// let app_fut = dioxus_web::run_with_props(App, RootProps { name: String::from("foo") });
/// wasm_bindgen_futures::spawn_local(app_fut);
/// ```
pub async fn run(virtual_dom: VirtualDom, web_config: Config) {
pub async fn run(virtual_dom: VirtualDom, web_config: Config) -> ! {
tracing::info!("Starting up");
let mut dom = virtual_dom;
@@ -82,20 +79,36 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) {
tracing::info!("rebuilding app");
let mut hydration_receiver: Option<futures_channel::mpsc::UnboundedReceiver<SuspenseMessage>> =
None;
if should_hydrate {
#[cfg(feature = "hydrate")]
{
dom.rebuild(&mut crate::rehydrate::OnlyWriteTemplates(&mut websys_dom));
if let Err(err) = websys_dom.rehydrate(&dom) {
tracing::error!("Rehydration failed. {:?}", err);
tracing::error!("Rebuild DOM into element from scratch");
websys_dom.root.set_text_content(None);
dom.rebuild(&mut websys_dom);
websys_dom.flush_edits();
websys_dom.only_write_templates = true;
// Get the initial hydration data from the client
#[wasm_bindgen::prelude::wasm_bindgen(inline_js = r#"
export function get_initial_hydration_data() {
const decoded = atob(window.initial_dioxus_hydration_data);
return Uint8Array.from(decoded, (c) => c.charCodeAt(0))
}
"#)]
extern "C" {
fn get_initial_hydration_data() -> js_sys::Uint8Array;
}
let hydration_data = get_initial_hydration_data().to_vec();
let server_data = HTMLDataCursor::from_serialized(&hydration_data);
with_server_data(server_data, || {
dom.rebuild(&mut websys_dom);
});
websys_dom.only_write_templates = false;
let rx = websys_dom.rehydrate(&dom).unwrap();
hydration_receiver = Some(rx);
}
#[cfg(not(feature = "hydrate"))]
{
panic!("Hydration is not enabled. Please enable the `hydrate` feature.");
}
} else {
dom.rebuild(&mut websys_dom);
@@ -109,33 +122,77 @@ pub async fn run(virtual_dom: VirtualDom, web_config: Config) {
loop {
// if virtual dom has nothing, wait for it to have something before requesting idle time
// if there is work then this future resolves immediately.
let (mut res, template) = {
let mut res;
#[cfg(all(feature = "hot_reload", debug_assertions))]
let template;
#[allow(unused)]
let mut hydration_work: Option<SuspenseMessage> = None;
{
let work = dom.wait_for_work().fuse();
pin_mut!(work);
let mut rx_next = rx.select_next_some();
let mut hydration_receiver_iter = futures_util::stream::iter(&mut hydration_receiver)
.fuse()
.flatten();
let mut rx_hydration = hydration_receiver_iter.select_next_some();
#[cfg(all(feature = "hot_reload", debug_assertions))]
#[allow(unused)]
{
let mut hot_reload_next = hotreload_rx.select_next_some();
select! {
_ = work => (None, None),
new_template = hot_reload_next => (None, Some(new_template)),
evt = rx_next => (Some(evt), None),
_ = work => {
res = None;
template = None;
},
new_template = hot_reload_next => {
res = None;
template = Some(new_template);
},
evt = rx_next => {
res = Some(evt);
template = None;
}
hydration_data = rx_hydration => {
res = None;
template = None;
#[cfg(feature = "hydrate")]
{
hydration_work = Some(hydration_data);
}
},
}
}
#[cfg(not(all(feature = "hot_reload", debug_assertions)))]
select! {
_ = work => (None, None),
evt = rx_next => (Some(evt), None),
#[allow(unused)]
{
select! {
_ = work => res = None,
evt = rx_next => res = Some(evt),
hyd = rx_hydration => {
res = None;
#[cfg(feature = "hydrate")]
{
hydration_work = Some(hyd);
}
}
}
}
};
}
#[cfg(all(feature = "hot_reload", debug_assertions))]
if let Some(template) = template {
dom.replace_template(template);
}
#[cfg(feature = "hydrate")]
if let Some(hydration_data) = hydration_work {
websys_dom.rehydrate_streaming(hydration_data, &mut dom);
}
// Dequeue all of the events from the channel in send order
// todo: we should re-order these if possible
while let Some(evt) = res {
+62 -5
View File
@@ -1,10 +1,8 @@
use crate::dom::UiEvent;
use crate::dom::WebsysDom;
use dioxus_core::prelude::*;
use dioxus_core::WriteMutations;
use dioxus_core::{AttributeValue, ElementId};
use dioxus_html::event_bubbles;
use dioxus_html::PlatformEventData;
use dioxus_interpreter_js::minimal_bindings;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
@@ -45,7 +43,6 @@ impl WebsysDom {
el.dyn_into().unwrap()
}
Text { text } => self.document.create_text_node(text).dyn_into().unwrap(),
DynamicText { .. } => self.document.create_text_node("p").dyn_into().unwrap(),
Dynamic { .. } => {
let el = self.document.create_element("pre").unwrap();
let _ = el.toggle_attribute("hidden");
@@ -67,11 +64,11 @@ impl WebsysDom {
for id in self.queued_mounted_events.drain(..) {
let node = self.interpreter.base().get_node(id.0 as u32);
if let Some(element) = node.dyn_ref::<web_sys::Element>() {
let _ = self.event_channel.unbounded_send(UiEvent {
let _ = self.event_channel.unbounded_send(crate::dom::UiEvent {
name: "mounted".to_string(),
bubbles: false,
element: id,
data: PlatformEventData::new(Box::new(element.clone())),
data: dioxus_html::PlatformEventData::new(Box::new(element.clone())),
});
}
}
@@ -81,6 +78,18 @@ impl WebsysDom {
pub(crate) fn send_mount_event(&mut self, id: ElementId) {
self.queued_mounted_events.push(id);
}
#[inline]
fn only_write_templates(&self) -> bool {
#[cfg(feature = "hydrate")]
{
self.only_write_templates
}
#[cfg(not(feature = "hydrate"))]
{
false
}
}
}
impl WriteMutations for WebsysDom {
@@ -98,28 +107,46 @@ impl WriteMutations for WebsysDom {
}
fn append_children(&mut self, id: ElementId, m: usize) {
if self.only_write_templates() {
return;
}
self.interpreter.append_children(id.0 as u32, m as u16)
}
fn assign_node_id(&mut self, path: &'static [u8], id: ElementId) {
if self.only_write_templates() {
return;
}
self.interpreter
.assign_id(path.as_ptr() as u32, path.len() as u8, id.0 as u32)
}
fn create_placeholder(&mut self, id: ElementId) {
if self.only_write_templates() {
return;
}
self.interpreter.create_placeholder(id.0 as u32)
}
fn create_text_node(&mut self, value: &str, id: ElementId) {
if self.only_write_templates() {
return;
}
self.interpreter.create_text_node(value, id.0 as u32)
}
fn hydrate_text_node(&mut self, path: &'static [u8], value: &str, id: ElementId) {
if self.only_write_templates() {
return;
}
self.interpreter
.hydrate_text(path.as_ptr() as u32, path.len() as u8, value, id.0 as u32)
}
fn load_template(&mut self, name: &'static str, index: usize, id: ElementId) {
if self.only_write_templates() {
return;
}
if let Some(tmpl_id) = self.templates.get(name) {
self.interpreter
.load_template(*tmpl_id, index as u16, id.0 as u32)
@@ -127,19 +154,31 @@ impl WriteMutations for WebsysDom {
}
fn replace_node_with(&mut self, id: ElementId, m: usize) {
if self.only_write_templates() {
return;
}
self.interpreter.replace_with(id.0 as u32, m as u16)
}
fn replace_placeholder_with_nodes(&mut self, path: &'static [u8], m: usize) {
if self.only_write_templates() {
return;
}
self.interpreter
.replace_placeholder(path.as_ptr() as u32, path.len() as u8, m as u16)
}
fn insert_nodes_after(&mut self, id: ElementId, m: usize) {
if self.only_write_templates() {
return;
}
self.interpreter.insert_after(id.0 as u32, m as u16)
}
fn insert_nodes_before(&mut self, id: ElementId, m: usize) {
if self.only_write_templates() {
return;
}
self.interpreter.insert_before(id.0 as u32, m as u16)
}
@@ -150,6 +189,9 @@ impl WriteMutations for WebsysDom {
value: &AttributeValue,
id: ElementId,
) {
if self.only_write_templates() {
return;
}
match value {
AttributeValue::Text(txt) => {
self.interpreter
@@ -182,10 +224,16 @@ impl WriteMutations for WebsysDom {
}
fn set_node_text(&mut self, value: &str, id: ElementId) {
if self.only_write_templates() {
return;
}
self.interpreter.set_text(id.0 as u32, value)
}
fn create_event_listener(&mut self, name: &'static str, id: ElementId) {
if self.only_write_templates() {
return;
}
// mounted events are fired immediately after the element is mounted.
if name == "mounted" {
#[cfg(feature = "mounted")]
@@ -198,6 +246,9 @@ impl WriteMutations for WebsysDom {
}
fn remove_event_listener(&mut self, name: &'static str, id: ElementId) {
if self.only_write_templates() {
return;
}
if name == "mounted" {
return;
}
@@ -207,10 +258,16 @@ impl WriteMutations for WebsysDom {
}
fn remove_node(&mut self, id: ElementId) {
if self.only_write_templates() {
return;
}
self.interpreter.remove(id.0 as u32)
}
fn push_root(&mut self, id: ElementId) {
if self.only_write_templates() {
return;
}
self.interpreter.push_root(id.0 as u32)
}
}
-219
View File
@@ -1,219 +0,0 @@
use crate::dom::WebsysDom;
use dioxus_core::prelude::*;
use dioxus_core::AttributeValue;
use dioxus_core::WriteMutations;
use dioxus_core::{DynamicNode, ElementId};
#[derive(Debug)]
#[non_exhaustive]
pub enum RehydrationError {
VNodeNotInitialized,
}
use RehydrationError::*;
impl WebsysDom {
// we're streaming in patches, but the nodes already exist
// so we're just going to write the correct IDs to the node and load them in
pub fn rehydrate(&mut self, dom: &VirtualDom) -> Result<(), RehydrationError> {
let root_scope = dom.base_scope();
let mut ids = Vec::new();
let mut to_mount = Vec::new();
// Recursively rehydrate the dom from the VirtualDom
self.rehydrate_scope(root_scope, dom, &mut ids, &mut to_mount)?;
self.interpreter.base().hydrate(ids);
#[cfg(feature = "mounted")]
for id in to_mount {
self.send_mount_event(id);
}
Ok(())
}
fn rehydrate_scope(
&mut self,
scope: &ScopeState,
dom: &VirtualDom,
ids: &mut Vec<u32>,
to_mount: &mut Vec<ElementId>,
) -> Result<(), RehydrationError> {
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.get().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> {
tracing::trace!("rehydrate template node: {:?}", node);
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 } | TemplateNode::DynamicText { 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> {
tracing::trace!("rehydrate dynamic node: {:?}", dynamic);
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(())
}
}
/// During rehydration, we don't want to actually write anything to the DOM, but we do need to store any templates that were created. This struct is used to only write templates to the DOM.
pub(crate) struct OnlyWriteTemplates<'a>(pub &'a mut WebsysDom);
impl WriteMutations for OnlyWriteTemplates<'_> {
fn register_template(&mut self, template: Template) {
let mut roots = vec![];
for root in template.roots {
roots.push(self.0.create_template_node(root))
}
self.0
.templates
.insert(template.name.to_owned(), self.0.max_template_id);
self.0
.interpreter
.base()
.save_template(roots, self.0.max_template_id);
self.0.max_template_id += 1
}
fn append_children(&mut self, _: ElementId, _: usize) {}
fn assign_node_id(&mut self, _: &'static [u8], _: ElementId) {}
fn create_placeholder(&mut self, _: ElementId) {}
fn create_text_node(&mut self, _: &str, _: ElementId) {}
fn hydrate_text_node(&mut self, _: &'static [u8], _: &str, _: ElementId) {}
fn load_template(&mut self, _: &'static str, _: usize, _: ElementId) {}
fn replace_node_with(&mut self, _: ElementId, _: usize) {}
fn replace_placeholder_with_nodes(&mut self, _: &'static [u8], _: usize) {}
fn insert_nodes_after(&mut self, _: ElementId, _: usize) {}
fn insert_nodes_before(&mut self, _: ElementId, _: usize) {}
fn set_attribute(
&mut self,
_: &'static str,
_: Option<&'static str>,
_: &AttributeValue,
_: ElementId,
) {
}
fn set_node_text(&mut self, _: &str, _: ElementId) {}
fn create_event_listener(&mut self, _: &'static str, _: ElementId) {}
fn remove_event_listener(&mut self, _: &'static str, _: ElementId) {}
fn remove_node(&mut self, _: ElementId) {}
fn push_root(&mut self, _: ElementId) {}
}