diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 55e0ff30a..9479bfc83 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -34,6 +34,7 @@ indexmap = "1.7" # Serialize the Edits for use in Webview/Liveview instances serde = { version = "1", features = ["derive"], optional = true } anyhow = "1.0.66" +bumpslab = "0.1.0" [dev-dependencies] tokio = { version = "*", features = ["full"] } diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index 532fe02ca..797fb8ddd 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -18,7 +18,6 @@ pub(crate) struct VProps<'a, P, A, F: ComponentReturn<'a, A> = Element<'a>> { pub render_fn: fn(Scope<'a, P>) -> F, pub memo: unsafe fn(&P, &P) -> bool, pub props: P, - // pub props: PropsAllocation

, _marker: PhantomData, } @@ -35,7 +34,6 @@ where render_fn, memo, props, - // props: PropsAllocation::Borrowed(props), _marker: PhantomData, } } @@ -60,7 +58,7 @@ where } fn render(&'a self, cx: &'a ScopeState) -> RenderReturn<'a> { - let scope = cx.bump().alloc(Scoped { + let scope: &mut Scoped

= cx.bump().alloc(Scoped { props: &self.props, scope: cx, }); diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index a0da5b6ef..6add899e8 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -1,10 +1,14 @@ -use crate::{nodes::VNode, virtual_dom::VirtualDom, Mutations, ScopeId}; +use crate::{ + factory::RenderReturn, nodes::VNode, virtual_dom::VirtualDom, AttributeValue, DynamicNode, + ScopeId, VFragment, +}; +use bumpalo::boxed::Box as BumpBox; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct ElementId(pub usize); -pub struct ElementRef { +pub(crate) struct ElementRef { // the pathway of the real element inside the template pub path: ElementPath, @@ -19,7 +23,7 @@ pub enum ElementPath { } impl ElementRef { - pub fn null() -> Self { + pub(crate) fn null() -> Self { Self { template: std::ptr::null_mut(), path: ElementPath::Root(0), @@ -28,74 +32,88 @@ impl ElementRef { } impl<'b> VirtualDom { - pub fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId { + pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId { let entry = self.elements.vacant_entry(); let id = entry.key(); - entry.insert(ElementRef { template: template as *const _ as *mut _, path: ElementPath::Deep(path), }); - - println!("Claiming {}", id); - ElementId(id) } - pub fn next_root(&mut self, template: &VNode, path: usize) -> ElementId { + pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId { let entry = self.elements.vacant_entry(); let id = entry.key(); - entry.insert(ElementRef { template: template as *const _ as *mut _, path: ElementPath::Root(path), }); - - println!("Claiming {}", id); - ElementId(id) } - pub fn cleanup_element(&mut self, id: ElementId) { - self.elements.remove(id.0); + pub(crate) fn reclaim(&mut self, el: ElementId) { + self.try_reclaim(el) + .unwrap_or_else(|| panic!("cannot reclaim {:?}", el)); } - pub fn drop_scope(&mut self, id: ScopeId) { - // let scope = self.scopes.get(id.0).unwrap(); - - // let root = scope.root_node(); - // let root = unsafe { std::mem::transmute(root) }; - - // self.drop_template(root, false); - todo!() - } - - pub fn reclaim(&mut self, el: ElementId) { + pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option { assert_ne!(el, ElementId(0)); - self.elements.remove(el.0); + self.elements.try_remove(el.0) } - pub fn drop_template( - &mut self, - mutations: &mut Mutations, - template: &'b VNode<'b>, - gen_roots: bool, - ) { - // for node in template.dynamic_nodes.iter() { - // match node { - // DynamicNode::Text { id, .. } => {} + // Drop a scope and all its children + pub(crate) fn drop_scope(&mut self, id: ScopeId) { + let scope = self.scopes.get(id.0).unwrap(); - // DynamicNode::Component { .. } => { - // todo!() - // } + if let Some(root) = scope.as_ref().try_root_node() { + let root = unsafe { root.extend_lifetime_ref() }; + match root { + RenderReturn::Sync(Ok(node)) => self.drop_scope_inner(node), + _ => {} + } + } - // DynamicNode::Fragment { inner, nodes } => {} - // DynamicNode::Placeholder(_) => todo!(), - // _ => todo!(), - // } - // } + let scope = self.scopes.get(id.0).unwrap(); + + if let Some(root) = unsafe { scope.as_ref().previous_frame().try_load_node() } { + let root = unsafe { root.extend_lifetime_ref() }; + match root { + RenderReturn::Sync(Ok(node)) => self.drop_scope_inner(node), + _ => {} + } + } + + let scope = self.scopes.get(id.0).unwrap().as_ref(); + + // Drop all the hooks once the children are dropped + // this means we'll drop hooks bottom-up + for hook in scope.hook_list.borrow_mut().drain(..) { + drop(unsafe { BumpBox::from_raw(hook) }); + } + } + + fn drop_scope_inner(&mut self, node: &VNode) { + for attr in node.dynamic_attrs { + if let AttributeValue::Listener(l) = &attr.value { + l.borrow_mut().take(); + } + } + + for (idx, _) in node.template.roots.iter().enumerate() { + match node.dynamic_root(idx) { + Some(DynamicNode::Component(c)) => self.drop_scope(c.scope.get().unwrap()), + Some(DynamicNode::Fragment(VFragment::NonEmpty(nodes))) => { + for node in *nodes { + self.drop_scope_inner(node); + } + } + _ => {} + } + } } } + impl ElementPath { pub(crate) fn is_ascendant(&self, big: &&[u8]) -> bool { match *self { diff --git a/packages/core/src/bump_frame.rs b/packages/core/src/bump_frame.rs index ca7ff66f6..581452836 100644 --- a/packages/core/src/bump_frame.rs +++ b/packages/core/src/bump_frame.rs @@ -2,9 +2,9 @@ use crate::factory::RenderReturn; use bumpalo::Bump; use std::cell::Cell; -pub struct BumpFrame { +pub(crate) struct BumpFrame { pub bump: Bump, - pub node: Cell<*mut RenderReturn<'static>>, + pub node: Cell<*const RenderReturn<'static>>, } impl BumpFrame { @@ -12,17 +12,18 @@ impl BumpFrame { let bump = Bump::with_capacity(capacity); Self { bump, - node: Cell::new(std::ptr::null_mut()), + node: Cell::new(std::ptr::null()), } } - pub fn reset(&mut self) { - self.bump.reset(); - self.node.set(std::ptr::null_mut()); - } - /// Creates a new lifetime out of thin air - pub unsafe fn load_node<'b>(&self) -> &'b RenderReturn<'b> { - unsafe { std::mem::transmute(&*self.node.get()) } + pub unsafe fn try_load_node<'b>(&self) -> Option<&'b RenderReturn<'b>> { + let node = self.node.get(); + + if node.is_null() { + return None; + } + + unsafe { std::mem::transmute(&*node) } } } diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index fc7ab37c7..1a308a577 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -5,9 +5,9 @@ use crate::mutations::Mutation::*; use crate::nodes::VNode; use crate::nodes::{DynamicNode, TemplateNode}; use crate::virtual_dom::VirtualDom; -use crate::{AttributeValue, ElementId, ScopeId, SuspenseContext, TemplateAttribute}; +use crate::{AttributeValue, ScopeId, SuspenseContext, TemplateAttribute}; -impl<'b: 'static> VirtualDom { +impl<'b> VirtualDom { /// Create a new template [`VNode`] and write it to the [`Mutations`] buffer. /// /// This method pushes the ScopeID to the internal scopestack and returns the number of nodes created. @@ -44,7 +44,14 @@ impl<'b: 'static> VirtualDom { DynamicNode::Text(VText { id: slot, value }) => { let id = self.next_element(template, template.template.node_paths[*id]); slot.set(id); - self.mutations.push(CreateTextNode { value, id }); + + // Safety: we promise not to re-alias this text later on after committing it to the mutation + let unbounded_text = unsafe { std::mem::transmute(*value) }; + self.mutations.push(CreateTextNode { + value: unbounded_text, + id, + }); + 1 } @@ -96,16 +103,24 @@ impl<'b: 'static> VirtualDom { let attribute = template.dynamic_attrs.get(attr_id).unwrap(); attribute.mounted_element.set(id); + // Safety: we promise not to re-alias this text later on after committing it to the mutation + let unbounded_name = unsafe { std::mem::transmute(attribute.name) }; + match &attribute.value { - AttributeValue::Text(value) => self.mutations.push(SetAttribute { - name: attribute.name, - value: *value, - ns: attribute.namespace, - id, - }), + AttributeValue::Text(value) => { + // Safety: we promise not to re-alias this text later on after committing it to the mutation + let unbounded_value = unsafe { std::mem::transmute(*value) }; + + self.mutations.push(SetAttribute { + name: unbounded_name, + value: unbounded_value, + ns: attribute.namespace, + id, + }) + } AttributeValue::Bool(value) => { self.mutations.push(SetBoolAttribute { - name: attribute.name, + name: unbounded_name, value: *value, id, }) @@ -113,7 +128,7 @@ impl<'b: 'static> VirtualDom { AttributeValue::Listener(_) => { self.mutations.push(NewEventListener { // all listeners start with "on" - event_name: &attribute.name[2..], + event_name: &unbounded_name[2..], scope: cur_scope, id, }) @@ -315,11 +330,14 @@ impl<'b: 'static> VirtualDom { // Make sure the text node is assigned to the correct element text.id.set(new_id); + // Safety: we promise not to re-alias this text later on after committing it to the mutation + let value = unsafe { std::mem::transmute(text.value) }; + // Add the mutation to the list self.mutations.push(HydrateText { id: new_id, path: &template.template.node_paths[idx][1..], - value: text.value, + value, }); // Since we're hydrating an existing node, we don't create any new nodes @@ -356,18 +374,20 @@ impl<'b: 'static> VirtualDom { } } - fn create_component_node( + pub(super) fn create_component_node( &mut self, template: &'b VNode<'b>, component: &'b VComponent<'b>, idx: usize, ) -> usize { - let props = component.props.replace(None).unwrap(); + let props = component + .props + .replace(None) + .expect("Props to always exist when a component is being created"); - let prop_ptr = unsafe { std::mem::transmute(props.as_ref()) }; - let scope = self.new_scope(prop_ptr).id; + let unbounded_props = unsafe { std::mem::transmute(props) }; - component.props.replace(Some(props)); + let scope = self.new_scope(unbounded_props).id; component.scope.set(Some(scope)); let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() }; @@ -375,10 +395,7 @@ impl<'b: 'static> VirtualDom { use RenderReturn::*; match return_nodes { - Sync(Ok(t)) => { - self.mount_component(scope, template, t, idx) - // self.create(t) - } + Sync(Ok(t)) => self.mount_component(scope, template, t, idx), Sync(Err(_e)) => todo!("Propogate error upwards"), Async(_) => self.mount_component_placeholder(template, idx, scope), } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 75141e002..669288ca7 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -13,39 +13,29 @@ use fxhash::{FxHashMap, FxHashSet}; use std::cell::Cell; use DynamicNode::*; -impl<'b: 'static> VirtualDom { +impl<'b> VirtualDom { pub fn diff_scope(&mut self, scope: ScopeId) { let scope_state = &mut self.scopes[scope.0]; - // Load the old and new bump arenas - let cur_arena = scope_state.current_frame(); - let prev_arena = scope_state.previous_frame(); - - // Make sure the nodes arent null (they've been set properly) - // This is a rough check to make sure we're not entering any UB - assert_ne!( - cur_arena.node.get(), - std::ptr::null_mut(), - "Call rebuild before diffing" - ); - assert_ne!( - prev_arena.node.get(), - std::ptr::null_mut(), - "Call rebuild before diffing" - ); - self.scope_stack.push(scope); unsafe { - let cur_arena = cur_arena.load_node(); - let prev_arena = prev_arena.load_node(); - self.diff_maybe_node(prev_arena, cur_arena); + // Load the old and new bump arenas + let old = scope_state + .previous_frame() + .try_load_node() + .expect("Call rebuild before diffing"); + let new = scope_state + .current_frame() + .try_load_node() + .expect("Call rebuild before diffing"); + self.diff_maybe_node(old, new); } self.scope_stack.pop(); } - fn diff_maybe_node(&mut self, left: &'b RenderReturn<'b>, right: &'b RenderReturn<'b>) { + fn diff_maybe_node(&mut self, old: &'b RenderReturn<'b>, new: &'b RenderReturn<'b>) { use RenderReturn::{Async, Sync}; - match (left, right) { + match (old, new) { (Sync(Ok(l)), Sync(Ok(r))) => self.diff_node(l, r), // Err cases @@ -62,10 +52,10 @@ impl<'b: 'static> VirtualDom { } } - fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, e: &anyhow::Error) { + fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _e: &anyhow::Error) { todo!("Not yet handling error rollover") } - fn diff_err_to_ok(&mut self, e: &anyhow::Error, l: &'b VNode<'b>) { + fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) { todo!("Not yet handling error rollover") } @@ -88,11 +78,13 @@ impl<'b: 'static> VirtualDom { // todo: add more types of attribute values match right_attr.value { AttributeValue::Text(text) => { + let name = unsafe { std::mem::transmute(left_attr.name) }; + let value = unsafe { std::mem::transmute(text) }; self.mutations.push(Mutation::SetAttribute { id: left_attr.mounted_element.get(), - name: left_attr.name, - value: text, ns: right_attr.namespace, + name, + value, }); } // todo: more types of attribute values @@ -101,15 +93,18 @@ impl<'b: 'static> VirtualDom { } } - for (left_node, right_node) in left_template + for (idx, (left_node, right_node)) in left_template .dynamic_nodes .iter() .zip(right_template.dynamic_nodes.iter()) + .enumerate() { match (left_node, right_node) { (Text(left), Text(right)) => self.diff_vtext(left, right), (Fragment(left), Fragment(right)) => self.diff_vfragment(left, right), - (Component(left), Component(right)) => self.diff_vcomponent(left, right), + (Component(left), Component(right)) => { + self.diff_vcomponent(left, right, right_template, idx) + } _ => self.replace(left_template, right_template), }; } @@ -124,24 +119,44 @@ impl<'b: 'static> VirtualDom { } } - fn diff_vcomponent(&mut self, left: &'b VComponent<'b>, right: &'b VComponent<'b>) { - // Due to how templates work, we should never get two different components. The only way we could enter - // this codepath is through "light_diff", but we check there that the pointers are the same - assert_eq!(left.render_fn, right.render_fn); + fn diff_vcomponent( + &mut self, + left: &'b VComponent<'b>, + right: &'b VComponent<'b>, + right_template: &'b VNode<'b>, + idx: usize, + ) { + // Replace components that have different render fns + if left.render_fn != right.render_fn { + dbg!(&left, &right); + let created = self.create_component_node(right_template, right, idx); + let head = unsafe { + self.scopes[left.scope.get().unwrap().0] + .root_node() + .extend_lifetime_ref() + }; + let id = match head { + RenderReturn::Sync(Ok(node)) => self.replace_inner(node), + _ => todo!(), + }; + self.mutations + .push(Mutation::ReplaceWith { id, m: created }); + return; + } // Make sure the new vcomponent has the right scopeid associated to it let scope_id = left.scope.get().unwrap(); right.scope.set(Some(scope_id)); // copy out the box for both - let old = left.props.replace(None).unwrap(); + let old = self.scopes[scope_id.0].props.as_ref(); let new = right.props.replace(None).unwrap(); // If the props are static, then we try to memoize by setting the new with the old // The target scopestate still has the reference to the old props, so there's no need to update anything // This also implicitly drops the new props since they're not used if left.static_props && unsafe { old.memoize(new.as_ref()) } { - return right.props.set(Some(old)); + return; } // If the props are dynamic *or* the memoization failed, then we need to diff the props @@ -198,7 +213,8 @@ impl<'b: 'static> VirtualDom { None => self.replace(left, right), Some(components) => components .into_iter() - .for_each(|(l, r)| self.diff_vcomponent(l, r)), + .enumerate() + .for_each(|(idx, (l, r))| self.diff_vcomponent(l, r, right, idx)), } } @@ -207,12 +223,12 @@ impl<'b: 'static> VirtualDom { /// This just moves the ID of the old node over to the new node, and then sets the text of the new node if it's /// different. fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>) { - right.id.set(left.id.get()); + let id = left.id.get(); + + right.id.set(id); if left.value != right.value { - self.mutations.push(Mutation::SetText { - id: left.id.get(), - value: right.value, - }); + let value = unsafe { std::mem::transmute(right.value) }; + self.mutations.push(Mutation::SetText { id, value }); } } @@ -234,22 +250,25 @@ impl<'b: 'static> VirtualDom { } fn replace_nodes_with_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b Cell) { + // Create the placeholder first, ensuring we get a dedicated ID for the placeholder + let placeholder = self.next_element(&l[0], &[]); + r.set(placeholder); + self.mutations + .push(Mutation::CreatePlaceholder { id: placeholder }); + // Remove the old nodes, except for onea let first = self.replace_inner(&l[0]); self.remove_nodes(&l[1..]); - let placeholder = self.next_element(&l[0], &[]); - r.set(placeholder); - - self.mutations - .push(Mutation::CreatePlaceholder { id: placeholder }); self.mutations .push(Mutation::ReplaceWith { id: first, m: 1 }); - self.reclaim(first); + self.try_reclaim(first); } - // Remove all the top-level nodes, returning the firstmost root ElementId + /// Remove all the top-level nodes, returning the firstmost root ElementId + /// + /// All IDs will be garbage collected fn replace_inner(&mut self, node: &'b VNode<'b>) -> ElementId { let id = match node.dynamic_root(0) { None => node.root_ids[0].get(), @@ -284,8 +303,13 @@ impl<'b: 'static> VirtualDom { /// /// Simply walks through the dynamic nodes fn clean_up_node(&mut self, node: &'b VNode<'b>) { - for node in node.dynamic_nodes { - match node { + for (idx, dyn_node) in node.dynamic_nodes.iter().enumerate() { + // Roots are cleaned up automatically? + if node.template.node_paths[idx].len() == 1 { + continue; + } + + match dyn_node { Component(comp) => { let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { @@ -596,10 +620,9 @@ impl<'b: 'static> VirtualDom { // 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() { - if let Some(first_old) = old.get(0) { + if let Some(_) = old.get(0) { self.remove_nodes(&old[1..]); - let nodes_created = self.create_children(new); - self.replace_node_with_on_stack(first_old, nodes_created); + self.replace_many(&old[0], new); } else { // I think this is wrong - why are we appending? // only valid of the if there are no trailing elements @@ -718,10 +741,6 @@ impl<'b: 'static> VirtualDom { } } - fn replace_node_with_on_stack(&mut self, old: &'b VNode<'b>, m: usize) { - todo!() - } - /// Remove these nodes from the dom /// Wont generate mutations for the inner nodes fn remove_nodes(&mut self, nodes: &'b [VNode<'b>]) { @@ -766,7 +785,7 @@ impl<'b: 'static> VirtualDom { } /// Push all the real nodes on the stack - fn push_all_real_nodes(&mut self, node: &VNode) -> usize { + fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize { let mut onstack = 0; for (idx, _) in node.template.roots.iter().enumerate() { @@ -785,7 +804,12 @@ impl<'b: 'static> VirtualDom { } } Some(Component(comp)) => { - todo!() + let scope = comp.scope.get().unwrap(); + onstack += + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(node)) => self.push_all_real_nodes(node), + _ => todo!(), + } } None => { self.mutations.push(Mutation::PushRoot { @@ -815,7 +839,7 @@ impl<'b: 'static> VirtualDom { self.mutations.push(Mutation::InsertAfter { id, m }) } - fn find_first_element(&self, node: &VNode) -> ElementId { + fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId { match node.dynamic_root(0) { None => node.root_ids[0].get(), Some(Text(t)) => t.id.get(), @@ -823,7 +847,7 @@ impl<'b: 'static> VirtualDom { Some(Fragment(VFragment::Empty(t))) => t.get(), Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); - match self.scopes[scope.0].root_node() { + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { RenderReturn::Sync(Ok(t)) => self.find_first_element(t), _ => todo!("cannot handle nonstandard nodes"), } @@ -831,7 +855,7 @@ impl<'b: 'static> VirtualDom { } } - fn find_last_element(&self, node: &VNode) -> ElementId { + fn find_last_element(&self, node: &'b VNode<'b>) -> ElementId { match node.dynamic_root(node.template.roots.len() - 1) { None => node.root_ids.last().unwrap().get(), Some(Text(t)) => t.id.get(), @@ -839,7 +863,7 @@ impl<'b: 'static> VirtualDom { Some(Fragment(VFragment::Empty(t))) => t.get(), Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); - match self.scopes[scope.0].root_node() { + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { RenderReturn::Sync(Ok(t)) => self.find_last_element(t), _ => todo!("cannot handle nonstandard nodes"), } @@ -855,7 +879,18 @@ impl<'b: 'static> VirtualDom { id: first, m: created, }); - self.reclaim(id); + self.try_reclaim(id); + } + + fn replace_many(&mut self, left: &'b VNode<'b>, right: &'b [VNode<'b>]) { + let first = self.find_first_element(left); + let id = self.replace_inner(left); + let created = self.create_children(right); + self.mutations.push(Mutation::ReplaceWith { + id: first, + m: created, + }); + self.try_reclaim(id); } } @@ -883,7 +918,7 @@ fn matching_components<'a>( _ => return None, }; - (l.render_fn == r.render_fn).then(|| (l, r)) + Some((l, r)) }) .collect() } @@ -894,6 +929,7 @@ fn matching_components<'a>( /// - for text - we can use SetTextContent /// - for clearning children we can use RemoveChildren /// - for appending children we can use AppendChildren +#[allow(dead_code)] fn is_dyn_node_only_child(node: &VNode, idx: usize) -> bool { let path = node.template.node_paths[idx]; diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs index c2a1947da..6f40c2484 100644 --- a/packages/core/src/error_boundary.rs +++ b/packages/core/src/error_boundary.rs @@ -1,29 +1,19 @@ -use std::{cell::RefCell, rc::Rc}; +use std::cell::RefCell; -use crate::{ScopeId, ScopeState}; +use crate::ScopeId; -pub struct ErrorContext { +/// A boundary that will capture any errors from child components +#[allow(dead_code)] +pub struct ErrorBoundary { error: RefCell>, + id: ScopeId, } -/// Catch all errors from the children and bubble them up to this component -/// -/// Returns the error and scope that caused the error -pub fn use_catch_error(cx: &ScopeState) -> Option<&(anyhow::Error, ScopeId)> { - let err_ctx = use_error_context(cx); - - let out = cx.use_hook(|| None); - - if let Some(error) = err_ctx.error.take() { - *out = Some(error); +impl ErrorBoundary { + pub fn new(id: ScopeId) -> Self { + Self { + error: RefCell::new(None), + id, + } } - - out.as_ref() -} - -/// Create a new error context at this component. -/// -/// This component will start to catch any errors that occur in its children. -pub fn use_error_context(cx: &ScopeState) -> &ErrorContext { - cx.use_hook(|| cx.provide_context(Rc::new(ErrorContext { error: None.into() }))) } diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index cb14734d2..f511e73f9 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -50,58 +50,6 @@ impl std::fmt::Debug for UiEvent { } } -/// Priority of Event Triggers. -/// -/// Internally, Dioxus will abort work that's taking too long if new, more important work arrives. Unlike React, Dioxus -/// won't be afraid to pause work or flush changes to the Real Dom. This is called "cooperative scheduling". Some Renderers -/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well. -/// -/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes. -/// -/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now, -/// we keep it simple, and just use a 3-tier priority system. -/// -/// - `NoPriority` = 0 -/// - `LowPriority` = 1 -/// - `NormalPriority` = 2 -/// - `UserBlocking` = 3 -/// - `HighPriority` = 4 -/// - `ImmediatePriority` = 5 -/// -/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will. -/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be -/// flushed before proceeding. Multiple discrete events is highly unlikely, though. -#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] -pub enum EventPriority { - /// Work that must be completed during the EventHandler phase. - /// - /// Currently this is reserved for controlled inputs. - Immediate = 3, - - /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work. - /// - /// This is typically reserved for things like user interaction. - /// - /// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate). - High = 2, - - /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important - /// than "High Priority" events and will take precedence over low priority events. - /// - /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input. - /// - /// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc). - Medium = 1, - - /// "Low Priority" work will always be preempted unless the work is significantly delayed, in which case it will be - /// advanced to the front of the work queue until completed. - /// - /// The primary user of Low Priority work is the asynchronous work system (Suspense). - /// - /// This is considered "idle" work or "background" work. - Low = 0, -} - type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>; /// The callback type generated by the `rsx!` macro when an `on` field is specified for components. diff --git a/packages/core/src/factory.rs b/packages/core/src/factory.rs index d92899f3d..a529d38bc 100644 --- a/packages/core/src/factory.rs +++ b/packages/core/src/factory.rs @@ -82,9 +82,9 @@ impl ScopeState { // let as_dyn: &dyn AnyProps = self.bump().alloc(vcomp); // todo: clean up borrowed props // if !P::IS_STATIC { - // let vcomp = &*vcomp; - // let vcomp = unsafe { std::mem::transmute(vcomp) }; - // self.scope.items.borrow_mut().borrowed_props.push(vcomp); + // let vcomp = ex; + // let vcomp = unsafe { std::mem::transmute(vcomp) }; + // self.items.borrow_mut().borrowed_props.push(vcomp); // } DynamicNode::Component(VComponent { diff --git a/packages/core/src/fragment.rs b/packages/core/src/fragment.rs index f2e539d17..ebe9104e1 100644 --- a/packages/core/src/fragment.rs +++ b/packages/core/src/fragment.rs @@ -26,17 +26,17 @@ use crate::innerlude::*; /// /// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it. #[allow(non_upper_case_globals, non_snake_case)] -pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element { - let children = cx.props.0.as_ref().unwrap(); - Ok(VNode { - node_id: children.node_id.clone(), - key: children.key.clone(), - parent: children.parent.clone(), - template: children.template, - root_ids: children.root_ids, - dynamic_nodes: children.dynamic_nodes, - dynamic_attrs: children.dynamic_attrs, - }) +pub fn Fragment<'a>(_cx: Scope<'a, FragmentProps<'a>>) -> Element { + // let children = cx.props.0.as_ref().unwrap(); + todo!() + // Ok(VNode { + // key: children.key.clone(), + // parent: children.parent.clone(), + // template: children.template, + // root_ids: children.root_ids, + // dynamic_nodes: children.dynamic_nodes, + // dynamic_attrs: children.dynamic_attrs, + // }) } pub struct FragmentProps<'a>(Element<'a>); @@ -96,7 +96,7 @@ impl<'a> Properties for FragmentProps<'a> { type Builder = FragmentBuilder<'a, false>; const IS_STATIC: bool = false; fn builder() -> Self::Builder { - FragmentBuilder(VNode::empty()) + todo!() } unsafe fn memoize(&self, _other: &Self) -> bool { false diff --git a/packages/core/src/future_container.rs b/packages/core/src/future_container.rs deleted file mode 100644 index 0d2f5b549..000000000 --- a/packages/core/src/future_container.rs +++ /dev/null @@ -1,63 +0,0 @@ -use futures_channel::mpsc::UnboundedSender; -use slab::Slab; -use std::future::Future; -use std::{cell::RefCell, rc::Rc, sync::Arc}; - -use crate::innerlude::ScopeId; -/// The type of message that can be sent to the scheduler. -/// -/// These messages control how the scheduler will process updates to the UI. -#[derive(Debug)] -pub enum SchedulerMsg { - /// Events from athe Renderer - Event, - - /// Immediate updates from Components that mark them as dirty - Immediate(ScopeId), - - /// Mark all components as dirty and update them - DirtyAll, - - /// New tasks from components that should be polled when the next poll is ready - NewTask(ScopeId), -} - -// todo extract this so spawning doesn't require refcell and rc doesnt need to be tracked -#[derive(Clone)] -pub struct FutureQueue { - pub sender: UnboundedSender, - pub queue: RefCell>>>, -} - -impl FutureQueue { - pub fn new(sender: UnboundedSender) -> Self { - Self { - sender, - queue: Default::default(), - } - } - - pub fn spawn(&self, scope: ScopeId, fut: impl Future + 'static) -> TaskId { - let id = self.queue.borrow_mut().insert(Arc::new(fut)); - - TaskId { id, scope } - } - - pub fn remove(&self, id: TaskId) { - todo!() - } -} - -/// A task's unique identifier. -/// -/// `TaskId` is a `usize` that is unique across the entire [`VirtualDom`] and across time. [`TaskID`]s will never be reused -/// once a Task has been completed. -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct TaskId { - /// The global ID of the task - pub id: usize, - - /// The original scope that this task was scheduled in - pub scope: ScopeId, -} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index ac9b4e10f..1754f2766 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -20,6 +20,7 @@ mod virtual_dom; pub(crate) mod innerlude { pub use crate::arena::*; pub use crate::dirty_scope::*; + pub use crate::error_boundary::*; pub use crate::events::*; pub use crate::fragment::*; pub use crate::lazynodes::*; @@ -78,8 +79,6 @@ pub use crate::innerlude::{ DynamicNode, Element, ElementId, - ElementRef, - EventPriority, Fragment, LazyNodes, Mutation, @@ -109,9 +108,9 @@ pub use crate::innerlude::{ /// This includes types like [`Scope`], [`Element`], and [`Component`]. pub mod prelude { pub use crate::innerlude::{ - fc_to_builder, Element, EventHandler, EventPriority, Fragment, LazyNodes, NodeFactory, - Properties, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, - TemplateNode, UiEvent, VNode, VirtualDom, + fc_to_builder, Element, EventHandler, Fragment, LazyNodes, NodeFactory, Properties, Scope, + ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, UiEvent, + VNode, VirtualDom, }; } diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 7e8e10f88..b1dff4011 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -1,6 +1,7 @@ use crate::{arena::ElementId, ScopeId}; #[derive(Debug)] +#[must_use = "not handling edits can lead to visual inconsistencies in UI"] pub struct Mutations<'a> { pub subtree: usize, pub template_mutations: Vec>, diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 1750d4276..40aefbee8 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,4 +1,5 @@ use crate::{any_props::AnyProps, arena::ElementId, Element, ScopeId, ScopeState, UiEvent}; +use bumpalo::boxed::Box as BumpBox; use std::{ any::{Any, TypeId}, cell::{Cell, RefCell}, @@ -12,9 +13,6 @@ pub type TemplateId = &'static str; /// static parts of the template. #[derive(Debug, Clone)] pub struct VNode<'a> { - // The ID assigned for the root of this template - pub node_id: Cell, - /// The key given to the root of this template. /// /// In fragments, this is the key of the first child. In other cases, it is the key of the root. @@ -40,7 +38,6 @@ pub struct VNode<'a> { impl<'a> VNode<'a> { pub fn empty() -> Element<'a> { Ok(VNode { - node_id: Cell::new(ElementId(0)), key: None, parent: None, root_ids: &[], @@ -156,26 +153,28 @@ pub enum AttributeValue<'a> { Float(f32), Int(i32), Bool(bool), - Listener(RefCell<&'a mut dyn FnMut(UiEvent)>), - Any(&'a dyn AnyValue), + Listener(RefCell) + 'a>>>), + Any(BumpBox<'a, dyn AnyValue>), None, } impl<'a> AttributeValue<'a> { pub fn new_listener( cx: &'a ScopeState, - mut f: impl FnMut(UiEvent) + 'a, + mut callback: impl FnMut(UiEvent) + 'a, ) -> AttributeValue<'a> { - AttributeValue::Listener(RefCell::new(cx.bump().alloc( - move |event: UiEvent| { + let boxed: BumpBox<'a, dyn FnMut(_) + 'a> = unsafe { + BumpBox::from_raw(cx.bump().alloc(move |event: UiEvent| { if let Ok(data) = event.data.downcast::() { - f(UiEvent { + callback(UiEvent { bubbles: event.bubbles, data, }) } - }, - ))) + })) + }; + + AttributeValue::Listener(RefCell::new(Some(boxed))) } } @@ -201,7 +200,7 @@ impl<'a> PartialEq for AttributeValue<'a> { (Self::Int(l0), Self::Int(r0)) => l0 == r0, (Self::Bool(l0), Self::Bool(r0)) => l0 == r0, (Self::Listener(_), Self::Listener(_)) => true, - (Self::Any(l0), Self::Any(r0)) => l0.any_cmp(*r0), + (Self::Any(l0), Self::Any(r0)) => l0.any_cmp(r0.as_ref()), _ => core::mem::discriminant(self) == core::mem::discriminant(other), } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 9259a9f56..2419c52a0 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -7,6 +7,7 @@ use crate::{ scheduler::RcWake, scopes::{ScopeId, ScopeState}, virtual_dom::VirtualDom, + AttributeValue, DynamicNode, VFragment, VNode, }; use futures_util::FutureExt; use std::{ @@ -17,13 +18,13 @@ use std::{ }; impl VirtualDom { - pub(super) fn new_scope(&mut self, props: *const dyn AnyProps<'static>) -> &mut ScopeState { + pub(super) fn new_scope(&mut self, props: Box>) -> &mut ScopeState { let parent = self.acquire_current_scope_raw(); let entry = self.scopes.vacant_entry(); let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) }; let id = ScopeId(entry.key()); - entry.insert(ScopeState { + entry.insert(Box::new(ScopeState { parent, id, height, @@ -38,19 +39,64 @@ impl VirtualDom { hook_idx: Default::default(), shared_contexts: Default::default(), tasks: self.scheduler.clone(), - }) + })) } fn acquire_current_scope_raw(&mut self) -> Option<*mut ScopeState> { self.scope_stack .last() .copied() - .and_then(|id| self.scopes.get_mut(id.0).map(|f| f as *mut ScopeState)) + .and_then(|id| self.scopes.get_mut(id.0).map(|f| f.as_mut() as *mut _)) + } + + pub fn ensure_drop_safety(&self, scope: ScopeId) { + let scope = &self.scopes[scope.0]; + let node = unsafe { scope.previous_frame().try_load_node() }; + + // And now we want to make sure the previous frame has dropped anything that borrows self + if let Some(RenderReturn::Sync(Ok(node))) = node { + self.ensure_drop_safety_inner(node); + } + } + + fn ensure_drop_safety_inner(&self, node: &VNode) { + for attr in node.dynamic_attrs { + if let AttributeValue::Listener(l) = &attr.value { + l.borrow_mut().take(); + } + } + + for child in node.dynamic_nodes { + match child { + DynamicNode::Component(c) => { + // Only descend if the props are borrowed + if !c.static_props { + self.ensure_drop_safety(c.scope.get().unwrap()); + c.props.set(None); + } + } + DynamicNode::Fragment(VFragment::NonEmpty(f)) => { + for node in *f { + self.ensure_drop_safety_inner(node); + } + } + _ => {} + } + } } pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn { + // Cycle to the next frame and then reset it + // This breaks any latent references, invalidating every pointer referencing into it. + // Remove all the outdated listeners + self.ensure_drop_safety(scope_id); + let mut new_nodes = unsafe { - let scope = &mut self.scopes[scope_id.0]; + let scope = self.scopes[scope_id.0].as_mut(); + + scope.previous_frame_mut().bump.reset(); + + // Make sure to reset the hook counter so we give out hooks in the right order scope.hook_idx.set(0); // safety: due to how we traverse the tree, we know that the scope is not currently aliased @@ -113,7 +159,7 @@ impl VirtualDom { let frame = scope.previous_frame(); // set the new head of the bump frame - let alloced = frame.bump.alloc(new_nodes); + let alloced = &*frame.bump.alloc(new_nodes); frame.node.set(alloced); // And move the render generation forward by one diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 0fdccd820..946ae90dc 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -82,12 +82,12 @@ pub struct ScopeState { pub(crate) tasks: Rc, pub(crate) spawned_tasks: HashSet, - pub(crate) props: *const dyn AnyProps<'static>, + pub(crate) props: Box>, pub(crate) placeholder: Cell>, } impl ScopeState { - pub fn current_frame(&self) -> &BumpFrame { + pub(crate) fn current_frame(&self) -> &BumpFrame { match self.render_cnt.get() % 2 { 0 => &self.node_arena_1, 1 => &self.node_arena_2, @@ -95,7 +95,7 @@ impl ScopeState { } } - pub fn previous_frame(&self) -> &BumpFrame { + pub(crate) fn previous_frame(&self) -> &BumpFrame { match self.render_cnt.get() % 2 { 1 => &self.node_arena_1, 0 => &self.node_arena_2, @@ -103,6 +103,14 @@ impl ScopeState { } } + pub(crate) fn previous_frame_mut(&mut self) -> &mut BumpFrame { + match self.render_cnt.get() % 2 { + 1 => &mut self.node_arena_1, + 0 => &mut self.node_arena_2, + _ => unreachable!(), + } + } + /// Get the current render since the inception of this component /// /// This can be used as a helpful diagnostic when debugging hooks/renders, etc @@ -117,7 +125,8 @@ impl ScopeState { /// /// If you need to allocate items that need to be dropped, use bumpalo's box. pub fn bump(&self) -> &Bump { - &self.current_frame().bump + // note that this is actually the previous frame since we use that as scratch space while the component is rendering + &self.previous_frame().bump } /// Get a handle to the currently active head node arena for this Scope diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 3faf732d9..374559e8f 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -6,12 +6,12 @@ use crate::{ any_props::VProps, arena::{ElementId, ElementRef}, factory::RenderReturn, - innerlude::{DirtyScope, ElementPath, Mutations, Scheduler, SchedulerMsg}, + innerlude::{DirtyScope, Mutations, Scheduler, SchedulerMsg, ErrorBoundary}, mutations::Mutation, nodes::{Template, TemplateId}, scheduler::{SuspenseBoundary, SuspenseId}, scopes::{ScopeId, ScopeState}, - AttributeValue, Element, EventPriority, Scope, SuspenseContext, UiEvent, + AttributeValue, Element, Scope, SuspenseContext, UiEvent, }; use futures_util::{pin_mut, StreamExt}; use slab::Slab; @@ -149,7 +149,7 @@ use std::{ /// ``` pub struct VirtualDom { pub(crate) templates: HashMap>, - pub(crate) scopes: Slab, + pub(crate) scopes: Slab>, pub(crate) dirty_scopes: BTreeSet, pub(crate) scheduler: Rc, @@ -224,10 +224,7 @@ impl VirtualDom { /// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" }); /// let mutations = dom.rebuild(); /// ``` - pub fn new_with_props

(root: fn(Scope

) -> Element, root_props: P) -> Self - where - P: 'static, - { + pub fn new_with_props(root: fn(Scope

) -> Element, root_props: P) -> Self { let (tx, rx) = futures_channel::mpsc::unbounded(); let mut dom = Self { rx, @@ -242,11 +239,11 @@ impl VirtualDom { mutations: Mutations::new(), }; - let root = dom.new_scope(Box::leak(Box::new(VProps::new( + let root = dom.new_scope(Box::new(VProps::new( root, |_, _| unreachable!(), root_props, - )))); + ))); // The root component is always a suspense boundary for any async children // This could be unexpected, so we might rethink this behavior later @@ -254,6 +251,9 @@ impl VirtualDom { // We *could* just panic if the suspense boundary is not found root.provide_context(Rc::new(SuspenseBoundary::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)))); + // the root element is always given element ID 0 since it's the container for the entire tree dom.elements.insert(ElementRef::null()); @@ -264,7 +264,7 @@ impl VirtualDom { /// /// This is useful for inserting or removing contexts from a scope, or rendering out its root node pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> { - self.scopes.get(id.0) + self.scopes.get(id.0).map(|f| f.as_ref()) } /// Get the single scope at the top of the VirtualDom tree that will always be around @@ -285,7 +285,7 @@ impl VirtualDom { /// Manually mark a scope as requiring a re-render /// /// Whenever the VirtualDom "works", it will re-render this scope - pub fn mark_dirty_scope(&mut self, id: ScopeId) { + pub fn mark_dirty(&mut self, id: ScopeId) { let height = self.scopes[id.0].height; self.dirty_scopes.insert(DirtyScope { height, id }); } @@ -329,7 +329,6 @@ impl VirtualDom { data: Rc, element: ElementId, bubbles: bool, - _priority: EventPriority, ) { /* ------------------------ @@ -393,7 +392,10 @@ impl VirtualDom { // We check the bubble state between each call to see if the event has been stopped from bubbling for listener in listeners.drain(..).rev() { if let AttributeValue::Listener(listener) = listener { - listener.borrow_mut()(uievent.clone()); + if let Some(cb) = listener.borrow_mut().as_deref_mut() { + cb(uievent.clone()); + } + if !uievent.bubbles.get() { return; } @@ -427,7 +429,7 @@ impl VirtualDom { match some_msg.take() { // If a bunch of messages are ready in a sequence, try to pop them off synchronously Some(msg) => match msg { - SchedulerMsg::Immediate(id) => self.mark_dirty_scope(id), + SchedulerMsg::Immediate(id) => self.mark_dirty(id), SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task), SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id), }, @@ -479,7 +481,6 @@ impl VirtualDom { /// /// apply_edits(edits); /// ``` - #[must_use] pub fn rebuild<'a>(&'a mut self) -> Mutations<'a> { match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } { // Rebuilding implies we append the created elements to the root @@ -497,7 +498,6 @@ impl VirtualDom { /// Render whatever the VirtualDom has ready as fast as possible without requiring an executor to progress /// suspended subtrees. - #[must_use] pub fn render_immediate(&mut self) -> Mutations { // Build a waker that won't wake up since our deadline is already expired when it's polled let waker = futures_util::task::noop_waker(); @@ -612,6 +612,6 @@ impl VirtualDom { impl Drop for VirtualDom { fn drop(&mut self) { - // self.drop_scope(ScopeId(0)); + self.drop_scope(ScopeId(0)); } } diff --git a/packages/core/tests/attr_cleanup.rs b/packages/core/tests/attr_cleanup.rs index 2f70ca527..99ce9a01b 100644 --- a/packages/core/tests/attr_cleanup.rs +++ b/packages/core/tests/attr_cleanup.rs @@ -30,7 +30,7 @@ fn attrs_cycle() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -42,7 +42,7 @@ fn attrs_cycle() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -51,7 +51,7 @@ fn attrs_cycle() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -64,7 +64,7 @@ fn attrs_cycle() { ); // we take the node taken by attributes since we reused it - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ diff --git a/packages/core/tests/bubble_error.rs b/packages/core/tests/bubble_error.rs index 310f5118d..6dbd0f3f7 100644 --- a/packages/core/tests/bubble_error.rs +++ b/packages/core/tests/bubble_error.rs @@ -22,9 +22,7 @@ fn it_goes() { let edits = dom.rebuild().santize(); - dbg!(edits); - - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); dom.render_immediate(); } diff --git a/packages/core/tests/context_api.rs b/packages/core/tests/context_api.rs index 71069d2fc..2ec1ea18b 100644 --- a/packages/core/tests/context_api.rs +++ b/packages/core/tests/context_api.rs @@ -27,22 +27,22 @@ fn state_shares() { ] ); - dom.mark_dirty_scope(ScopeId(0)); - dom.render_immediate(); + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); assert_eq!(dom.base_scope().consume_context::().unwrap(), 1); - dom.mark_dirty_scope(ScopeId(0)); - dom.render_immediate(); + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); assert_eq!(dom.base_scope().consume_context::().unwrap(), 2); - dom.mark_dirty_scope(ScopeId(2)); + dom.mark_dirty(ScopeId(2)); assert_eq!( dom.render_immediate().santize().edits, [SetText { value: "Value is 2", id: ElementId(1,) },] ); - dom.mark_dirty_scope(ScopeId(0)); - dom.mark_dirty_scope(ScopeId(2)); + dom.mark_dirty(ScopeId(0)); + dom.mark_dirty(ScopeId(2)); let edits = dom.render_immediate(); assert_eq!( edits.santize().edits, diff --git a/packages/core/tests/create_lists.rs b/packages/core/tests/create_lists.rs index 30d6abc2f..27540d9f0 100644 --- a/packages/core/tests/create_lists.rs +++ b/packages/core/tests/create_lists.rs @@ -43,7 +43,7 @@ fn list_renders() { CreateStaticText { value: "hello world! " }, AppendChildren { m: 1 }, CreateElement { name: "p" }, - CreateStaticText { value: "d" }, + CreateTextPlaceholder, AppendChildren { m: 1 }, AppendChildren { m: 2 }, SaveTemplate { name: "template", m: 1 } diff --git a/packages/core/tests/cycle.rs b/packages/core/tests/cycle.rs index 803edede7..72e1a028d 100644 --- a/packages/core/tests/cycle.rs +++ b/packages/core/tests/cycle.rs @@ -21,7 +21,7 @@ fn cycling_elements() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().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_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -40,7 +40,7 @@ fn cycling_elements() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ diff --git a/packages/core/tests/diff_component.rs b/packages/core/tests/diff_component.rs new file mode 100644 index 000000000..6e72f04cd --- /dev/null +++ b/packages/core/tests/diff_component.rs @@ -0,0 +1,102 @@ +use dioxus::core::{ElementId, Mutation::*}; +use dioxus::prelude::*; + +/// When returning sets of components, we do a light diff of the contents to preserve some react-like functionality +/// +/// This means that nav_bar should never get re-created and that we should only be swapping out +/// different pointers +#[test] +fn component_swap() { + fn app(cx: Scope) -> Element { + let render_phase = cx.use_hook(|| 0); + + *render_phase += 1; + + cx.render(match *render_phase { + 0 => rsx! { + nav_bar {} + dash_board {} + }, + 1 => rsx! { + nav_bar {} + dash_results {} + }, + 2 => rsx! { + nav_bar {} + dash_board {} + }, + 3 => rsx! { + nav_bar {} + dash_results {} + }, + 4 => rsx! { + nav_bar {} + dash_board {} + }, + _ => rsx!("blah"), + }) + } + + fn nav_bar(cx: Scope) -> Element { + cx.render(rsx! { + h1 { + "NavBar" + (0..3).map(|_| rsx!(nav_link {})) + } + }) + } + + fn nav_link(cx: Scope) -> Element { + cx.render(rsx!( h1 { "nav_link" } )) + } + + fn dash_board(cx: Scope) -> Element { + cx.render(rsx!( div { "dashboard" } )) + } + + fn dash_results(cx: Scope) -> Element { + cx.render(rsx!( div { "results" } )) + } + + let mut dom = VirtualDom::new(app); + let edits = dom.rebuild().santize(); + assert_eq!( + edits.edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, + LoadTemplate { name: "template", index: 0, id: ElementId(2) }, + LoadTemplate { name: "template", index: 0, id: ElementId(3) }, + LoadTemplate { name: "template", index: 0, id: ElementId(4) }, + ReplacePlaceholder { path: &[1], m: 3 }, + LoadTemplate { name: "template", index: 0, id: ElementId(5) }, + AppendChildren { m: 2 } + ] + ); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(6) }, + ReplaceWith { id: ElementId(5), m: 1 } + ] + ); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(5) }, + ReplaceWith { id: ElementId(6), m: 1 } + ] + ); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + LoadTemplate { name: "template", index: 0, id: ElementId(6) }, + ReplaceWith { id: ElementId(5), m: 1 } + ] + ); +} diff --git a/packages/core/tests/diff_element.rs b/packages/core/tests/diff_element.rs index 5dff0fbc8..3ab9b284a 100644 --- a/packages/core/tests/diff_element.rs +++ b/packages/core/tests/diff_element.rs @@ -10,21 +10,21 @@ fn text_diff() { } let mut vdom = VirtualDom::new(app); - vdom.rebuild(); + _ = vdom.rebuild(); - vdom.mark_dirty_scope(ScopeId(0)); + vdom.mark_dirty(ScopeId(0)); assert_eq!( vdom.render_immediate().edits, [SetText { value: "hello 1", id: ElementId(2) }] ); - vdom.mark_dirty_scope(ScopeId(0)); + vdom.mark_dirty(ScopeId(0)); assert_eq!( vdom.render_immediate().edits, [SetText { value: "hello 2", id: ElementId(2) }] ); - vdom.mark_dirty_scope(ScopeId(0)); + vdom.mark_dirty(ScopeId(0)); assert_eq!( vdom.render_immediate().edits, [SetText { value: "hello 3", id: ElementId(2) }] @@ -44,9 +44,9 @@ fn element_swap() { } let mut vdom = VirtualDom::new(app); - vdom.rebuild(); + _ = vdom.rebuild(); - vdom.mark_dirty_scope(ScopeId(0)); + vdom.mark_dirty(ScopeId(0)); assert_eq!( vdom.render_immediate().santize().edits, [ @@ -55,7 +55,7 @@ fn element_swap() { ] ); - vdom.mark_dirty_scope(ScopeId(0)); + vdom.mark_dirty(ScopeId(0)); assert_eq!( vdom.render_immediate().santize().edits, [ @@ -64,7 +64,7 @@ fn element_swap() { ] ); - vdom.mark_dirty_scope(ScopeId(0)); + vdom.mark_dirty(ScopeId(0)); assert_eq!( vdom.render_immediate().santize().edits, [ @@ -73,7 +73,7 @@ fn element_swap() { ] ); - vdom.mark_dirty_scope(ScopeId(0)); + vdom.mark_dirty(ScopeId(0)); assert_eq!( vdom.render_immediate().santize().edits, [ diff --git a/packages/core/tests/diff_keyed_list.rs b/packages/core/tests/diff_keyed_list.rs index 521d9385a..6acbaebbb 100644 --- a/packages/core/tests/diff_keyed_list.rs +++ b/packages/core/tests/diff_keyed_list.rs @@ -1,5 +1,3 @@ -#![allow(unused, non_upper_case_globals)] - //! Diffing Tests //! //! These tests only verify that the diffing algorithm works properly for single components. @@ -39,7 +37,7 @@ fn keyed_diffing_out_of_order() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().edits, [ @@ -64,7 +62,7 @@ fn keyed_diffing_out_of_order_adds() { _ = dom.rebuild(); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().edits, [ @@ -90,7 +88,7 @@ fn keyed_diffing_out_of_order_adds_3() { _ = dom.rebuild(); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().edits, [ @@ -116,7 +114,7 @@ fn keyed_diffing_out_of_order_adds_4() { _ = dom.rebuild(); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().edits, [ @@ -142,7 +140,7 @@ fn keyed_diffing_out_of_order_adds_5() { _ = dom.rebuild(); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().edits, [ @@ -167,7 +165,7 @@ fn keyed_diffing_additions() { _ = dom.rebuild(); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -192,7 +190,7 @@ fn keyed_diffing_additions_and_moves_on_ends() { _ = dom.rebuild(); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -222,7 +220,7 @@ fn keyed_diffing_additions_and_moves_in_middle() { _ = dom.rebuild(); // LIS: 4, 5, 6 - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -256,7 +254,7 @@ fn controlled_keyed_diffing_out_of_order() { _ = dom.rebuild(); // LIS: 5, 6 - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -289,7 +287,7 @@ fn controlled_keyed_diffing_out_of_order_max_test() { _ = dom.rebuild(); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -318,7 +316,7 @@ fn remove_list() { _ = dom.rebuild(); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -328,3 +326,31 @@ fn remove_list() { ] ); } + +#[test] +fn no_common_keys() { + let mut dom = VirtualDom::new(|cx| { + let order: &[_] = match cx.generation() % 2 { + 0 => &[1, 2, 3], + 1 => &[4, 5, 6], + _ => unreachable!(), + }; + + cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" })))) + }); + + _ = dom.rebuild(); + + dom.mark_dirty(ScopeId(0)); + assert_eq!( + dom.render_immediate().santize().edits, + [ + Remove { id: ElementId(2) }, + Remove { id: ElementId(3) }, + LoadTemplate { name: "template", index: 0, id: ElementId(3) }, + LoadTemplate { name: "template", index: 0, id: ElementId(2) }, + LoadTemplate { name: "template", index: 0, id: ElementId(4) }, + ReplaceWith { id: ElementId(1), m: 3 } + ] + ); +} diff --git a/packages/core/tests/diff_unkeyed_list.rs b/packages/core/tests/diff_unkeyed_list.rs index 96de44cc3..5252d1ece 100644 --- a/packages/core/tests/diff_unkeyed_list.rs +++ b/packages/core/tests/diff_unkeyed_list.rs @@ -26,7 +26,7 @@ fn list_creates_one_by_one() { ); // Rendering the first item should replace the placeholder with an element - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -37,7 +37,7 @@ fn list_creates_one_by_one() { ); // Rendering the next item should insert after the previous - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -48,7 +48,7 @@ fn list_creates_one_by_one() { ); // ... and again! - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -59,7 +59,7 @@ fn list_creates_one_by_one() { ); // once more - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -106,14 +106,14 @@ fn removes_one_by_one() { // Remove div(3) // Rendering the first item should replace the placeholder with an element - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [Remove { id: ElementId(6) }] ); // Remove div(2) - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [Remove { id: ElementId(4) }] @@ -121,7 +121,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_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -132,7 +132,7 @@ 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_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -169,7 +169,7 @@ fn list_shrink_multiroot() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -181,7 +181,7 @@ fn list_shrink_multiroot() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -193,7 +193,7 @@ fn list_shrink_multiroot() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -248,19 +248,19 @@ fn removes_one_by_one_multiroot() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [Remove { id: ElementId(10) }, Remove { id: ElementId(12) }] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [Remove { id: ElementId(6) }, Remove { id: ElementId(8) }] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); assert_eq!( dom.render_immediate().santize().edits, [ @@ -324,7 +324,7 @@ fn remove_many() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); let edits = dom.render_immediate().santize(); assert_eq!( edits.edits, @@ -335,7 +335,7 @@ fn remove_many() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); let edits = dom.render_immediate().santize(); assert_eq!( edits.edits, @@ -352,7 +352,7 @@ fn remove_many() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); let edits = dom.render_immediate().santize(); assert_eq!( edits.edits, @@ -366,7 +366,7 @@ fn remove_many() { ] ); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); let edits = dom.render_immediate().santize(); assert_eq!( edits.edits, diff --git a/packages/core/tests/hotreloading.rs b/packages/core/tests/hotreloading.rs new file mode 100644 index 000000000..8e9f3400b --- /dev/null +++ b/packages/core/tests/hotreloading.rs @@ -0,0 +1 @@ +//! It should be possible to swap out templates at runtime, enabling hotreloading diff --git a/packages/core/tests/lifecycle.rs b/packages/core/tests/lifecycle.rs index 06baf32a0..ab7be8793 100644 --- a/packages/core/tests/lifecycle.rs +++ b/packages/core/tests/lifecycle.rs @@ -44,10 +44,7 @@ fn events_generate() { match *count { 0 => cx.render(rsx! { - div { - onclick: move |_| { - *count += 1 - }, + div { onclick: move |_| *count += 1, div { "nested" } "Click me!" } @@ -59,15 +56,9 @@ fn events_generate() { let mut dom = VirtualDom::new(app); _ = dom.rebuild(); - dom.handle_event( - "click", - Rc::new(MouseData::default()), - ElementId(1), - true, - EventPriority::Immediate, - ); + dom.handle_event("click", Rc::new(MouseData::default()), ElementId(1), true); - dom.mark_dirty_scope(ScopeId(0)); + dom.mark_dirty(ScopeId(0)); let edits = dom.render_immediate(); assert_eq!( @@ -175,95 +166,3 @@ fn events_generate() { // ] // ); // } - -// #[test] -// fn component_swap() { -// fn app(cx: Scope) -> Element { -// let render_phase = cx.use_hook(|| 0); -// *render_phase += 1; - -// cx.render(match *render_phase { -// 0 => rsx_without_templates!( -// div { -// NavBar {} -// Dashboard {} -// } -// ), -// 1 => rsx_without_templates!( -// div { -// NavBar {} -// Results {} -// } -// ), -// 2 => rsx_without_templates!( -// div { -// NavBar {} -// Dashboard {} -// } -// ), -// 3 => rsx_without_templates!( -// div { -// NavBar {} -// Results {} -// } -// ), -// 4 => rsx_without_templates!( -// div { -// NavBar {} -// Dashboard {} -// } -// ), -// _ => rsx_without_templates!("blah"), -// }) -// }; - -// static NavBar: Component = |cx| { -// println!("running navbar"); -// cx.render(rsx_without_templates! { -// h1 { -// "NavBar" -// {(0..3).map(|f| rsx_without_templates!(NavLink {}))} -// } -// }) -// }; - -// static NavLink: Component = |cx| { -// println!("running navlink"); -// cx.render(rsx_without_templates! { -// h1 { -// "NavLink" -// } -// }) -// }; - -// static Dashboard: Component = |cx| { -// println!("running dashboard"); -// cx.render(rsx_without_templates! { -// div { -// "dashboard" -// } -// }) -// }; - -// static Results: Component = |cx| { -// println!("running results"); -// cx.render(rsx_without_templates! { -// div { -// "results" -// } -// }) -// }; - -// let mut dom = VirtualDom::new(app); -// let edits = dom.rebuild(); -// dbg!(&edits); - -// let edits = dom.work_with_deadline(|| false); -// dbg!(&edits); -// let edits = dom.work_with_deadline(|| false); -// dbg!(&edits); -// let edits = dom.work_with_deadline(|| false); -// dbg!(&edits); -// let edits = dom.work_with_deadline(|| false); -// dbg!(&edits); -// } diff --git a/packages/core/tests/miri_simple.rs b/packages/core/tests/miri_simple.rs new file mode 100644 index 000000000..19d3a44a0 --- /dev/null +++ b/packages/core/tests/miri_simple.rs @@ -0,0 +1,127 @@ +use dioxus::prelude::*; + +#[test] +fn app_drops() { + fn app(cx: Scope) -> Element { + cx.render(rsx! { + div {} + }) + } + + let mut dom = VirtualDom::new(app); + + _ = dom.rebuild(); + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); +} + +#[test] +fn hooks_drop() { + fn app(cx: Scope) -> Element { + cx.use_hook(|| String::from("asd")); + cx.use_hook(|| String::from("asd")); + cx.use_hook(|| String::from("asd")); + cx.use_hook(|| String::from("asd")); + + cx.render(rsx! { + div {} + }) + } + + let mut dom = VirtualDom::new(app); + + _ = dom.rebuild(); + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); +} + +#[test] +fn contexts_drop() { + fn app(cx: Scope) -> Element { + cx.provide_context(String::from("asd")); + + cx.render(rsx! { + div { + child_comp {} + } + }) + } + + fn child_comp(cx: Scope) -> Element { + let el = cx.consume_context::().unwrap(); + + cx.render(rsx! { + div { "hello {el}" } + }) + } + + let mut dom = VirtualDom::new(app); + + _ = dom.rebuild(); + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); +} + +#[test] +fn tasks_drop() { + fn app(cx: Scope) -> Element { + cx.spawn(async { + tokio::time::sleep(std::time::Duration::from_millis(100000)).await; + }); + + cx.render(rsx! { + div { } + }) + } + + let mut dom = VirtualDom::new(app); + + _ = dom.rebuild(); + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); +} + +#[test] +fn root_props_drop() { + struct RootProps(String); + + let mut dom = VirtualDom::new_with_props( + |cx| cx.render(rsx!( div { "{cx.props.0}" } )), + RootProps("asdasd".to_string()), + ); + + _ = dom.rebuild(); + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); +} + +#[test] +fn diffing_drops_old() { + fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + match cx.generation() % 2 { + 0 => rsx!( child_comp1 { name: "asdasd".to_string() }), + 1 => rsx!( child_comp2 { name: "asdasd".to_string() }), + _ => todo!() + } + } + }) + } + + #[inline_props] + fn child_comp1(cx: Scope, name: String) -> Element { + cx.render(rsx! { "Hello {name}" }) + } + + #[inline_props] + fn child_comp2(cx: Scope, name: String) -> Element { + cx.render(rsx! { "Goodbye {name}" }) + } + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + dom.mark_dirty(ScopeId(0)); + + _ = dom.render_immediate(); +} diff --git a/packages/core/tests/miri_stress.rs b/packages/core/tests/miri_stress.rs index 20a29b30c..3ebd5c504 100644 --- a/packages/core/tests/miri_stress.rs +++ b/packages/core/tests/miri_stress.rs @@ -1,32 +1,28 @@ #![allow(non_snake_case)] -/* -Stress Miri as much as possible. -Prove that we don't leak memory and that our methods are safe. - -Specifically: -- [ ] VirtualDom drops memory safely -- [ ] Borrowed components don't expose invalid pointers -- [ ] Async isn't busted -*/ +use std::rc::Rc; use dioxus::prelude::*; -/// This test ensures that if a component aborts early, it is replaced with a placeholder. -/// In debug, this should also toss a warning. +/// This test checks that we should release all memory used by the virtualdom when it exits. +/// +/// When miri runs, it'll let us know if we leaked or aliased. #[test] -#[ignore] fn test_memory_leak() { fn app(cx: Scope) -> Element { - let val = cx.use_hook(|| 0); + let val = cx.generation(); - *val += 1; + cx.spawn(async { + tokio::time::sleep(std::time::Duration::from_millis(100000)).await; + }); - if *val == 2 || *val == 4 { + if val == 2 || val == 4 { return cx.render(rsx!(())); } - let name = cx.use_hook(|| String::from("asd")); + let name = cx.use_hook(|| String::from("numbers: ")); + + name.push_str("123 "); cx.render(rsx!( div { "Hello, world!" } @@ -36,24 +32,26 @@ fn test_memory_leak() { Child {} Child {} Child {} - BorrowedChild { na: name } - BorrowedChild { na: name } - BorrowedChild { na: name } - BorrowedChild { na: name } - BorrowedChild { na: name } + BorrowedChild { name: name } + BorrowedChild { name: name } + BorrowedChild { name: name } + BorrowedChild { name: name } + BorrowedChild { name: name } )) } #[derive(Props)] struct BorrowedProps<'a> { - na: &'a str, + name: &'a str, } fn BorrowedChild<'a>(cx: Scope<'a, BorrowedProps<'a>>) -> Element { - render!(div { - "goodbye {cx.props.na}" - Child {} - Child {} + cx.render(rsx! { + div { + "goodbye {cx.props.name}" + Child {} + Child {} + } }) } @@ -63,24 +61,21 @@ fn test_memory_leak() { let mut dom = VirtualDom::new(app); - dom.rebuild(); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); + _ = dom.rebuild(); + + for _ in 0..5 { + dom.mark_dirty(ScopeId(0)); + _ = dom.render_immediate(); + } } #[test] fn memo_works_properly() { fn app(cx: Scope) -> Element { - let val = cx.use_hook(|| 0); + let val = cx.generation(); - *val += 1; - - if *val == 2 || *val == 4 { - return None; + if val == 2 || val == 4 { + return cx.render(rsx!(())); } let name = cx.use_hook(|| String::from("asd")); @@ -100,309 +95,257 @@ fn memo_works_properly() { render!(div { "goodbye world" }) } - let mut dom = new_dom(app, ()); + let mut dom = VirtualDom::new(app); dom.rebuild(); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); - dom.hard_diff(ScopeId(0)); -} - -#[test] -fn free_works_on_root_props() { - fn app(cx: Scope) -> Element { - cx.render(rsx! { - Child { a: "alpha"} - Child { a: "beta"} - Child { a: "gamma"} - Child { a: "delta"} - }) - } - - #[derive(Props, PartialEq)] - struct ChildProps { - a: &'static str, - } - - fn Child(cx: Scope) -> Element { - render!("child {cx.props.a}") - } - - struct Custom { - val: String, - } - - impl Drop for Custom { - fn drop(&mut self) { - dbg!("dropped! {}", &self.val); - } - } - - let mut dom = new_dom(app, Custom { val: String::from("asd") }); - dom.rebuild(); -} - -#[test] -fn free_works_on_borrowed() { - fn app(cx: Scope) -> Element { - cx.render(rsx! { - Child { a: "alpha", b: "asd".to_string() } - }) - } - #[derive(Props)] - struct ChildProps<'a> { - a: &'a str, - b: String, - } - - fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element { - dbg!("rendering child"); - render!("child {cx.props.a}, {cx.props.b}") - } - - impl Drop for ChildProps<'_> { - fn drop(&mut self) { - dbg!("dropped child!"); - } - } - - let mut dom = new_dom(app, ()); - let _ = dom.rebuild(); + todo!() + // dom.hard_diff(ScopeId(0)); + // dom.hard_diff(ScopeId(0)); + // dom.hard_diff(ScopeId(0)); + // dom.hard_diff(ScopeId(0)); + // dom.hard_diff(ScopeId(0)); + // dom.hard_diff(ScopeId(0)); + // dom.hard_diff(ScopeId(0)); } #[test] fn free_works_on_root_hooks() { /* - On Drop, scopearena drops all the hook contents. + On Drop, scopearena drops all the hook contents. and props */ - - struct Droppable(T); - impl Drop for Droppable { - fn drop(&mut self) { - dbg!("dropping droppable"); - } + #[derive(PartialEq, Clone, Props)] + struct AppProps { + inner: Rc, } - fn app(cx: Scope) -> Element { - let name = cx.use_hook(|| Droppable(String::from("asd"))); - render!(div { "{name.0}" }) + fn app(cx: Scope) -> Element { + let name: &AppProps = cx.use_hook(|| cx.props.clone()); + render!(child_component { inner: name.inner.clone() }) } - let mut dom = new_dom(app, ()); - let _ = dom.rebuild(); -} - -#[test] -fn old_props_arent_stale() { - fn app(cx: Scope) -> Element { - dbg!("rendering parent"); - let cnt = cx.use_hook(|| 0); - *cnt += 1; - - if *cnt == 1 { - render!(div { Child { a: "abcdef".to_string() } }) - } else { - render!(div { Child { a: "abcdef".to_string() } }) - } + fn child_component(cx: Scope) -> Element { + render!(div { "{cx.props.inner}" }) } - #[derive(Props, PartialEq)] - struct ChildProps { - a: String, - } - fn Child(cx: Scope) -> Element { - dbg!("rendering child", &cx.props.a); - render!(div { "child {cx.props.a}" }) - } - - let mut dom = new_dom(app, ()); + let ptr = Rc::new("asdasd".to_string()); + let mut dom = VirtualDom::new_with_props(app, AppProps { inner: ptr.clone() }); let _ = dom.rebuild(); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); + // ptr gets cloned into props and then into the hook + assert_eq!(Rc::strong_count(&ptr), 4); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); + drop(dom); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); - - dbg!("forcing update to child"); - - dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); - dom.work_with_deadline(|| false); - - dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); - dom.work_with_deadline(|| false); - - dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); - dom.work_with_deadline(|| false); + assert_eq!(Rc::strong_count(&ptr), 1); } -#[test] -fn basic() { - fn app(cx: Scope) -> Element { - render!(div { - Child { a: "abcdef".to_string() } - }) - } +// #[test] +// fn old_props_arent_stale() { +// fn app(cx: Scope) -> Element { +// dbg!("rendering parent"); +// let cnt = cx.use_hook(|| 0); +// *cnt += 1; - #[derive(Props, PartialEq)] - struct ChildProps { - a: String, - } +// if *cnt == 1 { +// render!(div { Child { a: "abcdef".to_string() } }) +// } else { +// render!(div { Child { a: "abcdef".to_string() } }) +// } +// } - fn Child(cx: Scope) -> Element { - dbg!("rendering child", &cx.props.a); - render!(div { "child {cx.props.a}" }) - } +// #[derive(Props, PartialEq)] +// struct ChildProps { +// a: String, +// } +// fn Child(cx: Scope) -> Element { +// dbg!("rendering child", &cx.props.a); +// render!(div { "child {cx.props.a}" }) +// } - let mut dom = new_dom(app, ()); - let _ = dom.rebuild(); +// let mut dom = new_dom(app, ()); +// let _ = dom.rebuild(); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); -} +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); -#[test] -fn leak_thru_children() { - fn app(cx: Scope) -> Element { - cx.render(rsx! { - Child { - name: "asd".to_string(), - } - }); - cx.render(rsx! { - div {} - }) - } +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); - #[inline_props] - fn Child(cx: Scope, name: String) -> Element { - render!(div { "child {name}" }) - } +// dbg!("forcing update to child"); - let mut dom = new_dom(app, ()); - let _ = dom.rebuild(); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); +// dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); +// dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); -} +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); +// dom.work_with_deadline(|| false); +// } -#[test] -fn test_pass_thru() { - #[inline_props] - fn NavContainer<'a>(cx: Scope, children: Element<'a>) -> Element { - cx.render(rsx! { - header { - nav { children } - } - }) - } +// #[test] +// fn basic() { +// fn app(cx: Scope) -> Element { +// render!(div { +// Child { a: "abcdef".to_string() } +// }) +// } - fn NavMenu(cx: Scope) -> Element { - render!( NavBrand {} - div { - NavStart {} - NavEnd {} - } - ) - } +// #[derive(Props, PartialEq)] +// struct ChildProps { +// a: String, +// } - fn NavBrand(cx: Scope) -> Element { - render!(div {}) - } +// fn Child(cx: Scope) -> Element { +// dbg!("rendering child", &cx.props.a); +// render!(div { "child {cx.props.a}" }) +// } - fn NavStart(cx: Scope) -> Element { - render!(div {}) - } +// let mut dom = new_dom(app, ()); +// let _ = dom.rebuild(); - fn NavEnd(cx: Scope) -> Element { - render!(div {}) - } +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); - #[inline_props] - fn MainContainer<'a>( - cx: Scope, - nav: Element<'a>, - body: Element<'a>, - footer: Element<'a>, - ) -> Element { - cx.render(rsx! { - div { - class: "columns is-mobile", - div { - class: "column is-full", - nav, - body, - footer, - } - } - }) - } +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); +// } - fn app(cx: Scope) -> Element { - let nav = cx.render(rsx! { - NavContainer { - NavMenu {} - } - }); - let body = cx.render(rsx! { - div {} - }); - let footer = cx.render(rsx! { - div {} - }); +// #[test] +// fn leak_thru_children() { +// fn app(cx: Scope) -> Element { +// cx.render(rsx! { +// Child { +// name: "asd".to_string(), +// } +// }); +// cx.render(rsx! { +// div {} +// }) +// } - cx.render(rsx! { - MainContainer { - nav: nav, - body: body, - footer: footer, - } - }) - } +// #[inline_props] +// fn Child(cx: Scope, name: String) -> Element { +// render!(div { "child {name}" }) +// } - let mut dom = new_dom(app, ()); - let _ = dom.rebuild(); +// let mut dom = new_dom(app, ()); +// let _ = dom.rebuild(); - for _ in 0..40 { - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); - dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); - dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); +// } - dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); - dom.work_with_deadline(|| false); +// #[test] +// fn test_pass_thru() { +// #[inline_props] +// fn NavContainer<'a>(cx: Scope, children: Element<'a>) -> Element { +// cx.render(rsx! { +// header { +// nav { children } +// } +// }) +// } - dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); - dom.work_with_deadline(|| false); - dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); - dom.work_with_deadline(|| false); - } -} +// fn NavMenu(cx: Scope) -> Element { +// render!( NavBrand {} +// div { +// NavStart {} +// NavEnd {} +// } +// ) +// } + +// fn NavBrand(cx: Scope) -> Element { +// render!(div {}) +// } + +// fn NavStart(cx: Scope) -> Element { +// render!(div {}) +// } + +// fn NavEnd(cx: Scope) -> Element { +// render!(div {}) +// } + +// #[inline_props] +// fn MainContainer<'a>( +// cx: Scope, +// nav: Element<'a>, +// body: Element<'a>, +// footer: Element<'a>, +// ) -> Element { +// cx.render(rsx! { +// div { +// class: "columns is-mobile", +// div { +// class: "column is-full", +// nav, +// body, +// footer, +// } +// } +// }) +// } + +// fn app(cx: Scope) -> Element { +// let nav = cx.render(rsx! { +// NavContainer { +// NavMenu {} +// } +// }); +// let body = cx.render(rsx! { +// div {} +// }); +// let footer = cx.render(rsx! { +// div {} +// }); + +// cx.render(rsx! { +// MainContainer { +// nav: nav, +// body: body, +// footer: footer, +// } +// }) +// } + +// let mut dom = new_dom(app, ()); +// let _ = dom.rebuild(); + +// for _ in 0..40 { +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(0))); +// dom.work_with_deadline(|| false); + +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); +// dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); +// dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(1))); +// dom.work_with_deadline(|| false); + +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); +// dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); +// dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(2))); +// dom.work_with_deadline(|| false); + +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); +// dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); +// dom.work_with_deadline(|| false); +// dom.handle_message(SchedulerMsg::Immediate(ScopeId(3))); +// dom.work_with_deadline(|| false); +// } +// } diff --git a/packages/core/tests/safety.rs b/packages/core/tests/safety.rs index 318e1088d..564f21eb6 100644 --- a/packages/core/tests/safety.rs +++ b/packages/core/tests/safety.rs @@ -13,10 +13,6 @@ fn root_node_isnt_null() { // We haven't built the tree, so trying to get out the root node should fail assert!(scope.try_root_node().is_none()); - // There should be no way to gain an invalid pointer - assert!(scope.current_frame().node.get().is_null()); - assert!(scope.previous_frame().node.get().is_null()); - // The height should be 0 assert_eq!(scope.height(), 0); diff --git a/packages/core/tests/suspense.rs b/packages/core/tests/suspense.rs index c0d89a10f..481ade2e3 100644 --- a/packages/core/tests/suspense.rs +++ b/packages/core/tests/suspense.rs @@ -11,7 +11,7 @@ async fn it_works() { let mutations = dom.rebuild().santize(); - // We should at least get the top-level template in + // We should at least get the top-level template in before pausing for the children assert_eq!( mutations.template_mutations, [ diff --git a/packages/core/tests/task.rs b/packages/core/tests/task.rs index be7c969d3..2704c5eb2 100644 --- a/packages/core/tests/task.rs +++ b/packages/core/tests/task.rs @@ -13,7 +13,7 @@ async fn it_works() { tokio::select! { _ = dom.wait_for_work() => {} - _ = tokio::time::sleep(Duration::from_millis(10)) => {} + _ = tokio::time::sleep(Duration::from_millis(500)) => {} }; // By the time the tasks are finished, we should've accumulated ticks from two tasks diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index f1fe0cafb..8d5a9da59 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -144,7 +144,6 @@ impl<'a> ToTokens for TemplateRenderer<'a> { attr_paths: &[ #(#attr_paths),* ], }; ::dioxus::core::VNode { - node_id: Default::default(), parent: None, key: #key_tokens, template: TEMPLATE,