Files
dioxus/packages/core/src/diff/iterator.rs
T
Evan AlmloffandJonathan Kelley 022e4ad203 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>
2024-07-01 20:50:36 -07:00

502 lines
19 KiB
Rust

use crate::{
innerlude::{ElementRef, WriteMutations},
nodes::VNode,
DynamicNode, ScopeId, VirtualDom,
};
use rustc_hash::{FxHashMap, FxHashSet};
impl VirtualDom {
pub(crate) fn diff_non_empty_fragment(
&mut self,
to: Option<&mut impl WriteMutations>,
old: &[VNode],
new: &[VNode],
parent: Option<ElementRef>,
) {
let new_is_keyed = new[0].key.is_some();
let old_is_keyed = old[0].key.is_some();
debug_assert!(
new.iter().all(|n| n.key.is_some() == new_is_keyed),
"all siblings must be keyed or all siblings must be non-keyed"
);
debug_assert!(
old.iter().all(|o| o.key.is_some() == old_is_keyed),
"all siblings must be keyed or all siblings must be non-keyed"
);
if new_is_keyed && old_is_keyed {
self.diff_keyed_children(to, old, new, parent);
} else {
self.diff_non_keyed_children(to, old, new, parent);
}
}
// Diff children that are not keyed.
//
// The parent must be on the top of the change list stack when entering this
// function:
//
// [... parent]
//
// the change list stack is in the same state when this function returns.
fn diff_non_keyed_children(
&mut self,
mut to: Option<&mut impl WriteMutations>,
old: &[VNode],
new: &[VNode],
parent: Option<ElementRef>,
) {
use std::cmp::Ordering;
// Handled these cases in `diff_children` before calling this function.
debug_assert!(!new.is_empty());
debug_assert!(!old.is_empty());
match old.len().cmp(&new.len()) {
Ordering::Greater => self.remove_nodes(to.as_deref_mut(), &old[new.len()..], None),
Ordering::Less => self.create_and_insert_after(
to.as_deref_mut(),
&new[old.len()..],
old.last().unwrap(),
parent,
),
Ordering::Equal => {}
}
for (new, old) in new.iter().zip(old.iter()) {
old.diff_node(new, self, to.as_deref_mut());
}
}
// Diffing "keyed" children.
//
// With keyed children, we care about whether we delete, move, or create nodes
// versus mutate existing nodes in place. Presumably there is some sort of CSS
// transition animation that makes the virtual DOM diffing algorithm
// observable. By specifying keys for nodes, we know which virtual DOM nodes
// must reuse (or not reuse) the same physical DOM nodes.
//
// This is loosely based on Inferno's keyed patching implementation. However, we
// have to modify the algorithm since we are compiling the diff down into change
// list instructions that will be executed later, rather than applying the
// changes to the DOM directly as we compare virtual DOMs.
//
// https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739
//
// The stack is empty upon entry.
fn diff_keyed_children(
&mut self,
mut to: Option<&mut impl WriteMutations>,
old: &[VNode],
new: &[VNode],
parent: Option<ElementRef>,
) {
if cfg!(debug_assertions) {
let mut keys = rustc_hash::FxHashSet::default();
let mut assert_unique_keys = |children: &[VNode]| {
keys.clear();
for child in children {
let key = child.key.clone();
debug_assert!(
key.is_some(),
"if any sibling is keyed, all siblings must be keyed"
);
keys.insert(key);
}
debug_assert_eq!(
children.len(),
keys.len(),
"keyed siblings must each have a unique key"
);
};
assert_unique_keys(old);
assert_unique_keys(new);
}
// First up, we diff all the nodes with the same key at the beginning of the
// children.
//
// `shared_prefix_count` is the count of how many nodes at the start of
// `new` and `old` share the same keys.
let (left_offset, right_offset) =
match self.diff_keyed_ends(to.as_deref_mut(), old, new, parent) {
Some(count) => count,
None => return,
};
// Ok, we now hopefully have a smaller range of children in the middle
// within which to re-order nodes with the same keys, remove old nodes with
// now-unused keys, and create new nodes with fresh keys.
let old_middle = &old[left_offset..(old.len() - right_offset)];
let new_middle = &new[left_offset..(new.len() - right_offset)];
debug_assert!(
!((old_middle.len() == new_middle.len()) && old_middle.is_empty()),
"keyed children must have the same number of children"
);
if new_middle.is_empty() {
// remove the old elements
self.remove_nodes(to, old_middle, None);
} else if old_middle.is_empty() {
// there were no old elements, so just create the new elements
// we need to find the right "foothold" though - we shouldn't use the "append" at all
if left_offset == 0 {
// insert at the beginning of the old list
let foothold = &old[old.len() - right_offset];
self.create_and_insert_before(to, new_middle, foothold, parent);
} else if right_offset == 0 {
// insert at the end the old list
let foothold = old.last().unwrap();
self.create_and_insert_after(to, new_middle, foothold, parent);
} else {
// inserting in the middle
let foothold = &old[left_offset - 1];
self.create_and_insert_after(to, new_middle, foothold, parent);
}
} else {
self.diff_keyed_middle(to, old_middle, new_middle, parent);
}
}
/// Diff both ends of the children that share keys.
///
/// Returns a left offset and right offset of that indicates a smaller section to pass onto the middle diffing.
///
/// If there is no offset, then this function returns None and the diffing is complete.
fn diff_keyed_ends(
&mut self,
mut to: Option<&mut impl WriteMutations>,
old: &[VNode],
new: &[VNode],
parent: Option<ElementRef>,
) -> Option<(usize, usize)> {
let mut left_offset = 0;
for (old, new) in old.iter().zip(new.iter()) {
// abort early if we finally run into nodes with different keys
if old.key != new.key {
break;
}
old.diff_node(new, self, to.as_deref_mut());
left_offset += 1;
}
// If that was all of the old children, then create and append the remaining
// new children and we're finished.
if left_offset == old.len() {
self.create_and_insert_after(to, &new[left_offset..], old.last().unwrap(), parent);
return None;
}
// And if that was all of the new children, then remove all of the remaining
// old children and we're finished.
if left_offset == new.len() {
self.remove_nodes(to, &old[left_offset..], None);
return None;
}
// if the shared prefix is less than either length, then we need to walk backwards
let mut right_offset = 0;
for (old, new) in old.iter().rev().zip(new.iter().rev()) {
// abort early if we finally run into nodes with different keys
if old.key != new.key {
break;
}
old.diff_node(new, self, to.as_deref_mut());
right_offset += 1;
}
Some((left_offset, right_offset))
}
// The most-general, expensive code path for keyed children diffing.
//
// We find the longest subsequence within `old` of children that are relatively
// ordered the same way in `new` (via finding a longest-increasing-subsequence
// of the old child's index within `new`). The children that are elements of
// this subsequence will remain in place, minimizing the number of DOM moves we
// will have to do.
//
// Upon entry to this function, the change list stack must be empty.
//
// This function will load the appropriate nodes onto the stack and do diffing in place.
//
// Upon exit from this function, it will be restored to that same self.
#[allow(clippy::too_many_lines)]
fn diff_keyed_middle(
&mut self,
mut to: Option<&mut impl WriteMutations>,
old: &[VNode],
new: &[VNode],
parent: Option<ElementRef>,
) {
/*
1. Map the old keys into a numerical ordering based on indices.
2. Create a map of old key to its index
3. Map each new key to the old key, carrying over the old index.
- IE if we have ABCD becomes BACD, our sequence would be 1,0,2,3
- if we have ABCD to ABDE, our sequence would be 0,1,3,MAX because E doesn't exist
now, we should have a list of integers that indicates where in the old list the new items map to.
4. Compute the LIS of this list
- this indicates the longest list of new children that won't need to be moved.
5. Identify which nodes need to be removed
6. Identify which nodes will need to be diffed
7. Going along each item in the new list, create it and insert it before the next closest item in the LIS.
- if the item already existed, just move it to the right place.
8. Finally, generate instructions to remove any old children.
9. Generate instructions to finally diff children that are the same between both
*/
// 0. Debug sanity checks
// Should have already diffed the shared-key prefixes and suffixes.
debug_assert_ne!(new.first().map(|i| &i.key), old.first().map(|i| &i.key));
debug_assert_ne!(new.last().map(|i| &i.key), old.last().map(|i| &i.key));
// 1. Map the old keys into a numerical ordering based on indices.
// 2. Create a map of old key to its index
// IE if the keys were A B C, then we would have (A, 0) (B, 1) (C, 2).
let old_key_to_old_index = old
.iter()
.enumerate()
.map(|(i, o)| (o.key.as_ref().unwrap().as_str(), i))
.collect::<FxHashMap<_, _>>();
let mut shared_keys = FxHashSet::default();
// 3. Map each new key to the old key, carrying over the old index.
let new_index_to_old_index = new
.iter()
.map(|node| {
let key = node.key.as_ref().unwrap();
if let Some(&index) = old_key_to_old_index.get(key.as_str()) {
shared_keys.insert(key);
index
} else {
usize::MAX
}
})
.collect::<Box<[_]>>();
// If none of the old keys are reused by the new children, then we remove all the remaining old children and
// create the new children afresh.
if shared_keys.is_empty() {
debug_assert!(
!old.is_empty(),
"we should never be appending - just creating N"
);
let m = self.create_children(to.as_deref_mut(), new, parent);
self.remove_nodes(to, old, Some(m));
return;
}
// remove any old children that are not shared
for child_to_remove in old
.iter()
.filter(|child| !shared_keys.contains(child.key.as_ref().unwrap()))
{
child_to_remove.remove_node(self, to.as_deref_mut(), None);
}
// 4. Compute the LIS of this list
let mut lis_sequence = Vec::with_capacity(new_index_to_old_index.len());
let mut allocation = vec![0; new_index_to_old_index.len() * 2];
let (predecessors, starts) = allocation.split_at_mut(new_index_to_old_index.len());
longest_increasing_subsequence::lis_with(
&new_index_to_old_index,
&mut lis_sequence,
|a, b| a < b,
predecessors,
starts,
);
// if a new node gets u32 max and is at the end, then it might be part of our LIS (because u32 max is a valid LIS)
if lis_sequence.first().map(|f| new_index_to_old_index[*f]) == Some(usize::MAX) {
lis_sequence.remove(0);
}
// Diff each nod in the LIS
for idx in &lis_sequence {
old[new_index_to_old_index[*idx]].diff_node(&new[*idx], self, to.as_deref_mut());
}
/// Create or diff each node in a range depending on whether it is in the LIS or not
/// Returns the number of nodes created on the stack
fn create_or_diff(
vdom: &mut VirtualDom,
new: &[VNode],
old: &[VNode],
mut to: Option<&mut impl WriteMutations>,
parent: Option<ElementRef>,
new_index_to_old_index: &[usize],
range: std::ops::Range<usize>,
) -> usize {
let range_start = range.start;
new[range]
.iter()
.enumerate()
.map(|(idx, new_node)| {
let new_idx = range_start + idx;
let old_index = new_index_to_old_index[new_idx];
// If the node existed in the old list, diff it
if let Some(old_node) = old.get(old_index) {
old_node.diff_node(new_node, vdom, to.as_deref_mut());
if let Some(to) = to.as_deref_mut() {
new_node.push_all_root_nodes(vdom, to)
} else {
0
}
} else {
// Otherwise, just add it to the stack
new_node.create(vdom, parent, to.as_deref_mut())
}
})
.sum()
}
// add mount instruction for the items before the LIS
let last = *lis_sequence.first().unwrap();
if last < (new.len() - 1) {
let nodes_created = create_or_diff(
self,
new,
old,
to.as_deref_mut(),
parent,
&new_index_to_old_index,
(last + 1)..new.len(),
);
// Insert all the nodes that we just created after the last node in the LIS
self.insert_after(to.as_deref_mut(), nodes_created, &new[last]);
}
// For each node inside of the LIS, but not included in the LIS, generate a mount instruction
// We loop over the LIS in reverse order and insert any nodes we find in the gaps between indexes
let mut lis_iter = lis_sequence.iter();
let mut last = *lis_iter.next().unwrap();
for next in lis_iter {
if last - next > 1 {
let nodes_created = create_or_diff(
self,
new,
old,
to.as_deref_mut(),
parent,
&new_index_to_old_index,
(next + 1)..last,
);
self.insert_before(to.as_deref_mut(), nodes_created, &new[last]);
}
last = *next;
}
// add mount instruction for the items after the LIS
let first_lis = *lis_sequence.last().unwrap();
if first_lis > 0 {
let nodes_created = create_or_diff(
self,
new,
old,
to.as_deref_mut(),
parent,
&new_index_to_old_index,
0..first_lis,
);
self.insert_before(to, nodes_created, &new[first_lis]);
}
}
fn create_and_insert_before(
&mut self,
mut to: Option<&mut impl WriteMutations>,
new: &[VNode],
before: &VNode,
parent: Option<ElementRef>,
) {
let m = self.create_children(to.as_deref_mut(), new, parent);
self.insert_before(to, m, before);
}
fn insert_before(&mut self, to: Option<&mut impl WriteMutations>, new: usize, before: &VNode) {
if let Some(to) = to {
if new > 0 {
let id = before.find_first_element(self);
to.insert_nodes_before(id, new);
}
}
}
fn create_and_insert_after(
&mut self,
mut to: Option<&mut impl WriteMutations>,
new: &[VNode],
after: &VNode,
parent: Option<ElementRef>,
) {
let m = self.create_children(to.as_deref_mut(), new, parent);
self.insert_after(to, m, after);
}
fn insert_after(&mut self, to: Option<&mut impl WriteMutations>, new: usize, after: &VNode) {
if let Some(to) = to {
if new > 0 {
let id = after.find_last_element(self);
to.insert_nodes_after(id, new);
}
}
}
}
impl VNode {
/// Push all the root nodes on the stack
pub(crate) fn push_all_root_nodes(
&self,
dom: &VirtualDom,
to: &mut impl WriteMutations,
) -> usize {
let template = self.template.get();
let mount = dom.mounts.get(self.mount.get().0).unwrap();
template
.roots
.iter()
.enumerate()
.map(
|(root_idx, _)| match self.get_dynamic_root_node_and_id(root_idx) {
Some((_, DynamicNode::Fragment(nodes))) => {
let mut accumulated = 0;
for node in nodes {
accumulated += node.push_all_root_nodes(dom, to);
}
accumulated
}
Some((idx, DynamicNode::Component(_))) => {
let scope = ScopeId(mount.mounted_dynamic_nodes[idx]);
let node = dom.get_scope(scope).unwrap().root_node();
node.push_all_root_nodes(dom, to)
}
// This is a static root node or a single dynamic node, just push it
None | Some((_, DynamicNode::Placeholder(_) | DynamicNode::Text(_))) => {
to.push_root(mount.root_ids[root_idx]);
1
}
},
)
.sum()
}
}