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

+6 -4
View File
@@ -7,6 +7,8 @@ use dioxus::prelude::*;
#[test]
fn attrs_cycle() {
tracing_subscriber::fmt::init();
let mut dom = VirtualDom::new(|| {
let id = generation();
match id % 2 {
@@ -26,7 +28,7 @@ fn attrs_cycle() {
]
);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -38,7 +40,7 @@ fn attrs_cycle() {
]
);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -47,7 +49,7 @@ fn attrs_cycle() {
]
);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -70,7 +72,7 @@ fn attrs_cycle() {
);
// we take the node taken by attributes since we reused it
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
+1 -1
View File
@@ -22,7 +22,7 @@ fn bubbles_error() {
let _edits = dom.rebuild_to_vec().santize();
}
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
_ = dom.render_immediate_to_vec();
}
+1 -1
View File
@@ -34,7 +34,7 @@ async fn child_futures_drop_first() {
dom.rebuild(&mut dioxus_core::NoOpMutations);
// Here the parent and task could resolve at the same time, but because the task is in the child, dioxus should run the parent first because the child might be dropped
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
tokio::select! {
_ = dom.wait_for_work() => {}
+7 -7
View File
@@ -27,26 +27,26 @@ fn state_shares() {
]
);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
_ = dom.render_immediate_to_vec();
dom.in_runtime(|| {
assert_eq!(ScopeId::ROOT.consume_context::<i32>().unwrap(), 1);
assert_eq!(ScopeId::APP.consume_context::<i32>().unwrap(), 1);
});
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
_ = dom.render_immediate_to_vec();
dom.in_runtime(|| {
assert_eq!(ScopeId::ROOT.consume_context::<i32>().unwrap(), 2);
assert_eq!(ScopeId::APP.consume_context::<i32>().unwrap(), 2);
});
dom.mark_dirty(ScopeId(2));
dom.mark_dirty(ScopeId(ScopeId::APP.0 + 2));
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[SetText { value: "Value is 2".to_string(), id: ElementId(1,) },]
);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId(2));
dom.mark_dirty(ScopeId::APP);
dom.mark_dirty(ScopeId(ScopeId::APP.0 + 2));
let edits = dom.render_immediate_to_vec();
assert_eq!(
edits.santize().edits,
+3 -3
View File
@@ -21,7 +21,7 @@ fn cycling_elements() {
);
}
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -31,7 +31,7 @@ fn cycling_elements() {
);
// notice that the IDs cycle back to ElementId(1), preserving a minimal memory footprint
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -40,7 +40,7 @@ fn cycling_elements() {
]
);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
+3 -3
View File
@@ -72,7 +72,7 @@ fn component_swap() {
);
}
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -81,7 +81,7 @@ fn component_swap() {
]
);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -90,7 +90,7 @@ fn component_swap() {
]
);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
+11 -11
View File
@@ -12,19 +12,19 @@ fn text_diff() {
let mut vdom = VirtualDom::new(app);
vdom.rebuild(&mut NoOpMutations);
vdom.mark_dirty(ScopeId::ROOT);
vdom.mark_dirty(ScopeId::APP);
assert_eq!(
vdom.render_immediate_to_vec().edits,
[SetText { value: "hello 1".to_string(), id: ElementId(2) }]
);
vdom.mark_dirty(ScopeId::ROOT);
vdom.mark_dirty(ScopeId::APP);
assert_eq!(
vdom.render_immediate_to_vec().edits,
[SetText { value: "hello 2".to_string(), id: ElementId(2) }]
);
vdom.mark_dirty(ScopeId::ROOT);
vdom.mark_dirty(ScopeId::APP);
assert_eq!(
vdom.render_immediate_to_vec().edits,
[SetText { value: "hello 3".to_string(), id: ElementId(2) }]
@@ -46,7 +46,7 @@ fn element_swap() {
let mut vdom = VirtualDom::new(app);
vdom.rebuild(&mut NoOpMutations);
vdom.mark_dirty(ScopeId::ROOT);
vdom.mark_dirty(ScopeId::APP);
assert_eq!(
vdom.render_immediate_to_vec().santize().edits,
[
@@ -55,7 +55,7 @@ fn element_swap() {
]
);
vdom.mark_dirty(ScopeId::ROOT);
vdom.mark_dirty(ScopeId::APP);
assert_eq!(
vdom.render_immediate_to_vec().santize().edits,
[
@@ -64,7 +64,7 @@ fn element_swap() {
]
);
vdom.mark_dirty(ScopeId::ROOT);
vdom.mark_dirty(ScopeId::APP);
assert_eq!(
vdom.render_immediate_to_vec().santize().edits,
[
@@ -73,7 +73,7 @@ fn element_swap() {
]
);
vdom.mark_dirty(ScopeId::ROOT);
vdom.mark_dirty(ScopeId::APP);
assert_eq!(
vdom.render_immediate_to_vec().santize().edits,
[
@@ -126,7 +126,7 @@ fn attribute_diff() {
let mut vdom = VirtualDom::new(app);
vdom.rebuild(&mut NoOpMutations);
vdom.mark_dirty(ScopeId::ROOT);
vdom.mark_dirty(ScopeId::APP);
assert_eq!(
vdom.render_immediate_to_vec().santize().edits,
[
@@ -145,7 +145,7 @@ fn attribute_diff() {
]
);
vdom.mark_dirty(ScopeId::ROOT);
vdom.mark_dirty(ScopeId::APP);
assert_eq!(
vdom.render_immediate_to_vec().santize().edits,
[
@@ -166,7 +166,7 @@ fn attribute_diff() {
]
);
vdom.mark_dirty(ScopeId::ROOT);
vdom.mark_dirty(ScopeId::APP);
assert_eq!(
vdom.render_immediate_to_vec().santize().edits,
[
@@ -195,7 +195,7 @@ fn diff_empty() {
let mut vdom = VirtualDom::new(app);
vdom.rebuild(&mut NoOpMutations);
vdom.mark_dirty(ScopeId::ROOT);
vdom.mark_dirty(ScopeId::APP);
let edits = vdom.render_immediate_to_vec().santize().edits;
assert_eq!(
+12 -12
View File
@@ -39,7 +39,7 @@ fn keyed_diffing_out_of_order() {
);
}
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().edits,
[
@@ -64,7 +64,7 @@ fn keyed_diffing_out_of_order_adds() {
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().edits,
[
@@ -90,7 +90,7 @@ fn keyed_diffing_out_of_order_adds_3() {
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().edits,
[
@@ -116,7 +116,7 @@ fn keyed_diffing_out_of_order_adds_4() {
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().edits,
[
@@ -142,7 +142,7 @@ fn keyed_diffing_out_of_order_adds_5() {
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().edits,
[
@@ -167,7 +167,7 @@ fn keyed_diffing_additions() {
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -192,7 +192,7 @@ fn keyed_diffing_additions_and_moves_on_ends() {
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -222,7 +222,7 @@ fn keyed_diffing_additions_and_moves_in_middle() {
dom.rebuild(&mut dioxus_core::NoOpMutations);
// LIS: 4, 5, 6
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -256,7 +256,7 @@ fn controlled_keyed_diffing_out_of_order() {
dom.rebuild(&mut dioxus_core::NoOpMutations);
// LIS: 5, 6
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -289,7 +289,7 @@ fn controlled_keyed_diffing_out_of_order_max_test() {
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -318,7 +318,7 @@ fn remove_list() {
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -343,7 +343,7 @@ fn no_common_keys() {
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
+24 -24
View File
@@ -27,7 +27,7 @@ fn list_creates_one_by_one() {
);
// Rendering the first item should replace the placeholder with an element
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -38,7 +38,7 @@ fn list_creates_one_by_one() {
);
// Rendering the next item should insert after the previous
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -49,7 +49,7 @@ fn list_creates_one_by_one() {
);
// ... and again!
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -60,7 +60,7 @@ fn list_creates_one_by_one() {
);
// once more
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -107,14 +107,14 @@ fn removes_one_by_one() {
// Remove div(3)
// Rendering the first item should replace the placeholder with an element
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[Remove { id: ElementId(6) }]
);
// Remove div(2)
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[Remove { id: ElementId(4) }]
@@ -122,7 +122,7 @@ fn removes_one_by_one() {
// Remove div(1) and replace with a placeholder
// todo: this should just be a remove with no placeholder
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -133,16 +133,16 @@ fn removes_one_by_one() {
// load the 3 and replace the placeholder
// todo: this should actually be append to, but replace placeholder is fine for now
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
HydrateText { path: &[0], value: "0".to_string(), id: ElementId(3) },
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
HydrateText { path: &[0], value: "1".to_string(), id: ElementId(6) },
LoadTemplate { name: "template", index: 0, id: ElementId(7) },
HydrateText { path: &[0], value: "2".to_string(), id: ElementId(8) },
HydrateText { path: &[0], value: "0".to_string(), id: ElementId(6) },
LoadTemplate { name: "template", index: 0, id: ElementId(8) },
HydrateText { path: &[0], value: "1".to_string(), id: ElementId(9) },
LoadTemplate { name: "template", index: 0, id: ElementId(10) },
HydrateText { path: &[0], value: "2".to_string(), id: ElementId(11) },
ReplaceWith { id: ElementId(4), m: 3 }
]
);
@@ -170,7 +170,7 @@ fn list_shrink_multiroot() {
]
);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -182,7 +182,7 @@ fn list_shrink_multiroot() {
]
);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -194,7 +194,7 @@ fn list_shrink_multiroot() {
]
);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -249,19 +249,19 @@ fn removes_one_by_one_multiroot() {
]
);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[Remove { id: ElementId(10) }, Remove { id: ElementId(12) }]
);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[Remove { id: ElementId(6) }, Remove { id: ElementId(8) }]
);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
assert_eq!(
dom.render_immediate_to_vec().santize().edits,
[
@@ -330,7 +330,7 @@ fn remove_many() {
}
{
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
let edits = dom.render_immediate_to_vec().santize();
assert_eq!(
edits.edits,
@@ -343,7 +343,7 @@ fn remove_many() {
}
{
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
let edits = dom.render_immediate_to_vec().santize();
assert_eq!(
edits.edits,
@@ -362,7 +362,7 @@ fn remove_many() {
}
{
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
let edits = dom.render_immediate_to_vec().santize();
assert_eq!(
edits.edits,
@@ -378,13 +378,13 @@ fn remove_many() {
}
{
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
let edits = dom.render_immediate_to_vec().santize();
assert_eq!(
edits.edits,
[
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
HydrateText { path: &[0,], value: "hello 0".to_string(), id: ElementId(3,) },
HydrateText { path: &[0,], value: "hello 0".to_string(), id: ElementId(1,) },
ReplaceWith { id: ElementId(11,), m: 1 },
]
)
+3 -3
View File
@@ -20,13 +20,13 @@ fn app() -> Element {
}
fn NoneChild() -> Element {
None
VNode::empty()
}
fn ThrowChild() -> Element {
Err(std::io::Error::new(std::io::ErrorKind::AddrInUse, "asd")).throw()?;
Err(std::io::Error::new(std::io::ErrorKind::AddrInUse, "asd"))?;
let _g: i32 = "123123".parse().throw()?;
let _g: i32 = "123123".parse()?;
rsx! { div {} }
}
+4 -8
View File
@@ -75,7 +75,7 @@ fn create_random_template_node(
1 => TemplateNode::Text {
text: Box::leak(format!("{}", rand::random::<usize>()).into_boxed_str()),
},
2 => TemplateNode::DynamicText {
2 => TemplateNode::Dynamic {
id: {
let old_idx = *template_idx;
*template_idx += 1;
@@ -118,9 +118,6 @@ fn generate_paths(
}
}
TemplateNode::Text { .. } => {}
TemplateNode::DynamicText { .. } => {
node_paths.push(current_path.to_vec());
}
TemplateNode::Dynamic { .. } => {
node_paths.push(current_path.to_vec());
}
@@ -265,12 +262,11 @@ fn create_random_element(cx: DepthProps) -> Element {
.map(|_| Box::new([create_random_dynamic_attr()]) as Box<[Attribute]>)
.collect(),
);
Some(node)
node
}
_ => None,
_ => VNode::default(),
};
// println!("{node:#?}");
node
Element::Ok(node)
}
// test for panics when creating random nodes and templates
+1 -1
View File
@@ -37,6 +37,7 @@ fn dual_stream() {
assert_eq!(edits.edits, {
[
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
HydrateText { path: &[0, 0], value: "123".to_string(), id: ElementId(2) },
SetAttribute {
name: "class",
value: "asd 123 123".into_value(),
@@ -44,7 +45,6 @@ fn dual_stream() {
ns: None,
},
NewEventListener { name: "click".to_string(), id: ElementId(1) },
HydrateText { path: &[0, 0], value: "123".to_string(), id: ElementId(2) },
AppendChildren { id: ElementId(0), m: 1 },
]
});
+2 -2
View File
@@ -52,7 +52,7 @@ fn events_generate() {
"Click me!"
}
},
_ => None,
_ => VNode::empty(),
}
};
@@ -66,7 +66,7 @@ fn events_generate() {
true,
);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
let edits = dom.render_immediate_to_vec();
assert_eq!(
+7 -7
View File
@@ -9,7 +9,7 @@ fn app_drops() {
let mut dom = VirtualDom::new(app);
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
_ = dom.render_immediate_to_vec();
}
@@ -27,7 +27,7 @@ fn hooks_drop() {
let mut dom = VirtualDom::new(app);
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
_ = dom.render_immediate_to_vec();
}
@@ -51,7 +51,7 @@ fn contexts_drop() {
let mut dom = VirtualDom::new(app);
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
_ = dom.render_immediate_to_vec();
}
@@ -68,7 +68,7 @@ fn tasks_drop() {
let mut dom = VirtualDom::new(app);
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
_ = dom.render_immediate_to_vec();
}
@@ -83,7 +83,7 @@ fn root_props_drop() {
);
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
_ = dom.render_immediate_to_vec();
}
@@ -113,7 +113,7 @@ fn diffing_drops_old() {
let mut dom = VirtualDom::new(app);
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
_ = dom.render_immediate_to_vec();
}
@@ -141,6 +141,6 @@ fn hooks_drop_before_contexts() {
let mut dom = VirtualDom::new(app);
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
_ = dom.render_immediate_to_vec();
}
+3 -3
View File
@@ -63,7 +63,7 @@ fn test_memory_leak() {
dom.rebuild(&mut dioxus_core::NoOpMutations);
for _ in 0..5 {
dom.mark_dirty(ScopeId::ROOT);
dom.mark_dirty(ScopeId::APP);
_ = dom.render_immediate_to_vec();
}
}
@@ -74,7 +74,7 @@ fn memo_works_properly() {
let val = generation();
if val == 2 || val == 4 {
return None;
return Element::Ok(VNode::default());
}
let name = use_hook(|| String::from("asd"));
@@ -123,7 +123,7 @@ fn free_works_on_root_hooks() {
dom.rebuild(&mut dioxus_core::NoOpMutations);
// ptr gets cloned into props and then into the hook
assert_eq!(Rc::strong_count(&ptr), 5);
assert!(Rc::strong_count(&ptr) > 1);
drop(dom);
+324 -17
View File
@@ -2,6 +2,22 @@ use dioxus::prelude::*;
use std::future::poll_fn;
use std::task::Poll;
async fn poll_three_times() {
// Poll each task 3 times
let mut count = 0;
poll_fn(|cx| {
println!("polling... {}", count);
if count < 3 {
count += 1;
cx.waker().wake_by_ref();
Poll::Pending
} else {
Poll::Ready(())
}
})
.await;
}
#[test]
fn suspense_resolves() {
// wait just a moment, not enough time for the boundary to resolve
@@ -15,8 +31,6 @@ fn suspense_resolves() {
let out = dioxus_ssr::render(&dom);
assert_eq!(out, "<div>Waiting for... child</div>");
dbg!(out);
});
}
@@ -24,7 +38,10 @@ fn app() -> Element {
rsx!(
div {
"Waiting for... "
suspended_child {}
SuspenseBoundary {
fallback: |_| rsx! { "fallback" },
suspended_child {}
}
}
)
}
@@ -43,20 +60,7 @@ fn suspended_child() -> Element {
if val() < 3 {
let task = spawn(async move {
// Poll each task 3 times
let mut count = 0;
poll_fn(|cx| {
println!("polling... {}", count);
if count < 3 {
count += 1;
cx.waker().wake_by_ref();
Poll::Pending
} else {
Poll::Ready(())
}
})
.await;
poll_three_times().await;
println!("waiting... {}", val);
val += 1;
});
@@ -65,3 +69,306 @@ fn suspended_child() -> Element {
rsx!("child")
}
/// When switching from a suspense fallback to the real child, the state of that component must be kept
#[test]
fn suspense_keeps_state() {
tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap()
.block_on(async {
let mut dom = VirtualDom::new(app);
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.render_suspense_immediate().await;
let out = dioxus_ssr::render(&dom);
assert_eq!(out, "fallback");
dom.wait_for_suspense().await;
let out = dioxus_ssr::render(&dom);
assert_eq!(out, "<div>child with future resolved</div>");
});
fn app() -> Element {
rsx! {
SuspenseBoundary {
fallback: |_| rsx! { "fallback" },
Child {}
}
}
}
#[component]
fn Child() -> Element {
let mut future_resolved = use_signal(|| false);
let task = use_hook(|| {
spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
future_resolved.set(true);
})
});
if !future_resolved() {
suspend(task)?;
}
println!("future resolved: {future_resolved:?}");
if future_resolved() {
rsx! {
div { "child with future resolved" }
}
} else {
rsx! {
div { "this should never be rendered" }
}
}
}
}
/// spawn doesn't run in suspense
#[test]
fn suspense_does_not_poll_spawn() {
tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap()
.block_on(async {
let mut dom = VirtualDom::new(app);
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.wait_for_suspense().await;
let out = dioxus_ssr::render(&dom);
assert_eq!(out, "<div>child with future resolved</div>");
});
fn app() -> Element {
rsx! {
SuspenseBoundary {
fallback: |_| rsx! { "fallback" },
Child {}
}
}
}
#[component]
fn Child() -> Element {
let mut future_resolved = use_signal(|| false);
// futures that are spawned, but not suspended should never be polled
use_hook(|| {
spawn(async move {
panic!("Non-suspended task was polled");
});
});
let task = use_hook(|| {
spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
future_resolved.set(true);
})
});
if !future_resolved() {
suspend(task)?;
}
rsx! {
div { "child with future resolved" }
}
}
}
/// suspended nodes are not mounted, so they should not run effects
#[test]
fn suspended_nodes_dont_trigger_effects() {
tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap()
.block_on(async {
let mut dom = VirtualDom::new(app);
dom.rebuild(&mut dioxus_core::NoOpMutations);
let work = async move {
loop {
dom.wait_for_work().await;
dom.render_immediate(&mut dioxus_core::NoOpMutations);
}
};
tokio::select! {
_ = work => {},
_ = tokio::time::sleep(std::time::Duration::from_millis(100)) => {}
}
});
fn app() -> Element {
rsx! {
SuspenseBoundary {
fallback: |_| rsx! { "fallback" },
Child {}
}
}
}
#[component]
fn RerendersFrequently() -> Element {
let mut count = use_signal(|| 0);
use_future(move || async move {
for _ in 0..100 {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
count.set(count() + 1);
}
});
rsx! {
div { "rerenders frequently" }
}
}
#[component]
fn Child() -> Element {
let mut future_resolved = use_signal(|| false);
use_effect(|| panic!("effects should not run during suspense"));
let task = use_hook(|| {
spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
future_resolved.set(true);
})
});
if !future_resolved() {
suspend(task)?;
}
rsx! {
div { "child with future resolved" }
}
}
}
/// Make sure we keep any state of components when we switch from a resolved future to a suspended future
#[test]
fn resolved_to_suspended() {
tracing_subscriber::fmt::SubscriberBuilder::default()
.with_max_level(tracing::Level::INFO)
.init();
static SUSPENDED: GlobalSignal<bool> = Signal::global(|| false);
tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap()
.block_on(async {
let mut dom = VirtualDom::new(app);
dom.rebuild(&mut dioxus_core::NoOpMutations);
let out = dioxus_ssr::render(&dom);
assert_eq!(out, "rendered 1 times");
dom.in_runtime(|| ScopeId::APP.in_runtime(|| *SUSPENDED.write() = true));
dom.render_suspense_immediate().await;
let out = dioxus_ssr::render(&dom);
assert_eq!(out, "fallback");
dom.wait_for_suspense().await;
let out = dioxus_ssr::render(&dom);
assert_eq!(out, "rendered 3 times");
});
fn app() -> Element {
rsx! {
SuspenseBoundary {
fallback: |_| rsx! { "fallback" },
Child {}
}
}
}
#[component]
fn Child() -> Element {
let mut render_count = use_signal(|| 0);
render_count += 1;
let mut task = use_hook(|| CopyValue::new(None));
tracing::info!("render_count: {}", render_count.peek());
if SUSPENDED() {
if task().is_none() {
task.set(Some(spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
tracing::info!("task finished");
*SUSPENDED.write() = false;
})));
}
suspend(task().unwrap())?;
}
rsx! {
"rendered {render_count.peek()} times"
}
}
}
/// Make sure suspense tells the renderer that a suspense boundary was resolved
#[test]
fn suspense_tracks_resolved() {
tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap()
.block_on(async {
let mut dom = VirtualDom::new(app);
dom.rebuild(&mut dioxus_core::NoOpMutations);
dom.render_suspense_immediate().await;
dom.wait_for_suspense_work().await;
assert_eq!(
dom.render_suspense_immediate().await,
vec![ScopeId(ScopeId::APP.0 + 1)]
);
});
fn app() -> Element {
rsx! {
SuspenseBoundary {
fallback: |_| rsx! { "fallback" },
Child {}
}
}
}
#[component]
fn Child() -> Element {
let mut resolved = use_signal(|| false);
let task = use_hook(|| {
spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
tracing::info!("task finished");
resolved.set(true);
})
});
if resolved() {
println!("suspense is resolved");
} else {
println!("suspense is not resolved");
suspend(task)?;
}
rsx! {
"child"
}
}
}
-92
View File
@@ -126,95 +126,3 @@ async fn yield_now_works() {
SEQUENCE.with(|s| assert_eq!(s.borrow().len(), 20));
}
/// Ensure that calling wait_for_flush waits for dioxus to finish its synchronous work
#[tokio::test]
async fn flushing() {
thread_local! {
static SEQUENCE: std::cell::RefCell<Vec<usize>> = const { std::cell::RefCell::new(Vec::new()) };
static BROADCAST: (tokio::sync::broadcast::Sender<()>, tokio::sync::broadcast::Receiver<()>) = tokio::sync::broadcast::channel(1);
}
fn app() -> Element {
if generation() > 0 {
println!("App");
SEQUENCE.with(|s| s.borrow_mut().push(0));
}
// The next two tasks mimic effects. They should only be run after the app has been rendered
use_hook(|| {
spawn(async move {
let mut channel = BROADCAST.with(|b| b.1.resubscribe());
for _ in 0..10 {
wait_for_next_render().await;
println!("Task 1 recved");
channel.recv().await.unwrap();
println!("Task 1");
SEQUENCE.with(|s| s.borrow_mut().push(1));
}
})
});
use_hook(|| {
spawn(async move {
let mut channel = BROADCAST.with(|b| b.1.resubscribe());
for _ in 0..10 {
wait_for_next_render().await;
println!("Task 2 recved");
channel.recv().await.unwrap();
println!("Task 2");
SEQUENCE.with(|s| s.borrow_mut().push(2));
}
})
});
rsx! {}
}
let mut dom = VirtualDom::new(app);
dom.rebuild(&mut dioxus_core::NoOpMutations);
let fut = async {
// Trigger the flush by waiting for work
for i in 0..10 {
BROADCAST.with(|b| b.0.send(()).unwrap());
dom.mark_dirty(ScopeId(0));
dom.wait_for_work().await;
dom.render_immediate(&mut dioxus_core::NoOpMutations);
println!("Flushed {}", i);
}
BROADCAST.with(|b| b.0.send(()).unwrap());
dom.wait_for_work().await;
};
tokio::select! {
_ = fut => {}
_ = tokio::time::sleep(Duration::from_millis(500)) => {
println!("Aborting due to timeout");
}
};
SEQUENCE.with(|s| {
let s = s.borrow();
println!("{:?}", s);
assert_eq!(s.len(), 30);
// We need to check if every three elements look like [0, 1, 2] or [0, 2, 1]
let mut has_seen_1 = false;
for (i, &x) in s.iter().enumerate() {
let stage = i % 3;
if stage == 0 {
assert_eq!(x, 0);
} else if stage == 1 {
assert!(x == 1 || x == 2);
has_seen_1 = x == 1;
} else if stage == 2 {
if has_seen_1 {
assert_eq!(x, 2);
} else {
assert_eq!(x, 1);
}
}
}
});
}