ソースを参照

Merge pull request #671 from DioxusLabs/jk/suspense-rollover

feat: complete the rollover of aborted and async render states
Jon Kelley 2 年 前
コミット
641488e825

+ 11 - 0
examples/button.rs

@@ -0,0 +1,11 @@
+use dioxus::prelude::*;
+
+fn main() {
+    dioxus_desktop::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+    cx.render(rsx! {
+        button { "hello, desktop!" }
+    })
+}

+ 14 - 7
packages/core/src/any_props.rs

@@ -1,4 +1,4 @@
-use std::marker::PhantomData;
+use std::{marker::PhantomData, panic::AssertUnwindSafe};
 
 use crate::{
     innerlude::Scoped,
@@ -62,12 +62,19 @@ where
     }
 
     fn render(&'a self, cx: &'a ScopeState) -> RenderReturn<'a> {
-        let scope: &mut Scoped<P> = cx.bump().alloc(Scoped {
-            props: &self.props,
-            scope: cx,
-        });
+        let res = std::panic::catch_unwind(AssertUnwindSafe(move || {
+            // Call the render function directly
+            let scope: &mut Scoped<P> = cx.bump().alloc(Scoped {
+                props: &self.props,
+                scope: cx,
+            });
 
-        // Call the render function directly
-        (self.render_fn)(scope).into_return(cx)
+            (self.render_fn)(scope).into_return(cx)
+        }));
+
+        match res {
+            Ok(e) => e,
+            Err(_) => RenderReturn::default(),
+        }
     }
 }

+ 2 - 2
packages/core/src/arena.rs

@@ -81,12 +81,12 @@ impl VirtualDom {
         self.ensure_drop_safety(id);
 
         if let Some(root) = self.scopes[id.0].as_ref().try_root_node() {
-            if let RenderReturn::Sync(Some(node)) = unsafe { root.extend_lifetime_ref() } {
+            if let RenderReturn::Ready(node) = unsafe { root.extend_lifetime_ref() } {
                 self.drop_scope_inner(node)
             }
         }
         if let Some(root) = unsafe { self.scopes[id.0].as_ref().previous_frame().try_load_node() } {
-            if let RenderReturn::Sync(Some(node)) = unsafe { root.extend_lifetime_ref() } {
+            if let RenderReturn::Ready(node) = unsafe { root.extend_lifetime_ref() } {
                 self.drop_scope_inner(node)
             }
         }

+ 29 - 29
packages/core/src/create.rs

@@ -433,9 +433,9 @@ impl<'b> VirtualDom {
         use DynamicNode::*;
         match node {
             Text(text) => self.create_dynamic_text(template, text, idx),
-            Fragment(frag) => self.create_fragment(frag),
-            Placeholder(frag) => self.create_placeholder(frag, template, idx),
+            Placeholder(place) => self.create_placeholder(place, template, idx),
             Component(component) => self.create_component_node(template, component, idx),
+            Fragment(frag) => frag.iter().map(|child| self.create(child)).sum(),
         }
     }
 
@@ -487,40 +487,38 @@ impl<'b> VirtualDom {
         0
     }
 
-    pub(crate) fn create_fragment(&mut self, nodes: &'b [VNode<'b>]) -> usize {
-        nodes.iter().map(|child| self.create(child)).sum()
-    }
-
     pub(super) fn create_component_node(
         &mut self,
         template: &'b VNode<'b>,
         component: &'b VComponent<'b>,
         idx: usize,
     ) -> usize {
-        let scope = match component.props.take() {
-            Some(props) => {
-                let unbounded_props: Box<dyn AnyProps> = unsafe { std::mem::transmute(props) };
-                let scope = self.new_scope(unbounded_props, component.name);
-                scope.id
-            }
+        use RenderReturn::*;
 
-            // Component is coming back, it probably still exists, right?
-            None => component.scope.get().unwrap(),
-        };
+        // Load up a ScopeId for this vcomponent
+        let scope = self.load_scope_from_vcomponent(component);
 
         component.scope.set(Some(scope));
 
-        let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() };
-
-        use RenderReturn::*;
-
-        match return_nodes {
-            Sync(Some(t)) => self.mount_component(scope, template, t, idx),
-            Sync(None) => todo!("Propogate error upwards"),
-            Async(_) => self.mount_component_placeholder(template, idx, scope),
+        match unsafe { self.run_scope(scope).extend_lifetime_ref() } {
+            Ready(t) => self.mount_component(scope, template, t, idx),
+            Aborted(t) => self.mount_aborted(template, t),
+            Pending(_) => self.mount_async(template, idx, scope),
         }
     }
 
+    /// Load a scope from a vcomponent. If the props don't exist, that means the component is currently "live"
+    fn load_scope_from_vcomponent(&mut self, component: &VComponent) -> ScopeId {
+        component
+            .props
+            .take()
+            .map(|props| {
+                let unbounded_props: Box<dyn AnyProps> = unsafe { std::mem::transmute(props) };
+                self.new_scope(unbounded_props, component.name).id
+            })
+            .unwrap_or_else(|| component.scope.get().unwrap())
+    }
+
     fn mount_component(
         &mut self,
         scope: ScopeId,
@@ -575,15 +573,17 @@ impl<'b> VirtualDom {
         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 });
+        placeholder.id.set(Some(id));
+        1
+    }
+
     /// Take the rendered nodes from a component and handle them if they were async
     ///
     /// IE simply assign an ID to the placeholder
-    fn mount_component_placeholder(
-        &mut self,
-        template: &VNode,
-        idx: usize,
-        scope: ScopeId,
-    ) -> usize {
+    fn mount_async(&mut self, template: &VNode, idx: usize, scope: ScopeId) -> usize {
         let new_id = self.next_element(template, template.template.get().node_paths[idx]);
 
         // Set the placeholder of the scope

+ 61 - 28
packages/core/src/diff.rs

@@ -30,29 +30,44 @@ impl<'b> VirtualDom {
                 .try_load_node()
                 .expect("Call rebuild before diffing");
 
-            use RenderReturn::{Async, Sync};
+            use RenderReturn::{Aborted, Pending, Ready};
 
             match (old, new) {
-                (Sync(Some(l)), Sync(Some(r))) => self.diff_node(l, r),
-
-                // Err cases
-                (Sync(Some(l)), Sync(None)) => self.diff_ok_to_err(l),
-                (Sync(None), Sync(Some(r))) => self.diff_err_to_ok(r),
-                (Sync(None), Sync(None)) => { /* nothing */ }
-
-                // Async
-                (Sync(Some(_l)), Async(_)) => todo!(),
-                (Sync(None), Async(_)) => todo!(),
-                (Async(_), Sync(Some(_r))) => todo!(),
-                (Async(_), Sync(None)) => { /* nothing */ }
-                (Async(_), Async(_)) => { /* nothing */ }
+                // Normal pathway
+                (Ready(l), Ready(r)) => self.diff_node(l, r),
+
+                // Unwind the mutations if need be
+                (Ready(l), Aborted(p)) => self.diff_ok_to_err(l, p),
+
+                // Just move over the placeholder
+                (Aborted(l), Aborted(r)) => r.id.set(l.id.get()),
+
+                // Becomes async, do nothing while we ait
+                (Ready(_nodes), Pending(_fut)) => self.diff_ok_to_async(_nodes, scope),
+
+                // Placeholder becomes something
+                // We should also clear the error now
+                (Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]),
+
+                (Aborted(_), Pending(_)) => todo!("async should not resolve here"),
+                (Pending(_), Ready(_)) => todo!("async should not resolve here"),
+                (Pending(_), Aborted(_)) => todo!("async should not resolve here"),
+                (Pending(_), Pending(_)) => {
+                    // All suspense should resolve before we diff it again
+                    panic!("Should not roll from suspense to suspense.");
+                }
             };
         }
         self.scope_stack.pop();
     }
 
-    fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>) {}
-    fn diff_err_to_ok(&mut self, _l: &'b VNode<'b>) {}
+    fn diff_ok_to_async(&mut self, _new: &'b VNode<'b>, _scope: ScopeId) {
+        //
+    }
+
+    fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _p: &'b VPlaceholder) {
+        todo!()
+    }
 
     fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) {
         // If hot reloading is enabled, we need to make sure we're using the latest template
@@ -129,7 +144,7 @@ impl<'b> VirtualDom {
             (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),
-            (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, right),
+            (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."),
         };
@@ -208,8 +223,12 @@ impl<'b> VirtualDom {
     ) {
         let m = self.create_component_node(right_template, right, idx);
 
+        let pre_edits = self.mutations.edits.len();
+
         self.remove_component_node(left, true);
 
+        assert!(self.mutations.edits.len() > pre_edits);
+
         // We want to optimize the replace case to use one less mutation if possible
         // Since mutations are done in reverse, the last node removed will be the first in the stack
         // Instead of *just* removing it, we can use the replace mutation
@@ -691,7 +710,8 @@ impl<'b> VirtualDom {
                     Component(comp) => {
                         let scope = comp.scope.get().unwrap();
                         match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
-                            RenderReturn::Sync(Some(node)) => self.push_all_real_nodes(node),
+                            RenderReturn::Ready(node) => self.push_all_real_nodes(node),
+                            RenderReturn::Aborted(_node) => todo!(),
                             _ => todo!(),
                         }
                     }
@@ -719,7 +739,11 @@ impl<'b> VirtualDom {
     }
 
     /// Simply replace a placeholder with a list of nodes
-    fn replace_placeholder(&mut self, l: &'b VPlaceholder, r: &'b [VNode<'b>]) {
+    fn replace_placeholder(
+        &mut self,
+        l: &'b VPlaceholder,
+        r: impl IntoIterator<Item = &'b VNode<'b>>,
+    ) {
         let m = self.create_children(r);
         let id = l.id.get().unwrap();
         self.mutations.push(Mutation::ReplaceWith { id, m });
@@ -816,8 +840,11 @@ impl<'b> VirtualDom {
                 .map(|path| path.len());
             // if the path is 1 the attribute is in the root, so we don't need to clean it up
             // if the path is 0, the attribute is a not attached at all, so we don't need to clean it up
-            if let Some(..=1) = path_len {
-                continue;
+
+            if let Some(len) = path_len {
+                if (..=1).contains(&len) {
+                    continue;
+                }
             }
 
             let next_id = attr.mounted_element.get();
@@ -877,14 +904,20 @@ impl<'b> VirtualDom {
     }
 
     fn remove_component_node(&mut self, comp: &VComponent, gen_muts: bool) {
-        let scope = comp.scope.take().unwrap();
+        let scope = comp
+            .scope
+            .take()
+            .expect("VComponents to always have a scope");
 
         match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
-            RenderReturn::Sync(Some(t)) => {
-                println!("Removing component node sync {:?}", gen_muts);
-                self.remove_node(t, gen_muts)
+            RenderReturn::Ready(t) => self.remove_node(t, gen_muts),
+            RenderReturn::Aborted(t) => {
+                if let Some(id) = t.id.get() {
+                    self.try_reclaim(id);
+                }
+                return;
             }
-            _ => todo!("cannot handle nonstandard nodes"),
+            _ => todo!(),
         };
 
         let props = self.scopes[scope.0].props.take();
@@ -910,7 +943,7 @@ impl<'b> VirtualDom {
             Some(Component(comp)) => {
                 let scope = comp.scope.get().unwrap();
                 match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
-                    RenderReturn::Sync(Some(t)) => self.find_first_element(t),
+                    RenderReturn::Ready(t) => self.find_first_element(t),
                     _ => todo!("cannot handle nonstandard nodes"),
                 }
             }
@@ -926,7 +959,7 @@ impl<'b> VirtualDom {
             Some(Component(comp)) => {
                 let scope = comp.scope.get().unwrap();
                 match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
-                    RenderReturn::Sync(Some(t)) => self.find_last_element(t),
+                    RenderReturn::Ready(t) => self.find_last_element(t),
                     _ => todo!("cannot handle nonstandard nodes"),
                 }
             }

+ 191 - 7
packages/core/src/error_boundary.rs

@@ -1,19 +1,203 @@
-use std::cell::RefCell;
-
-use crate::ScopeId;
+use crate::{ScopeId, ScopeState};
+use std::{
+    any::{Any, TypeId},
+    cell::RefCell,
+    fmt::Debug,
+};
 
 /// A boundary that will capture any errors from child components
-#[allow(dead_code)]
 pub struct ErrorBoundary {
-    error: RefCell<Option<ScopeId>>,
-    id: ScopeId,
+    error: RefCell<Option<CapturedError>>,
+    _id: ScopeId,
+}
+
+/// An instance of an error captured by a descendant component.
+pub struct CapturedError {
+    /// The error captured by the error boundary
+    pub error: Box<dyn Debug + 'static>,
+
+    /// The scope that threw the error
+    pub scope: ScopeId,
+}
+
+impl CapturedError {
+    /// Downcast the error type into a concrete error type
+    pub fn downcast<T: 'static>(&self) -> Option<&T> {
+        if TypeId::of::<T>() == self.error.type_id() {
+            let raw = self.error.as_ref() as *const _ as *const T;
+            Some(unsafe { &*raw })
+        } else {
+            None
+        }
+    }
 }
 
 impl ErrorBoundary {
     pub fn new(id: ScopeId) -> Self {
         Self {
             error: RefCell::new(None),
-            id,
+            _id: id,
         }
     }
+
+    /// Push an error into this Error Boundary
+    pub fn insert_error(&self, scope: ScopeId, error: Box<dyn Debug + 'static>) {
+        self.error.replace(Some(CapturedError { error, scope }));
+    }
+}
+
+/// A trait to allow results to be thrown upwards to the nearest Error Boundary
+///
+/// The canonical way of using this trait is to throw results from hooks, aborting rendering
+/// through question mark synax. The throw method returns an option that evalutes to None
+/// if there is an error, injecting the error to the nearest error boundary.
+///
+/// If the value is `Ok`, then throw returns the value, not aborting the rendering preocess.
+///
+/// The call stack is saved for this component and provided to the error boundary
+///
+/// ```rust, ignore
+///
+/// #[inline_props]
+/// fn app(cx: Scope, count: String) -> Element {
+///     let id: i32 = count.parse().throw(cx)?;
+///
+///     cx.render(rsx! {
+///         div { "Count {}" }
+///     })
+/// }
+/// ```
+pub trait Throw<S = ()>: Sized {
+    /// The value that will be returned in if the given value is `Ok`.
+    type Out;
+
+    /// Returns an option that evalutes to None if there is an error, injecting the error to the nearest error boundary.
+    ///
+    /// If the value is `Ok`, then throw returns the value, not aborting the rendering preocess.
+    ///
+    /// The call stack is saved for this component and provided to the error boundary
+    ///
+    ///
+    /// Note that you can also manually throw errors using the throw method on `ScopeState` directly,
+    /// which is what this trait shells out to.
+    ///
+    ///
+    /// ```rust, ignore
+    ///
+    /// #[inline_props]
+    /// fn app(cx: Scope, count: String) -> Element {
+    ///     let id: i32 = count.parse().throw(cx)?;
+    ///
+    ///     cx.render(rsx! {
+    ///         div { "Count {}" }
+    ///     })
+    /// }
+    /// ```
+    fn throw(self, cx: &ScopeState) -> Option<Self::Out>;
+
+    /// Returns an option that evalutes to None if there is an error, injecting the error to the nearest error boundary.
+    ///
+    /// If the value is `Ok`, then throw returns the value, not aborting the rendering preocess.
+    ///
+    /// The call stack is saved for this component and provided to the error boundary
+    ///
+    ///
+    /// Note that you can also manually throw errors using the throw method on `ScopeState` directly,
+    /// which is what this trait shells out to.
+    ///
+    ///
+    /// ```rust, ignore
+    ///
+    /// #[inline_props]
+    /// fn app(cx: Scope, count: String) -> Element {
+    ///     let id: i32 = count.parse().throw(cx)?;
+    ///
+    ///     cx.render(rsx! {
+    ///         div { "Count {}" }
+    ///     })
+    /// }
+    /// ```
+    fn throw_with<D: Debug + 'static>(
+        self,
+        cx: &ScopeState,
+        e: impl FnOnce() -> D,
+    ) -> Option<Self::Out>;
+}
+
+/// We call clone on any errors that can be owned out of a reference
+impl<'a, T, O: Debug + 'static, E: ToOwned<Owned = O>> Throw for &'a Result<T, E> {
+    type Out = &'a T;
+
+    fn throw(self, cx: &ScopeState) -> Option<Self::Out> {
+        match self {
+            Ok(t) => Some(t),
+            Err(e) => {
+                cx.throw(e.to_owned());
+                None
+            }
+        }
+    }
+
+    fn throw_with<D: Debug + 'static>(
+        self,
+        cx: &ScopeState,
+        err: impl FnOnce() -> D,
+    ) -> Option<Self::Out> {
+        match self {
+            Ok(t) => Some(t),
+            Err(_e) => {
+                cx.throw(err());
+                None
+            }
+        }
+    }
+}
+
+/// Or just throw errors we know about
+impl<T, E: Debug + 'static> Throw for Result<T, E> {
+    type Out = T;
+
+    fn throw(self, cx: &ScopeState) -> Option<T> {
+        match self {
+            Ok(t) => Some(t),
+            Err(e) => {
+                cx.throw(e);
+                None
+            }
+        }
+    }
+
+    fn throw_with<D: Debug + 'static>(
+        self,
+        cx: &ScopeState,
+        error: impl FnOnce() -> D,
+    ) -> Option<Self::Out> {
+        self.ok().or_else(|| {
+            cx.throw(error());
+            None
+        })
+    }
+}
+
+/// Or just throw errors we know about
+impl<T> Throw for Option<T> {
+    type Out = T;
+
+    fn throw(self, cx: &ScopeState) -> Option<T> {
+        self.or_else(|| {
+            cx.throw("None error.");
+            None
+        })
+    }
+
+    fn throw_with<D: Debug + 'static>(
+        self,
+        cx: &ScopeState,
+        error: impl FnOnce() -> D,
+    ) -> Option<Self::Out> {
+        self.or_else(|| {
+            cx.throw(error());
+            None
+        })
+    }
 }

+ 5 - 5
packages/core/src/lib.rs

@@ -70,10 +70,10 @@ pub(crate) mod innerlude {
 }
 
 pub use crate::innerlude::{
-    fc_to_builder, Attribute, AttributeValue, Component, DynamicNode, Element, ElementId, Event,
-    Fragment, IntoDynNode, LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope,
-    ScopeId, ScopeState, Scoped, SuspenseContext, TaskId, Template, TemplateAttribute,
-    TemplateNode, VComponent, VNode, VText, VirtualDom,
+    fc_to_builder, Attribute, AttributeValue, CapturedError, Component, DynamicNode, Element,
+    ElementId, Event, Fragment, IntoDynNode, LazyNodes, Mutation, Mutations, Properties,
+    RenderReturn, Scope, ScopeId, ScopeState, Scoped, SuspenseContext, TaskId, Template,
+    TemplateAttribute, TemplateNode, VComponent, VNode, VText, VirtualDom,
 };
 
 /// The purpose of this module is to alleviate imports of many common types
@@ -83,7 +83,7 @@ pub mod prelude {
     pub use crate::innerlude::{
         fc_to_builder, Component, Element, Event, EventHandler, Fragment, LazyNodes, Properties,
         Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode,
-        VNode, VirtualDom,
+        Throw, VNode, VirtualDom,
     };
 }
 

+ 19 - 4
packages/core/src/nodes.rs

@@ -21,10 +21,22 @@ pub type TemplateId = &'static str;
 /// you might need to handle the case where there's no node immediately ready.
 pub enum RenderReturn<'a> {
     /// A currently-available element
-    Sync(Element<'a>),
+    Ready(VNode<'a>),
+
+    /// The component aborted rendering early. It might've thrown an error.
+    ///
+    /// In its place we've produced a placeholder to locate its spot in the dom when
+    /// it recovers.
+    Aborted(VPlaceholder),
 
     /// An ongoing future that will resolve to a [`Element`]
-    Async(BumpBox<'a, dyn Future<Output = Element<'a>> + 'a>),
+    Pending(BumpBox<'a, dyn Future<Output = Element<'a>> + 'a>),
+}
+
+impl<'a> Default for RenderReturn<'a> {
+    fn default() -> Self {
+        RenderReturn::Aborted(VPlaceholder::default())
+    }
 }
 
 /// A reference to a template along with any context needed to hydrate it
@@ -574,7 +586,10 @@ pub trait ComponentReturn<'a, A = ()> {
 
 impl<'a> ComponentReturn<'a> for Element<'a> {
     fn into_return(self, _cx: &ScopeState) -> RenderReturn<'a> {
-        RenderReturn::Sync(self)
+        match self {
+            Some(node) => RenderReturn::Ready(node),
+            None => RenderReturn::default(),
+        }
     }
 }
 
@@ -586,7 +601,7 @@ where
 {
     fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a> {
         let f: &mut dyn Future<Output = Element<'a>> = cx.bump().alloc(self);
-        RenderReturn::Async(unsafe { BumpBox::from_raw(f) })
+        RenderReturn::Pending(unsafe { BumpBox::from_raw(f) })
     }
 }
 

+ 4 - 4
packages/core/src/scheduler/mod.rs

@@ -8,7 +8,7 @@ mod waker;
 
 pub use suspense::*;
 pub use task::*;
-pub use waker::RcWake;
+pub use waker::ArcWake;
 
 /// The type of message that can be sent to the scheduler.
 ///
@@ -25,16 +25,16 @@ pub(crate) enum SchedulerMsg {
     SuspenseNotified(SuspenseId),
 }
 
-use std::{cell::RefCell, rc::Rc};
+use std::{cell::RefCell, rc::Rc, sync::Arc};
 
 pub(crate) struct Scheduler {
     pub sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
 
     /// Tasks created with cx.spawn
-    pub tasks: RefCell<Slab<Rc<LocalTask>>>,
+    pub tasks: RefCell<Slab<Arc<LocalTask>>>,
 
     /// Async components
-    pub leaves: RefCell<Slab<Rc<SuspenseLeaf>>>,
+    pub leaves: RefCell<Slab<Arc<SuspenseLeaf>>>,
 }
 
 impl Scheduler {

+ 4 - 4
packages/core/src/scheduler/suspense.rs

@@ -1,11 +1,11 @@
-use super::{waker::RcWake, SchedulerMsg};
+use super::{waker::ArcWake, SchedulerMsg};
 use crate::ElementId;
 use crate::{innerlude::Mutations, Element, ScopeId};
 use std::future::Future;
+use std::sync::Arc;
 use std::{
     cell::{Cell, RefCell},
     collections::HashSet,
-    rc::Rc,
 };
 
 /// An ID representing an ongoing suspended component
@@ -42,8 +42,8 @@ pub(crate) struct SuspenseLeaf {
     pub(crate) task: *mut dyn Future<Output = Element<'static>>,
 }
 
-impl RcWake for SuspenseLeaf {
-    fn wake_by_ref(arc_self: &Rc<Self>) {
+impl ArcWake for SuspenseLeaf {
+    fn wake_by_ref(arc_self: &Arc<Self>) {
         arc_self.notified.set(true);
         _ = arc_self
             .tx

+ 6 - 5
packages/core/src/scheduler/task.rs

@@ -1,8 +1,9 @@
-use super::{waker::RcWake, Scheduler, SchedulerMsg};
+use super::{waker::ArcWake, Scheduler, SchedulerMsg};
 use crate::ScopeId;
 use std::cell::RefCell;
 use std::future::Future;
-use std::{pin::Pin, rc::Rc};
+use std::pin::Pin;
+use std::sync::Arc;
 
 /// A task's unique identifier.
 ///
@@ -35,7 +36,7 @@ impl Scheduler {
         let entry = tasks.vacant_entry();
         let task_id = TaskId(entry.key());
 
-        entry.insert(Rc::new(LocalTask {
+        entry.insert(Arc::new(LocalTask {
             id: task_id,
             tx: self.sender.clone(),
             task: RefCell::new(Box::pin(task)),
@@ -57,8 +58,8 @@ impl Scheduler {
     }
 }
 
-impl RcWake for LocalTask {
-    fn wake_by_ref(arc_self: &Rc<Self>) {
+impl ArcWake for LocalTask {
+    fn wake_by_ref(arc_self: &Arc<Self>) {
         _ = arc_self
             .tx
             .unbounded_send(SchedulerMsg::TaskNotified(arc_self.id));

+ 7 - 4
packages/core/src/scheduler/wait.rs

@@ -10,7 +10,7 @@ use crate::{
     ScopeId, TaskId, VNode, VirtualDom,
 };
 
-use super::{waker::RcWake, SuspenseId};
+use super::{waker::ArcWake, SuspenseId};
 
 impl VirtualDom {
     /// Handle notifications by tasks inside the scheduler
@@ -67,18 +67,21 @@ impl VirtualDom {
         // we should attach them to that component and then render its children
         // continue rendering the tree until we hit yet another suspended component
         if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) {
-            // safety: we're not going to modify the suspense context but we don't want to make a clone of it
             let fiber = self.acquire_suspense_boundary(leaf.scope_id);
 
             let scope = &mut self.scopes[scope_id.0];
             let arena = scope.current_frame();
 
-            let ret = arena.bump.alloc(RenderReturn::Sync(new_nodes));
+            let ret = arena.bump.alloc(match new_nodes {
+                Some(new) => RenderReturn::Ready(new),
+                None => RenderReturn::default(),
+            });
+
             arena.node.set(ret);
 
             fiber.waiting_on.borrow_mut().remove(&id);
 
-            if let RenderReturn::Sync(Some(template)) = ret {
+            if let RenderReturn::Ready(template) = ret {
                 let mutations_ref = &mut fiber.mutations.borrow_mut();
                 let mutations = &mut **mutations_ref;
                 let template: &VNode = unsafe { std::mem::transmute(template) };

+ 13 - 12
packages/core/src/scheduler/waker.rs

@@ -1,36 +1,37 @@
+use std::mem;
+use std::sync::Arc;
 use std::task::{RawWaker, RawWakerVTable, Waker};
-use std::{mem, rc::Rc};
 
-pub trait RcWake: Sized {
+pub trait ArcWake: Sized {
     /// Create a waker from this self-wakening object
-    fn waker(self: &Rc<Self>) -> Waker {
-        unsafe fn rc_vtable<T: RcWake>() -> &'static RawWakerVTable {
+    fn waker(self: &Arc<Self>) -> Waker {
+        unsafe fn rc_vtable<T: ArcWake>() -> &'static RawWakerVTable {
             &RawWakerVTable::new(
                 |data| {
-                    let arc = mem::ManuallyDrop::new(Rc::<T>::from_raw(data.cast::<T>()));
+                    let arc = mem::ManuallyDrop::new(Arc::<T>::from_raw(data.cast::<T>()));
                     let _rc_clone: mem::ManuallyDrop<_> = arc.clone();
                     RawWaker::new(data, rc_vtable::<T>())
                 },
-                |data| Rc::from_raw(data.cast::<T>()).wake(),
+                |data| Arc::from_raw(data.cast::<T>()).wake(),
                 |data| {
-                    let arc = mem::ManuallyDrop::new(Rc::<T>::from_raw(data.cast::<T>()));
-                    RcWake::wake_by_ref(&arc);
+                    let arc = mem::ManuallyDrop::new(Arc::<T>::from_raw(data.cast::<T>()));
+                    ArcWake::wake_by_ref(&arc);
                 },
-                |data| drop(Rc::<T>::from_raw(data.cast::<T>())),
+                |data| drop(Arc::<T>::from_raw(data.cast::<T>())),
             )
         }
 
         unsafe {
             Waker::from_raw(RawWaker::new(
-                Rc::into_raw(self.clone()).cast(),
+                Arc::into_raw(self.clone()).cast(),
                 rc_vtable::<Self>(),
             ))
         }
     }
 
-    fn wake_by_ref(arc_self: &Rc<Self>);
+    fn wake_by_ref(arc_self: &Arc<Self>);
 
-    fn wake(self: Rc<Self>) {
+    fn wake(self: Arc<Self>) {
         Self::wake_by_ref(&self)
     }
 }

+ 11 - 6
packages/core/src/scope_arena.rs

@@ -4,7 +4,7 @@ use crate::{
     innerlude::DirtyScope,
     innerlude::{SuspenseId, SuspenseLeaf},
     nodes::RenderReturn,
-    scheduler::RcWake,
+    scheduler::ArcWake,
     scopes::{ScopeId, ScopeState},
     virtual_dom::VirtualDom,
 };
@@ -13,7 +13,7 @@ use futures_util::FutureExt;
 use std::{
     mem,
     pin::Pin,
-    rc::Rc,
+    sync::Arc,
     task::{Context, Poll},
 };
 
@@ -79,17 +79,18 @@ impl VirtualDom {
             // safety: due to how we traverse the tree, we know that the scope is not currently aliased
             let props: &dyn AnyProps = scope.props.as_ref().unwrap().as_ref();
             let props: &dyn AnyProps = mem::transmute(props);
+
             props.render(scope).extend_lifetime()
         };
 
         // immediately resolve futures that can be resolved
-        if let RenderReturn::Async(task) = &mut new_nodes {
+        if let RenderReturn::Pending(task) = &mut new_nodes {
             let mut leaves = self.scheduler.leaves.borrow_mut();
 
             let entry = leaves.vacant_entry();
             let suspense_id = SuspenseId(entry.key());
 
-            let leaf = Rc::new(SuspenseLeaf {
+            let leaf = Arc::new(SuspenseLeaf {
                 scope_id,
                 task: task.as_mut(),
                 id: suspense_id,
@@ -108,7 +109,11 @@ impl VirtualDom {
                 match pinned.poll_unpin(&mut cx) {
                     // If nodes are produced, then set it and we can break
                     Poll::Ready(nodes) => {
-                        new_nodes = RenderReturn::Sync(nodes);
+                        new_nodes = match nodes {
+                            Some(nodes) => RenderReturn::Ready(nodes),
+                            None => RenderReturn::default(),
+                        };
+
                         break;
                     }
 
@@ -150,6 +155,6 @@ impl VirtualDom {
         });
 
         // rebind the lifetime now that its stored internally
-        unsafe { mem::transmute(allocated) }
+        unsafe { allocated.extend_lifetime_ref() }
     }
 }

+ 15 - 2
packages/core/src/scopes.rs

@@ -4,7 +4,7 @@ use crate::{
     arena::ElementId,
     bump_frame::BumpFrame,
     innerlude::{DynamicNode, EventHandler, VComponent, VText},
-    innerlude::{Scheduler, SchedulerMsg},
+    innerlude::{ErrorBoundary, Scheduler, SchedulerMsg},
     lazynodes::LazyNodes,
     nodes::{ComponentReturn, IntoAttributeValue, IntoDynNode, RenderReturn},
     Attribute, AttributeValue, Element, Event, Properties, TaskId,
@@ -14,7 +14,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
 use std::{
     any::{Any, TypeId},
     cell::{Cell, RefCell},
-    fmt::Arguments,
+    fmt::{Arguments, Debug},
     future::Future,
     rc::Rc,
     sync::Arc,
@@ -509,6 +509,19 @@ impl<'src> ScopeState {
         AttributeValue::Listener(RefCell::new(Some(boxed)))
     }
 
+    /// Inject an error into the nearest error boundary and quit rendering
+    ///
+    /// The error doesn't need to implement Error or any specific traits since the boundary
+    /// itself will downcast the error into a trait object.
+    pub fn throw(&self, error: impl Debug + 'static) -> Option<()> {
+        if let Some(cx) = self.consume_context::<Rc<ErrorBoundary>>() {
+            cx.insert_error(self.scope_id(), Box::new(error));
+        }
+
+        // Always return none during a throw
+        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`.

+ 4 - 4
packages/core/src/virtual_dom.rs

@@ -466,7 +466,7 @@ impl VirtualDom {
         self.register_template_first_byte_index(template);
         // iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
         for (_, scope) in &self.scopes {
-            if let Some(RenderReturn::Sync(Some(sync))) = scope.try_root_node() {
+            if let Some(RenderReturn::Ready(sync)) = scope.try_root_node() {
                 if sync.template.get().name.rsplit_once(':').unwrap().0
                     == template.name.rsplit_once(':').unwrap().0
                 {
@@ -503,7 +503,7 @@ impl VirtualDom {
     pub fn rebuild(&mut self) -> Mutations {
         match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } {
             // Rebuilding implies we append the created elements to the root
-            RenderReturn::Sync(Some(node)) => {
+            RenderReturn::Ready(node) => {
                 let m = self.create_scope(ScopeId(0), node);
                 self.mutations.edits.push(Mutation::AppendChildren {
                     id: ElementId(0),
@@ -511,8 +511,8 @@ impl VirtualDom {
                 });
             }
             // If an error occurs, we should try to render the default error component and context where the error occured
-            RenderReturn::Sync(None) => panic!("Cannot catch errors during rebuild"),
-            RenderReturn::Async(_) => unreachable!("Root scope cannot be an async component"),
+            RenderReturn::Aborted(_placeholder) => panic!("Cannot catch errors during rebuild"),
+            RenderReturn::Pending(_) => unreachable!("Root scope cannot be an async component"),
         }
 
         self.finalize()

+ 58 - 0
packages/core/tests/error_boundary.rs

@@ -0,0 +1,58 @@
+use dioxus::prelude::*;
+use futures_util::Future;
+
+#[test]
+fn catches_panic() {
+    let mut dom = VirtualDom::new(app);
+
+    let a = dom.rebuild();
+
+    dbg!(a);
+}
+
+fn app(cx: Scope) -> Element {
+    cx.render(rsx! {
+        div {
+            h1 { "Title" }
+
+            NoneChild {}
+        }
+    })
+}
+
+fn NoneChild(cx: Scope) -> Element {
+    None
+}
+
+fn PanicChild(cx: Scope) -> Element {
+    panic!("Rendering panicked for whatever reason");
+
+    cx.render(rsx! {
+        h1 { "It works!" }
+    })
+}
+
+fn ThrowChild(cx: Scope) -> Element {
+    cx.throw(std::io::Error::new(std::io::ErrorKind::AddrInUse, "asd"))?;
+
+    let g: i32 = "123123".parse().throw(cx)?;
+
+    cx.render(rsx! {
+        div {}
+    })
+}
+
+fn custom_allocator(cx: Scope) -> Element {
+    let g = String::new();
+
+    let p = g.as_str();
+
+    let g2 = cx.use_hook(|| 123);
+    // cx.spawn(async move {
+
+    //     //
+    //     // println!("Thig is {p}");
+    // });
+
+    None
+}

+ 8 - 1
packages/router/src/components/link.rs

@@ -55,6 +55,9 @@ pub struct LinkProps<'a> {
 
     /// Pass children into the `<a>` element
     pub children: Element<'a>,
+
+    /// The onclick event handler.
+    pub onclick: Option<EventHandler<'a, MouseEvent>>,
 }
 
 /// A component that renders a link to a route.
@@ -119,7 +122,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
             title: format_args!("{}", title.unwrap_or("")),
             prevent_default: "{prevent_default}",
             target: format_args!("{}", if * new_tab { "_blank" } else { "" }),
-            onclick: move |_| {
+            onclick: move |evt| {
                 log::trace!("Clicked link to {}", to);
 
                 if !outerlink {
@@ -138,6 +141,10 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
                         );
                     }
                 }
+
+                if let Some(onclick) = cx.props.onclick.as_ref() {
+                    onclick.call(evt);
+                }
             },
             children
         }

+ 1 - 1
packages/router/src/components/redirect.rs

@@ -47,5 +47,5 @@ pub fn Redirect<'a>(cx: Scope<'a, RedirectProps<'a>>) -> Element {
         router.replace_route(cx.props.to, None, None);
     }
 
-    None
+    cx.render(rsx!(()))
 }

+ 1 - 1
packages/router/src/components/route.rs

@@ -52,6 +52,6 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element {
         cx.render(rsx!(&cx.props.children))
     } else {
         log::debug!("Route should *not* render: {:?}", cx.scope_id());
-        None
+        cx.render(rsx!(()))
     }
 }

+ 17 - 0
packages/router/src/hooks/use_route.rs

@@ -107,6 +107,23 @@ impl UseRoute {
     {
         self.segment(name).map(|value| value.parse::<T>())
     }
+
+    /// Get the named parameter from the path, as defined in your router. The
+    /// value will be parsed into the type specified by `T` by calling
+    /// `value.parse::<T>()`. This method returns `None` if the named
+    /// parameter does not exist in the current path.
+    pub fn parse_segment_or_404<T>(&self, name: &str) -> Option<T>
+    where
+        T: FromStr,
+    {
+        match self.parse_segment(name) {
+            Some(Ok(val)) => Some(val),
+            _ => {
+                // todo: throw a 404
+                None
+            }
+        }
+    }
 }
 
 // The entire purpose of this struct is to unubscribe this component when it is unmounted.

+ 9 - 0
packages/router/src/lib.rs

@@ -30,3 +30,12 @@ mod service;
 
 pub use routecontext::*;
 pub use service::*;
+
+/// An error specific to the Router
+#[derive(Debug)]
+pub enum Error {
+    /// The route was not found while trying to navigate to it.
+    ///
+    /// This will force the router to redirect to the 404 page.
+    NotFound,
+}

+ 2 - 2
packages/ssr/src/renderer.rs

@@ -51,7 +51,7 @@ impl Renderer {
     ) -> std::fmt::Result {
         // We should never ever run into async or errored nodes in SSR
         // Error boundaries and suspense boundaries will convert these to sync
-        if let RenderReturn::Sync(Some(node)) = dom.get_scope(scope).unwrap().root_node() {
+        if let RenderReturn::Ready(node) = dom.get_scope(scope).unwrap().root_node() {
             self.render_template(buf, dom, node)?
         };
 
@@ -89,7 +89,7 @@ impl Renderer {
                             let scope = dom.get_scope(id).unwrap();
                             let node = scope.root_node();
                             match node {
-                                RenderReturn::Sync(Some(node)) => {
+                                RenderReturn::Ready(node) => {
                                     self.render_template(buf, dom, node)?
                                 }
                                 _ => todo!(

+ 1 - 1
packages/tui/examples/tui_colorpicker.rs

@@ -17,7 +17,7 @@ fn app(cx: Scope) -> Element {
             width: "100%",
             background_color: "hsl({hue}, 70%, {brightness}%)",
             onmousemove: move |evt| {
-                if let RenderReturn::Sync(Some(node)) = cx.root_node() {
+                if let RenderReturn::Ready(node) = cx.root_node() {
                     if let Some(id) = node.root_ids.get(0){
                         let node = tui_query.get(id);
                         let Size{width, height} = node.size().unwrap();

+ 1 - 1
packages/tui/src/widgets/mod.rs

@@ -10,7 +10,7 @@ use dioxus_core::{ElementId, RenderReturn, Scope};
 pub use input::*;
 
 pub(crate) fn get_root_id<T>(cx: Scope<T>) -> Option<ElementId> {
-    if let RenderReturn::Sync(Some(sync)) = cx.root_node() {
+    if let RenderReturn::Ready(sync) = cx.root_node() {
         sync.root_ids.get(0)
     } else {
         None

+ 4 - 6
packages/web/src/dom.rs

@@ -121,12 +121,10 @@ impl WebsysDom {
                     } = attr
                     {
                         match namespace {
-                            Some(ns) if *ns == "style" => el
-                                .dyn_ref::<HtmlElement>()
-                                .unwrap()
-                                .style()
-                                .set_property(name, value)
-                                .unwrap(),
+                            Some(ns) if *ns == "style" => {
+                                el.dyn_ref::<HtmlElement>()
+                                    .map(|f| f.style().set_property(name, value));
+                            }
                             Some(ns) => el.set_attribute_ns(Some(ns), name, value).unwrap(),
                             None => el.set_attribute(name, value).unwrap(),
                         }

+ 10 - 9
packages/web/src/hot_reload.rs

@@ -18,15 +18,15 @@ pub(crate) fn init() -> UnboundedReceiver<Template<'static>> {
 
 #[cfg(debug_assertions)]
 pub(crate) fn init() -> UnboundedReceiver<Template<'static>> {
-    use core::panic;
     use std::convert::TryInto;
 
+    use serde::Deserialize;
+
     let window = web_sys::window().unwrap();
 
-    let protocol = if window.location().protocol().unwrap() == "https:" {
-        "wss:"
-    } else {
-        "ws:"
+    let protocol = match window.location().protocol().unwrap() {
+        prot if prot == "https:" => "wss:",
+        _ => "ws:",
     };
 
     let url = format!(
@@ -43,10 +43,11 @@ pub(crate) fn init() -> UnboundedReceiver<Template<'static>> {
         if let Ok(text) = e.data().dyn_into::<js_sys::JsString>() {
             let text: Result<String, _> = text.try_into();
             if let Ok(string) = text {
-                match serde_json::from_str(Box::leak(string.into_boxed_str())) {
-                    Ok(template) => _ = tx.unbounded_send(template),
-                    Err(e) => panic!("Failed to parse template: {}", e),
-                }
+                let val = serde_json::from_str::<serde_json::Value>(&string).unwrap();
+                // leak the value
+                let val: &'static serde_json::Value = Box::leak(Box::new(val));
+                let template: Template<'_> = Template::deserialize(val).unwrap();
+                tx.unbounded_send(template).unwrap();
             }
         }
     }) as Box<dyn FnMut(MessageEvent)>);