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:
1 parent
ffa36a67c3
commit
022e4ad203
175 files changed
+7872
-4010
No files matched your search
+213
-126
@@ -2,22 +2,22 @@
|
||||
//!
|
||||
//! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust.
|
||||
|
||||
use crate::Task;
|
||||
use crate::innerlude::{SuspenseBoundaryProps, Work};
|
||||
use crate::properties::RootProps;
|
||||
use crate::root_wrapper::RootScopeWrapper;
|
||||
use crate::{
|
||||
any_props::AnyProps,
|
||||
arena::ElementId,
|
||||
innerlude::{
|
||||
DirtyTasks, ElementRef, ErrorBoundary, NoOpMutations, SchedulerMsg, ScopeOrder, ScopeState,
|
||||
VNodeMount, VProps, WriteMutations,
|
||||
ElementRef, NoOpMutations, SchedulerMsg, ScopeOrder, ScopeState, VNodeMount, VProps,
|
||||
WriteMutations,
|
||||
},
|
||||
nodes::RenderReturn,
|
||||
nodes::{Template, TemplateId},
|
||||
runtime::{Runtime, RuntimeGuard},
|
||||
scopes::ScopeId,
|
||||
AttributeValue, ComponentFunction, Element, Event, Mutations, VNode,
|
||||
AttributeValue, ComponentFunction, Element, Event, Mutations,
|
||||
};
|
||||
use crate::{Task, VComponent};
|
||||
use futures_util::StreamExt;
|
||||
use rustc_hash::FxHashMap;
|
||||
use slab::Slab;
|
||||
use std::collections::BTreeSet;
|
||||
use std::{any::Any, rc::Rc};
|
||||
@@ -206,18 +206,25 @@ pub struct VirtualDom {
|
||||
pub(crate) scopes: Slab<ScopeState>,
|
||||
|
||||
pub(crate) dirty_scopes: BTreeSet<ScopeOrder>,
|
||||
pub(crate) dirty_tasks: BTreeSet<DirtyTasks>,
|
||||
|
||||
// Maps a template path to a map of byte indexes to templates
|
||||
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template>>,
|
||||
// if hot reload is enabled, we need to keep track of template overrides
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) templates: rustc_hash::FxHashMap<TemplateId, rustc_hash::FxHashMap<usize, Template>>,
|
||||
// Otherwise, we just need to keep track of what templates we have registered
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub(crate) templates: rustc_hash::FxHashSet<TemplateId>,
|
||||
|
||||
// Templates changes that are queued for the next render
|
||||
pub(crate) queued_templates: Vec<Template>,
|
||||
|
||||
// The element ids that are used in the renderer
|
||||
// These mark a specific place in a whole rsx block
|
||||
pub(crate) elements: Slab<Option<ElementRef>>,
|
||||
|
||||
// Once nodes are mounted, the information about where they are mounted is stored here
|
||||
// We need to store this information on the virtual dom so that we know what nodes are mounted where when we bubble events
|
||||
// Each mount is associated with a whole rsx block. [`VirtualDom::elements`] link to a specific node in the block
|
||||
pub(crate) mounts: Slab<VNodeMount>,
|
||||
|
||||
pub(crate) runtime: Rc<Runtime>,
|
||||
@@ -297,7 +304,13 @@ impl VirtualDom {
|
||||
root: impl ComponentFunction<P, M>,
|
||||
root_props: P,
|
||||
) -> Self {
|
||||
Self::new_with_component(VProps::new(root, |_, _| true, root_props, "root"))
|
||||
let render_fn = root.id();
|
||||
let props = VProps::new(root, |_, _| true, root_props, "Root");
|
||||
Self::new_with_component(VComponent {
|
||||
name: "root",
|
||||
render_fn,
|
||||
props: Box::new(props),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new virtualdom and build it immediately
|
||||
@@ -309,7 +322,7 @@ impl VirtualDom {
|
||||
|
||||
/// Create a new VirtualDom from something that implements [`AnyProps`]
|
||||
#[instrument(skip(root), level = "trace", name = "VirtualDom::new")]
|
||||
pub(crate) fn new_with_component(root: impl AnyProps + 'static) -> Self {
|
||||
pub(crate) fn new_with_component(root: VComponent) -> Self {
|
||||
let (tx, rx) = futures_channel::mpsc::unbounded();
|
||||
|
||||
let mut dom = Self {
|
||||
@@ -317,18 +330,19 @@ impl VirtualDom {
|
||||
runtime: Runtime::new(tx),
|
||||
scopes: Default::default(),
|
||||
dirty_scopes: Default::default(),
|
||||
dirty_tasks: Default::default(),
|
||||
templates: Default::default(),
|
||||
queued_templates: Default::default(),
|
||||
elements: Default::default(),
|
||||
mounts: Default::default(),
|
||||
};
|
||||
|
||||
let root = dom.new_scope(Box::new(root), "app");
|
||||
|
||||
// Unlike react, we provide a default error boundary that just renders the error as a string
|
||||
root.state()
|
||||
.provide_context(Rc::new(ErrorBoundary::new_in_scope(ScopeId::ROOT)));
|
||||
let root = VProps::new(
|
||||
RootScopeWrapper,
|
||||
|_, _| true,
|
||||
RootProps(root),
|
||||
"RootWrapper",
|
||||
);
|
||||
dom.new_scope(Box::new(root), "app");
|
||||
|
||||
// the root element is always given element ID 0 since it's the container for the entire tree
|
||||
dom.elements.insert(None);
|
||||
@@ -404,7 +418,7 @@ impl VirtualDom {
|
||||
tracing::Level::TRACE,
|
||||
"Marking task {:?} (spawned in {:?}) as dirty",
|
||||
task,
|
||||
scope.id
|
||||
scope.id,
|
||||
);
|
||||
|
||||
let order = ScopeOrder::new(scope.height(), scope.id);
|
||||
@@ -485,7 +499,7 @@ impl VirtualDom {
|
||||
SchedulerMsg::TaskNotified(id) => {
|
||||
// Instead of running the task immediately, we insert it into the runtime's task queue.
|
||||
// The task may be marked dirty at the same time as the scope that owns the task is dropped.
|
||||
self.mark_task_dirty(id);
|
||||
self.mark_task_dirty(Task::from_id(id));
|
||||
}
|
||||
SchedulerMsg::EffectQueued => {}
|
||||
};
|
||||
@@ -497,7 +511,7 @@ impl VirtualDom {
|
||||
while let Ok(Some(msg)) = self.rx.try_next() {
|
||||
match msg {
|
||||
SchedulerMsg::Immediate(id) => self.mark_dirty(id),
|
||||
SchedulerMsg::TaskNotified(task) => self.mark_task_dirty(task),
|
||||
SchedulerMsg::TaskNotified(task) => self.mark_task_dirty(Task::from_id(task)),
|
||||
SchedulerMsg::EffectQueued => {}
|
||||
}
|
||||
}
|
||||
@@ -524,24 +538,18 @@ impl VirtualDom {
|
||||
|
||||
// Keep polling tasks until there are no more effects or tasks to run
|
||||
// Or until we have no more dirty scopes
|
||||
while !self.dirty_tasks.is_empty() || !self.runtime.pending_effects.borrow().is_empty() {
|
||||
while !self.runtime.dirty_tasks.borrow().is_empty()
|
||||
|| !self.runtime.pending_effects.borrow().is_empty()
|
||||
{
|
||||
// Next, run any queued tasks
|
||||
// We choose not to poll the deadline since we complete pretty quickly anyways
|
||||
while let Some(task) = self.pop_task() {
|
||||
// Then poll any tasks that might be pending
|
||||
let mut tasks = task.tasks_queued.into_inner();
|
||||
while let Some(task) = tasks.pop_front() {
|
||||
let _ = self.runtime.handle_task_wakeup(task);
|
||||
let _ = self.runtime.handle_task_wakeup(task);
|
||||
|
||||
// Running that task, may mark a scope higher up as dirty. If it does, return from the function early
|
||||
self.queue_events();
|
||||
if self.has_dirty_scopes() {
|
||||
// requeue any remaining tasks
|
||||
for task in tasks {
|
||||
self.mark_task_dirty(task);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Running that task, may mark a scope higher up as dirty. If it does, return from the function early
|
||||
self.queue_events();
|
||||
if self.has_dirty_scopes() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,41 +573,45 @@ impl VirtualDom {
|
||||
/// This will only replace the parent template, not any nested templates.
|
||||
#[instrument(skip(self), level = "trace", name = "VirtualDom::replace_template")]
|
||||
pub fn replace_template(&mut self, template: Template) {
|
||||
self.register_template_first_byte_index(template);
|
||||
// we only replace templates if hot reloading is enabled
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.register_template_first_byte_index(template);
|
||||
|
||||
// iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
|
||||
let mut dirty = Vec::new();
|
||||
for (id, scope) in self.scopes.iter() {
|
||||
// Recurse into the dynamic nodes of the existing mounted node to see if the template is alive in the tree
|
||||
fn check_node_for_templates(node: &VNode, template: Template) -> bool {
|
||||
let this_template_name = node.template.get().name.rsplit_once(':').unwrap().0;
|
||||
// iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
|
||||
let mut dirty = Vec::new();
|
||||
for (id, scope) in self.scopes.iter() {
|
||||
// Recurse into the dynamic nodes of the existing mounted node to see if the template is alive in the tree
|
||||
fn check_node_for_templates(node: &crate::VNode, template: Template) -> bool {
|
||||
let this_template_name = node.template.get().name.rsplit_once(':').unwrap().0;
|
||||
|
||||
if this_template_name == template.name.rsplit_once(':').unwrap().0 {
|
||||
return true;
|
||||
}
|
||||
if this_template_name == template.name.rsplit_once(':').unwrap().0 {
|
||||
return true;
|
||||
}
|
||||
|
||||
for dynamic in node.dynamic_nodes.iter() {
|
||||
if let crate::DynamicNode::Fragment(nodes) = dynamic {
|
||||
for node in nodes {
|
||||
if check_node_for_templates(node, template) {
|
||||
return true;
|
||||
for dynamic in node.dynamic_nodes.iter() {
|
||||
if let crate::DynamicNode::Fragment(nodes) = dynamic {
|
||||
for node in nodes {
|
||||
if check_node_for_templates(node, template) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
if let Some(RenderReturn::Ready(sync)) = scope.try_root_node() {
|
||||
if check_node_for_templates(sync, template) {
|
||||
dirty.push(ScopeId(id));
|
||||
if let Some(sync) = scope.try_root_node() {
|
||||
if check_node_for_templates(sync, template) {
|
||||
dirty.push(ScopeId(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for dirty in dirty {
|
||||
self.mark_dirty(dirty);
|
||||
for dirty in dirty {
|
||||
self.mark_dirty(dirty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,8 +659,10 @@ impl VirtualDom {
|
||||
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||
let new_nodes = self.run_scope(ScopeId::ROOT);
|
||||
|
||||
self.scopes[ScopeId::ROOT.0].last_rendered_node = Some(new_nodes.clone());
|
||||
|
||||
// Rebuilding implies we append the created elements to the root
|
||||
let m = self.create_scope(to, ScopeId::ROOT, new_nodes, None);
|
||||
let m = self.create_scope(Some(to), ScopeId::ROOT, new_nodes, None);
|
||||
|
||||
to.append_children(ElementId(0), m);
|
||||
}
|
||||
@@ -666,22 +680,17 @@ impl VirtualDom {
|
||||
|
||||
// Next, diff any dirty scopes
|
||||
// We choose not to poll the deadline since we complete pretty quickly anyways
|
||||
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||
while let Some(work) = self.pop_work() {
|
||||
{
|
||||
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||
|
||||
// Then, poll any tasks that might be pending in the scope
|
||||
for task in work.tasks {
|
||||
let _ = self.runtime.handle_task_wakeup(task);
|
||||
match work {
|
||||
Work::PollTask(task) => {
|
||||
_ = self.runtime.handle_task_wakeup(task);
|
||||
// Make sure we process any new events
|
||||
self.queue_events();
|
||||
}
|
||||
|
||||
self.queue_events();
|
||||
|
||||
// If the scope is dirty, run the scope and get the mutations
|
||||
if work.rerun_scope {
|
||||
let new_nodes = self.run_scope(work.scope.id);
|
||||
|
||||
self.diff_scope(to, work.scope.id, new_nodes);
|
||||
Work::RerunScope(scope) => {
|
||||
// If the scope is dirty, run the scope and get the mutations
|
||||
self.run_and_diff_scope(Some(to), scope.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -705,70 +714,127 @@ impl VirtualDom {
|
||||
#[instrument(skip(self), level = "trace", name = "VirtualDom::wait_for_suspense")]
|
||||
pub async fn wait_for_suspense(&mut self) {
|
||||
loop {
|
||||
if self.runtime.suspended_tasks.get() == 0 {
|
||||
if !self.suspended_tasks_remaining() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Wait for a work to be ready (IE new suspense leaves to pop up)
|
||||
'wait_for_work: loop {
|
||||
// Process all events - Scopes are marked dirty, etc
|
||||
// Sometimes when wakers fire we get a slew of updates at once, so its important that we drain this completely
|
||||
self.queue_events();
|
||||
self.wait_for_suspense_work().await;
|
||||
|
||||
// Now that we have collected all queued work, we should check if we have any dirty scopes. If there are not, then we can poll any queued futures
|
||||
if self.has_dirty_scopes() {
|
||||
break;
|
||||
}
|
||||
self.render_suspense_immediate().await;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Make sure we set the runtime since we're running user code
|
||||
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||
// Next, run any queued tasks
|
||||
// We choose not to poll the deadline since we complete pretty quickly anyways
|
||||
while let Some(task) = self.pop_task() {
|
||||
// Then poll any tasks that might be pending
|
||||
let mut tasks = task.tasks_queued.into_inner();
|
||||
while let Some(task) = tasks.pop_front() {
|
||||
if self.runtime.task_runs_during_suspense(task) {
|
||||
let _ = self.runtime.handle_task_wakeup(task);
|
||||
// Running that task, may mark a scope higher up as dirty. If it does, return from the function early
|
||||
self.queue_events();
|
||||
if self.has_dirty_scopes() {
|
||||
// requeue any remaining tasks
|
||||
for task in tasks {
|
||||
self.mark_task_dirty(task);
|
||||
}
|
||||
break 'wait_for_work;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Check if there are any suspended tasks remaining
|
||||
pub fn suspended_tasks_remaining(&self) -> bool {
|
||||
self.runtime.suspended_tasks.get() > 0
|
||||
}
|
||||
|
||||
self.wait_for_event().await;
|
||||
/// Wait for the scheduler to have any work that should be run during suspense.
|
||||
pub async fn wait_for_suspense_work(&mut self) {
|
||||
// Wait for a work to be ready (IE new suspense leaves to pop up)
|
||||
loop {
|
||||
// Process all events - Scopes are marked dirty, etc
|
||||
// Sometimes when wakers fire we get a slew of updates at once, so its important that we drain this completely
|
||||
self.queue_events();
|
||||
|
||||
// Now that we have collected all queued work, we should check if we have any dirty scopes. If there are not, then we can poll any queued futures
|
||||
if self.has_dirty_scopes() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Render whatever work needs to be rendered, unlocking new futures and suspense leaves
|
||||
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||
while let Some(work) = self.pop_work() {
|
||||
// Then, poll any tasks that might be pending in the scope
|
||||
for task in work.tasks {
|
||||
{
|
||||
// Make sure we set the runtime since we're running user code
|
||||
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||
// Next, run any queued tasks
|
||||
// We choose not to poll the deadline since we complete pretty quickly anyways
|
||||
let mut tasks_polled = 0;
|
||||
while let Some(task) = self.pop_task() {
|
||||
if self.runtime.task_runs_during_suspense(task) {
|
||||
let _ = self.runtime.handle_task_wakeup(task);
|
||||
// Running that task, may mark a scope higher up as dirty. If it does, return from the function early
|
||||
self.queue_events();
|
||||
if self.has_dirty_scopes() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
tasks_polled += 1;
|
||||
// Once we have polled a few tasks, we manually yield to the scheduler to give it a chance to run other pending work
|
||||
if tasks_polled > 32 {
|
||||
yield_now().await;
|
||||
tasks_polled = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.wait_for_event().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Render any dirty scopes immediately, but don't poll any futures that are client only on that scope
|
||||
/// Returns a list of suspense boundaries that were resolved
|
||||
pub async fn render_suspense_immediate(&mut self) -> Vec<ScopeId> {
|
||||
// Queue any new events before we start working
|
||||
self.queue_events();
|
||||
|
||||
let mut resolved_scopes = Vec::new();
|
||||
|
||||
// Render whatever work needs to be rendered, unlocking new futures and suspense leaves
|
||||
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||
|
||||
let mut work_done = 0;
|
||||
while let Some(work) = self.pop_work() {
|
||||
match work {
|
||||
Work::PollTask(task) => {
|
||||
// During suspense, we only want to run tasks that are suspended
|
||||
if self.runtime.task_runs_during_suspense(task) {
|
||||
let _ = self.runtime.handle_task_wakeup(task);
|
||||
}
|
||||
}
|
||||
Work::RerunScope(scope) => {
|
||||
if self
|
||||
.runtime
|
||||
.get_state(scope.id)
|
||||
.filter(|scope| scope.should_run_during_suspense())
|
||||
.is_some()
|
||||
{
|
||||
let scope_state = self.get_scope(scope.id).unwrap();
|
||||
let was_suspended =
|
||||
SuspenseBoundaryProps::downcast_ref_from_props(&*scope_state.props)
|
||||
.filter(|props| props.suspended())
|
||||
.is_some();
|
||||
// If the scope is dirty, run the scope and get the mutations
|
||||
self.run_and_diff_scope(None::<&mut NoOpMutations>, scope.id);
|
||||
let scope_state = self.get_scope(scope.id).unwrap();
|
||||
let is_now_suspended =
|
||||
SuspenseBoundaryProps::downcast_ref_from_props(&*scope_state.props)
|
||||
.filter(|props| props.suspended())
|
||||
.is_some();
|
||||
|
||||
self.queue_events();
|
||||
|
||||
// If the scope is dirty, run the scope and get the mutations
|
||||
if work.rerun_scope {
|
||||
let new_nodes = self.run_scope(work.scope.id);
|
||||
|
||||
self.diff_scope(&mut NoOpMutations, work.scope.id, new_nodes);
|
||||
if is_now_suspended {
|
||||
resolved_scopes.retain(|&id| id != scope.id);
|
||||
} else if was_suspended {
|
||||
resolved_scopes.push(scope.id);
|
||||
}
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Scope {:?} was marked as dirty, but will not rerun during suspense. Only nodes that are under a suspense boundary rerun during suspense",
|
||||
scope.id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Queue any new events
|
||||
self.queue_events();
|
||||
work_done += 1;
|
||||
// Once we have polled a few tasks, we manually yield to the scheduler to give it a chance to run other pending work
|
||||
if work_done > 32 {
|
||||
yield_now().await;
|
||||
work_done = 0;
|
||||
}
|
||||
}
|
||||
|
||||
resolved_scopes.sort_by_key(|&id| self.runtime.get_state(id).unwrap().height);
|
||||
resolved_scopes
|
||||
}
|
||||
|
||||
/// Get the current runtime
|
||||
@@ -817,7 +883,11 @@ impl VirtualDom {
|
||||
while let Some(path) = parent {
|
||||
let mut listeners = vec![];
|
||||
|
||||
let el_ref = &self.mounts[path.mount.0].node;
|
||||
let Some(mount) = self.mounts.get(path.mount.0) else {
|
||||
// If the node is suspended and not mounted, we can just ignore the event
|
||||
return;
|
||||
};
|
||||
let el_ref = &mount.node;
|
||||
let node_template = el_ref.template.get();
|
||||
let target_path = path.path;
|
||||
|
||||
@@ -827,9 +897,7 @@ impl VirtualDom {
|
||||
|
||||
for attr in attrs.iter() {
|
||||
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
||||
if attr.name.trim_start_matches("on") == name
|
||||
&& target_path.is_decendant(this_path)
|
||||
{
|
||||
if attr.name.get(2..) == Some(name) && target_path.is_decendant(this_path) {
|
||||
listeners.push(&attr.value);
|
||||
|
||||
// Break if this is the exact target element.
|
||||
@@ -874,7 +942,11 @@ impl VirtualDom {
|
||||
name = "VirtualDom::handle_non_bubbling_event"
|
||||
)]
|
||||
fn handle_non_bubbling_event(&mut self, node: ElementRef, name: &str, uievent: Event<dyn Any>) {
|
||||
let el_ref = &self.mounts[node.mount.0].node;
|
||||
let Some(mount) = self.mounts.get(node.mount.0) else {
|
||||
// If the node is suspended and not mounted, we can just ignore the event
|
||||
return;
|
||||
};
|
||||
let el_ref = &mount.node;
|
||||
let node_template = el_ref.template.get();
|
||||
let target_path = node.path;
|
||||
|
||||
@@ -884,7 +956,7 @@ impl VirtualDom {
|
||||
for attr in attrs.iter() {
|
||||
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
||||
// Only call the listener if this is the exact target element.
|
||||
if attr.name.trim_start_matches("on") == name && target_path == this_path {
|
||||
if attr.name.get(2..) == Some(name) && target_path == this_path {
|
||||
if let AttributeValue::Listener(listener) = &attr.value {
|
||||
self.runtime.rendering.set(false);
|
||||
listener.call(uievent.clone());
|
||||
@@ -907,3 +979,18 @@ impl Drop for VirtualDom {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Yield control back to the async scheduler. This is used to give the scheduler a chance to run other pending work. Or cancel the task if the client has disconnected.
|
||||
async fn yield_now() {
|
||||
let mut yielded = false;
|
||||
std::future::poll_fn::<(), _>(move |cx| {
|
||||
if !yielded {
|
||||
cx.waker().wake_by_ref();
|
||||
yielded = true;
|
||||
std::task::Poll::Pending
|
||||
} else {
|
||||
std::task::Poll::Ready(())
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
Reference in new issue
Block a user