diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index d31e5ab47..8efdf13e4 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -5,10 +5,9 @@ use crate::mutations::Mutation::*; use crate::nodes::VNode; use crate::nodes::{DynamicNode, TemplateNode}; use crate::virtual_dom::VirtualDom; -use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, SuspenseContext, Template}; +use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, Template}; use std::cell::Cell; use std::iter::Peekable; -use std::rc::Rc; use TemplateNode::*; #[cfg(debug_assertions)] @@ -445,7 +444,7 @@ impl<'b> VirtualDom { match node { Text(text) => self.create_dynamic_text(template, text, idx), Placeholder(place) => self.create_placeholder(place, template, idx), - Component(component) => self.create_component_node(template, component, idx), + Component(component) => self.create_component_node(template, component), Fragment(frag) => frag.iter().map(|child| self.create(child)).sum(), } } @@ -502,7 +501,6 @@ impl<'b> VirtualDom { &mut self, template: &'b VNode<'b>, component: &'b VComponent<'b>, - idx: usize, ) -> usize { use RenderReturn::*; @@ -512,7 +510,8 @@ impl<'b> VirtualDom { component.scope.set(Some(scope)); match unsafe { self.run_scope(scope).extend_lifetime_ref() } { - Ready(t) => self.mount_component(scope, template, t, idx), + // Create the component's root element + Ready(t) => self.create_scope(scope, t), Aborted(t) => self.mount_aborted(template, t), } } @@ -529,60 +528,6 @@ impl<'b> VirtualDom { .unwrap_or_else(|| component.scope.get().unwrap()) } - fn mount_component( - &mut self, - scope: ScopeId, - parent: &'b VNode<'b>, - new: &'b VNode<'b>, - idx: usize, - ) -> usize { - // Keep track of how many mutations are in the buffer in case we need to split them out if a suspense boundary - // is encountered - let mutations_to_this_point = self.mutations.edits.len(); - - // Create the component's root element - let created = self.create_scope(scope, new); - - // If there are no suspense leaves below us, then just don't bother checking anything suspense related - if self.collected_leaves.is_empty() { - return created; - } - - // If running the scope has collected some leaves and *this* component is a boundary, then handle the suspense - let boundary = match self.scopes[scope].has_context::>() { - Some(boundary) => boundary, - _ => return created, - }; - - // Since this is a boundary, use its placeholder within the template as the placeholder for the suspense tree - let new_id = self.next_element(new, parent.template.get().node_paths[idx]); - - // Now connect everything to the boundary - self.scopes[scope].placeholder.set(Some(new_id)); - - // This involves breaking off the mutations to this point, and then creating a new placeholder for the boundary - // Note that we break off dynamic mutations only - since static mutations aren't rendered immediately - let split_off = unsafe { - std::mem::transmute::, Vec>( - self.mutations.edits.split_off(mutations_to_this_point), - ) - }; - boundary.mutations.borrow_mut().edits.extend(split_off); - boundary.created_on_stack.set(created); - boundary - .waiting_on - .borrow_mut() - .extend(self.collected_leaves.drain(..)); - - // Now assign the placeholder in the DOM - self.mutations.push(AssignId { - id: new_id, - path: &parent.template.get().node_paths[idx][1..], - }); - - 0 - } - fn mount_aborted(&mut self, parent: &'b VNode<'b>, placeholder: &VPlaceholder) -> usize { let id = self.next_element(parent, &[]); self.mutations.push(Mutation::CreatePlaceholder { id }); diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index c06e46d3f..cfda18505 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -124,9 +124,8 @@ impl<'b> VirtualDom { .dynamic_nodes .iter() .zip(right_template.dynamic_nodes.iter()) - .enumerate() - .for_each(|(idx, (left_node, right_node))| { - self.diff_dynamic_node(left_node, right_node, right_template, idx); + .for_each(|(left_node, right_node)| { + self.diff_dynamic_node(left_node, right_node, right_template); }); // Make sure the roots get transferred over while we're here @@ -145,13 +144,12 @@ impl<'b> VirtualDom { left_node: &'b DynamicNode<'b>, right_node: &'b DynamicNode<'b>, node: &'b VNode<'b>, - idx: usize, ) { match (left_node, right_node) { (Text(left), Text(right)) => self.diff_vtext(left, right, node), (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right), (Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()), - (Component(left), Component(right)) => self.diff_vcomponent(left, right, node, idx), + (Component(left), Component(right)) => self.diff_vcomponent(left, right, node), (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right), (Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right), _ => todo!("This is an usual custom case for dynamic nodes. We don't know how to handle it yet."), @@ -175,7 +173,6 @@ impl<'b> VirtualDom { left: &'b VComponent<'b>, right: &'b VComponent<'b>, right_template: &'b VNode<'b>, - idx: usize, ) { if std::ptr::eq(left, right) { return; @@ -183,7 +180,7 @@ impl<'b> VirtualDom { // Replace components that have different render fns if left.render_fn != right.render_fn { - return self.replace_vcomponent(right_template, right, idx, left); + return self.replace_vcomponent(right_template, right, left); } // Make sure the new vcomponent has the right scopeid associated to it @@ -220,10 +217,9 @@ impl<'b> VirtualDom { &mut self, right_template: &'b VNode<'b>, right: &'b VComponent<'b>, - idx: usize, left: &'b VComponent<'b>, ) { - let m = self.create_component_node(right_template, right, idx); + let m = self.create_component_node(right_template, right); let pre_edits = self.mutations.edits.len(); @@ -282,8 +278,7 @@ impl<'b> VirtualDom { None => self.replace(left, [right]), Some(components) => components .into_iter() - .enumerate() - .for_each(|(idx, (l, r))| self.diff_vcomponent(l, r, right, idx)), + .for_each(|(l, r)| self.diff_vcomponent(l, r, right)), } } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 304197dab..514999242 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -72,7 +72,7 @@ pub(crate) mod innerlude { pub use crate::innerlude::{ fc_to_builder, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue, CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode, LazyNodes, Mutation, - Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, SuspenseContext, + Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, VirtualDom, }; diff --git a/packages/core/src/scheduler/mod.rs b/packages/core/src/scheduler/mod.rs index f5c540dd3..a13958363 100644 --- a/packages/core/src/scheduler/mod.rs +++ b/packages/core/src/scheduler/mod.rs @@ -1,11 +1,9 @@ use crate::ScopeId; use slab::Slab; -mod suspense; mod task; mod wait; -pub use suspense::*; pub use task::*; /// The type of message that can be sent to the scheduler. diff --git a/packages/core/src/scheduler/suspense.rs b/packages/core/src/scheduler/suspense.rs index ad3d525b7..77c2981ae 100644 --- a/packages/core/src/scheduler/suspense.rs +++ b/packages/core/src/scheduler/suspense.rs @@ -11,17 +11,10 @@ use std::{ collections::HashSet, }; -/// An ID representing an ongoing suspended component -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub(crate) struct SuspenseId(pub usize); - /// A boundary in the VirtualDom that captures all suspended components below it pub struct SuspenseContext { pub(crate) id: ScopeId, - pub(crate) waiting_on: RefCell>, - pub(crate) mutations: RefCell>, - pub(crate) placeholder: Cell>, - pub(crate) created_on_stack: Cell, + pub(crate) waiting_on: RefCell>, } impl SuspenseContext { @@ -30,9 +23,10 @@ impl SuspenseContext { Self { id, waiting_on: Default::default(), - mutations: RefCell::new(Mutations::default()), - placeholder: Cell::new(None), - created_on_stack: Cell::new(0), } } + + pub fn mark_suspend(&self, id: ScopeId) { + self.waiting_on.borrow_mut().insert(id); + } } diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index 4e56f50f0..0a7dce207 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -1,5 +1,5 @@ -use crate::{innerlude::SuspenseContext, ScopeId, TaskId, VirtualDom}; -use std::{rc::Rc, task::Context}; +use crate::{TaskId, VirtualDom}; +use std::task::Context; impl VirtualDom { /// Handle notifications by tasks inside the scheduler @@ -27,10 +27,4 @@ impl VirtualDom { tasks.try_remove(id.0); } } - - pub(crate) fn acquire_suspense_boundary(&self, id: ScopeId) -> Rc { - self.scopes[id] - .consume_context::>() - .unwrap() - } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 96bf8e08b..306afe16b 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -25,10 +25,10 @@ impl VirtualDom { name, props: Some(props), tasks: self.scheduler.clone(), - placeholder: Default::default(), node_arena_1: BumpFrame::new(0), node_arena_2: BumpFrame::new(0), spawned_tasks: Default::default(), + suspended: Default::default(), render_cnt: Default::default(), hooks: Default::default(), hook_idx: Default::default(), @@ -54,6 +54,7 @@ impl VirtualDom { self.scopes[scope_id].previous_frame().bump_mut().reset(); let scope = &self.scopes[scope_id]; + scope.suspended.set(false); scope.hook_idx.set(0); @@ -82,6 +83,10 @@ impl VirtualDom { id: scope.id, }); + if scope.suspended.get() { + self.suspended_scopes.insert(scope.id); + } + // rebind the lifetime now that its stored internally unsafe { allocated.extend_lifetime_ref() } } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 02b9bb65a..7bc26fb02 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -1,7 +1,6 @@ use crate::{ any_props::AnyProps, any_props::VProps, - arena::ElementId, bump_frame::BumpFrame, innerlude::{DynamicNode, EventHandler, VComponent, VText}, innerlude::{ErrorBoundary, Scheduler, SchedulerMsg}, @@ -169,6 +168,7 @@ pub struct ScopeState { pub(crate) id: ScopeId, pub(crate) height: u32, + pub(crate) suspended: Cell, pub(crate) hooks: RefCell>>>, pub(crate) hook_idx: Cell, @@ -182,7 +182,6 @@ pub struct ScopeState { pub(crate) attributes_to_drop: RefCell>>, pub(crate) props: Option>>, - pub(crate) placeholder: Cell>, } impl<'src> ScopeState { @@ -655,6 +654,12 @@ impl<'src> ScopeState { None } + /// Mark this component as suspended and then return None + pub fn suspend(&self) -> Option<()> { + self.suspended.set(true); + None + } + /// Store a value between renders. The foundational hook for all other hooks. /// /// Accepts an `initializer` closure, which is run on the first use of the hook (typically the initial render). The return value of this closure is stored for the lifetime of the component, and a mutable reference to it is provided on every render as the return value of `use_hook`. diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 96c935d1b..1f056cfae 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -9,14 +9,13 @@ use crate::{ mutations::Mutation, nodes::RenderReturn, nodes::{Template, TemplateId}, - scheduler::SuspenseId, scopes::{ScopeId, ScopeState}, - AttributeValue, Element, Event, Scope, SuspenseContext, + AttributeValue, Element, Event, Scope, }; use futures_util::{pin_mut, StreamExt}; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use slab::Slab; -use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future::Future, rc::Rc}; +use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc}; /// A virtual node system that progresses user events and diffs UI trees. /// @@ -186,11 +185,9 @@ pub struct VirtualDom { // While diffing we need some sort of way of breaking off a stream of suspended mutations. pub(crate) scope_stack: Vec, - pub(crate) collected_leaves: Vec, - // Whenever a suspense tree is finished, we push its boundary onto this stack. - // When "render_with_deadline" is called, we pop the stack and return the mutations - pub(crate) finished_fibers: Vec, + // Currently suspended scopes + pub(crate) suspended_scopes: FxHashSet, pub(crate) rx: futures_channel::mpsc::UnboundedReceiver, @@ -262,8 +259,7 @@ impl VirtualDom { elements: Default::default(), scope_stack: Vec::new(), dirty_scopes: BTreeSet::new(), - collected_leaves: Vec::new(), - finished_fibers: Vec::new(), + suspended_scopes: FxHashSet::default(), mutations: Mutations::default(), }; @@ -272,12 +268,6 @@ impl VirtualDom { "app", ); - // The root component is always a suspense boundary for any async children - // This could be unexpected, so we might rethink this behavior later - // - // We *could* just panic if the suspense boundary is not found - root.provide_context(Rc::new(SuspenseContext::new(ScopeId(0)))); - // Unlike react, we provide a default error boundary that just renders the error as a string root.provide_context(Rc::new(ErrorBoundary::new(ScopeId(0)))); @@ -319,26 +309,6 @@ impl VirtualDom { } } - /// Determine whether or not a scope is currently in a suspended state - /// - /// This does not mean the scope is waiting on its own futures, just that the tree that the scope exists in is - /// currently suspended. - pub fn is_scope_suspended(&self, id: ScopeId) -> bool { - !self.scopes[id] - .consume_context::>() - .unwrap() - .waiting_on - .borrow() - .is_empty() - } - - /// Determine if the tree is at all suspended. Used by SSR and other outside mechanisms to determine if the tree is - /// ready to be rendered. - pub fn has_suspended_work(&self) -> bool { - todo!() - // !self.scheduler.leaves.borrow().is_empty() - } - /// Call a listener inside the VirtualDom with data from outside the VirtualDom. /// /// This method will identify the appropriate element. The data must match up with the listener delcared. Note that @@ -495,7 +465,7 @@ impl VirtualDom { Ok(None) => return, Err(_) => { // If we have any dirty scopes, or finished fiber trees then we should exit - if !self.dirty_scopes.is_empty() || !self.finished_fibers.is_empty() { + if !self.dirty_scopes.is_empty() || !self.suspended_scopes.is_empty() { return; } @@ -596,6 +566,19 @@ impl VirtualDom { } } + /// Render the virtual dom, waiting for all suspense to be finished + /// + /// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content + pub async fn wait_for_suspense(&mut self) { + loop { + if self.suspended_scopes.is_empty() { + break; + } + + self.wait_for_work().await; + } + } + /// Render what you can given the timeline and then move on /// /// It's generally a good idea to put some sort of limit on the suspense process in case a future is having issues. @@ -607,26 +590,6 @@ impl VirtualDom { self.process_events(); loop { - // first, unload any complete suspense trees - for finished_fiber in self.finished_fibers.drain(..) { - let scope = &self.scopes[finished_fiber]; - let context = scope.has_context::>().unwrap(); - - self.mutations - .templates - .append(&mut context.mutations.borrow_mut().templates); - - self.mutations - .edits - .append(&mut context.mutations.borrow_mut().edits); - - // TODO: count how many nodes are on the stack? - self.mutations.push(Mutation::ReplaceWith { - id: context.placeholder.get().unwrap(), - m: 1, - }) - } - // Next, diff any dirty scopes // We choose not to poll the deadline since we complete pretty quickly anyways if let Some(dirty) = self.dirty_scopes.iter().next().cloned() { @@ -637,40 +600,9 @@ impl VirtualDom { continue; } - // if the scope is currently suspended, then we should skip it, ignoring any tasks calling for an update - if self.is_scope_suspended(dirty.id) { - continue; - } - - // Save the current mutations length so we can split them into boundary - let mutations_to_this_point = self.mutations.edits.len(); - // Run the scope and get the mutations self.run_scope(dirty.id); self.diff_scope(dirty.id); - - // If suspended leaves are present, then we should find the boundary for this scope and attach things - // No placeholder necessary since this is a diff - if !self.collected_leaves.is_empty() { - let mut boundary = self.scopes[dirty.id] - .consume_context::>() - .unwrap(); - - let boundary_mut = boundary.borrow_mut(); - - // Attach mutations - boundary_mut - .mutations - .borrow_mut() - .edits - .extend(self.mutations.edits.split_off(mutations_to_this_point)); - - // Attach suspended leaves - boundary - .waiting_on - .borrow_mut() - .extend(self.collected_leaves.drain(..)); - } } // If there's more work, then just continue, plenty of work to do diff --git a/packages/core/tests/safety.rs b/packages/core/tests/safety.rs index 8c125f4c6..e5763674d 100644 --- a/packages/core/tests/safety.rs +++ b/packages/core/tests/safety.rs @@ -3,7 +3,6 @@ use std::rc::Rc; use dioxus::prelude::*; -use dioxus_core::SuspenseContext; /// Ensure no issues with not calling rebuild #[test] @@ -17,8 +16,4 @@ fn root_node_isnt_null() { // The height should be 0 assert_eq!(scope.height(), 0); - - // There should be a default suspense context - // todo: there should also be a default error boundary - assert!(scope.has_context::>().is_some()); } diff --git a/packages/core/tests/suspense.rs b/packages/core/tests/suspense.rs index 708494dd0..2b90a37da 100644 --- a/packages/core/tests/suspense.rs +++ b/packages/core/tests/suspense.rs @@ -1,5 +1,5 @@ use dioxus::core::ElementId; -use dioxus::core::{Mutation::*, SuspenseContext}; +use dioxus::core::Mutation::*; use dioxus::prelude::*; use std::future::IntoFuture; use std::rc::Rc;