Browse Source

Merge pull request #1197 from DioxusLabs/jk/simple-suspense

Rip out async components in favor of a simpler suspense
Jonathan Kelley 1 year ago
parent
commit
a6002a6c42

+ 6 - 6
examples/dog_app.rs

@@ -10,7 +10,7 @@ struct ListBreeds {
     message: HashMap<String, Vec<String>>,
 }
 
-async fn app_root(cx: Scope<'_>) -> Element {
+fn app_root(cx: Scope<'_>) -> Element {
     let breed = use_state(cx, || "deerhound".to_string());
 
     let breeds = use_future!(cx, || async move {
@@ -21,13 +21,13 @@ async fn app_root(cx: Scope<'_>) -> Element {
             .await
     });
 
-    match breeds.await {
-        Ok(breeds) => cx.render(rsx! {
+    match breeds.value()? {
+        Ok(breed_list) => cx.render(rsx! {
             div { height: "500px",
                 h1 { "Select a dog breed!" }
                 div { display: "flex",
                     ul { flex: "50%",
-                        for cur_breed in breeds.message.keys().take(10) {
+                        for cur_breed in breed_list.message.keys().take(10) {
                             li { key: "{cur_breed}",
                                 button {
                                     onclick: move |_| breed.set(cur_breed.clone()),
@@ -50,7 +50,7 @@ struct DogApi {
 }
 
 #[inline_props]
-async fn breed_pic(cx: Scope, breed: String) -> Element {
+fn breed_pic(cx: Scope, breed: String) -> Element {
     let fut = use_future!(cx, |breed| async move {
         reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random"))
             .await
@@ -59,7 +59,7 @@ async fn breed_pic(cx: Scope, breed: String) -> Element {
             .await
     });
 
-    match fut.await {
+    match fut.value()? {
         Ok(resp) => render! {
             div {
                 button {

+ 1 - 0
packages/core/Cargo.toml

@@ -40,6 +40,7 @@ tokio = { workspace = true, features = ["full"] }
 dioxus = { workspace = true }
 pretty_assertions = "1.3.0"
 rand = "0.8.5"
+dioxus-ssr = { workspace = true }
 
 [features]
 default = []

+ 10 - 19
packages/core/src/any_props.rs

@@ -1,11 +1,10 @@
-use std::{marker::PhantomData, panic::AssertUnwindSafe};
-
 use crate::{
     innerlude::Scoped,
-    nodes::{ComponentReturn, RenderReturn},
+    nodes::RenderReturn,
     scopes::{Scope, ScopeState},
     Element,
 };
+use std::panic::AssertUnwindSafe;
 
 /// A trait that essentially allows VComponentProps to be used generically
 ///
@@ -18,19 +17,15 @@ pub(crate) unsafe trait AnyProps<'a> {
     unsafe fn memoize(&self, other: &dyn AnyProps) -> bool;
 }
 
-pub(crate) struct VProps<'a, P, A, F: ComponentReturn<'a, A> = Element<'a>> {
-    pub render_fn: fn(Scope<'a, P>) -> F,
+pub(crate) struct VProps<'a, P> {
+    pub render_fn: fn(Scope<'a, P>) -> Element<'a>,
     pub memo: unsafe fn(&P, &P) -> bool,
     pub props: P,
-    _marker: PhantomData<A>,
 }
 
-impl<'a, P, A, F> VProps<'a, P, A, F>
-where
-    F: ComponentReturn<'a, A>,
-{
+impl<'a, P> VProps<'a, P> {
     pub(crate) fn new(
-        render_fn: fn(Scope<'a, P>) -> F,
+        render_fn: fn(Scope<'a, P>) -> Element<'a>,
         memo: unsafe fn(&P, &P) -> bool,
         props: P,
     ) -> Self {
@@ -38,15 +33,11 @@ where
             render_fn,
             memo,
             props,
-            _marker: PhantomData,
         }
     }
 }
 
-unsafe impl<'a, P, A, F> AnyProps<'a> for VProps<'a, P, A, F>
-where
-    F: ComponentReturn<'a, A>,
-{
+unsafe impl<'a, P> AnyProps<'a> for VProps<'a, P> {
     fn props_ptr(&self) -> *const () {
         &self.props as *const _ as *const ()
     }
@@ -69,12 +60,12 @@ where
                 scope: cx,
             });
 
-            (self.render_fn)(scope).into_return(cx)
+            (self.render_fn)(scope)
         }));
 
         match res {
-            Ok(e) => e,
-            Err(_) => RenderReturn::default(),
+            Ok(Some(e)) => RenderReturn::Ready(e),
+            _ => RenderReturn::default(),
         }
     }
 }

+ 4 - 78
packages/core/src/create.rs

@@ -5,10 +5,9 @@ use crate::mutations::Mutation::*;
 use crate::nodes::VNode;
 use crate::nodes::{DynamicNode, TemplateNode};
 use crate::virtual_dom::VirtualDom;
-use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, SuspenseContext, Template};
+use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, Template};
 use std::cell::Cell;
 use std::iter::Peekable;
-use std::rc::Rc;
 use TemplateNode::*;
 
 #[cfg(debug_assertions)]
@@ -445,7 +444,7 @@ impl<'b> VirtualDom {
         match node {
             Text(text) => self.create_dynamic_text(template, text, idx),
             Placeholder(place) => self.create_placeholder(place, template, idx),
-            Component(component) => self.create_component_node(template, component, idx),
+            Component(component) => self.create_component_node(template, component),
             Fragment(frag) => frag.iter().map(|child| self.create(child)).sum(),
         }
     }
@@ -502,7 +501,6 @@ impl<'b> VirtualDom {
         &mut self,
         template: &'b VNode<'b>,
         component: &'b VComponent<'b>,
-        idx: usize,
     ) -> usize {
         use RenderReturn::*;
 
@@ -512,9 +510,9 @@ impl<'b> VirtualDom {
         component.scope.set(Some(scope));
 
         match unsafe { self.run_scope(scope).extend_lifetime_ref() } {
-            Ready(t) => self.mount_component(scope, template, t, idx),
+            // Create the component's root element
+            Ready(t) => self.create_scope(scope, t),
             Aborted(t) => self.mount_aborted(template, t),
-            Pending(_) => self.mount_async(template, idx, scope),
         }
     }
 
@@ -530,60 +528,6 @@ impl<'b> VirtualDom {
             .unwrap_or_else(|| component.scope.get().unwrap())
     }
 
-    fn mount_component(
-        &mut self,
-        scope: ScopeId,
-        parent: &'b VNode<'b>,
-        new: &'b VNode<'b>,
-        idx: usize,
-    ) -> usize {
-        // Keep track of how many mutations are in the buffer in case we need to split them out if a suspense boundary
-        // is encountered
-        let mutations_to_this_point = self.mutations.edits.len();
-
-        // Create the component's root element
-        let created = self.create_scope(scope, new);
-
-        // If there are no suspense leaves below us, then just don't bother checking anything suspense related
-        if self.collected_leaves.is_empty() {
-            return created;
-        }
-
-        // If running the scope has collected some leaves and *this* component is a boundary, then handle the suspense
-        let boundary = match self.scopes[scope].has_context::<Rc<SuspenseContext>>() {
-            Some(boundary) => boundary,
-            _ => return created,
-        };
-
-        // Since this is a boundary, use its placeholder within the template as the placeholder for the suspense tree
-        let new_id = self.next_element(new, parent.template.get().node_paths[idx]);
-
-        // Now connect everything to the boundary
-        self.scopes[scope].placeholder.set(Some(new_id));
-
-        // This involves breaking off the mutations to this point, and then creating a new placeholder for the boundary
-        // Note that we break off dynamic mutations only - since static mutations aren't rendered immediately
-        let split_off = unsafe {
-            std::mem::transmute::<Vec<Mutation>, Vec<Mutation>>(
-                self.mutations.edits.split_off(mutations_to_this_point),
-            )
-        };
-        boundary.mutations.borrow_mut().edits.extend(split_off);
-        boundary.created_on_stack.set(created);
-        boundary
-            .waiting_on
-            .borrow_mut()
-            .extend(self.collected_leaves.drain(..));
-
-        // Now assign the placeholder in the DOM
-        self.mutations.push(AssignId {
-            id: new_id,
-            path: &parent.template.get().node_paths[idx][1..],
-        });
-
-        0
-    }
-
     fn mount_aborted(&mut self, parent: &'b VNode<'b>, placeholder: &VPlaceholder) -> usize {
         let id = self.next_element(parent, &[]);
         self.mutations.push(Mutation::CreatePlaceholder { id });
@@ -591,24 +535,6 @@ impl<'b> VirtualDom {
         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_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
-        self.scopes[scope].placeholder.set(Some(new_id));
-
-        // Since the placeholder is already in the DOM, we don't create any new nodes
-        self.mutations.push(AssignId {
-            id: new_id,
-            path: &template.template.get().node_paths[idx][1..],
-        });
-
-        0
-    }
-
     fn set_slot(
         &mut self,
         template: &'b VNode<'b>,

+ 7 - 29
packages/core/src/diff.rs

@@ -30,7 +30,7 @@ impl<'b> VirtualDom {
                 .try_load_node()
                 .expect("Call rebuild before diffing");
 
-            use RenderReturn::{Aborted, Pending, Ready};
+            use RenderReturn::{Aborted, Ready};
 
             match (old, new) {
                 // Normal pathway
@@ -42,29 +42,14 @@ impl<'b> VirtualDom {
                 // Just move over the placeholder
                 (Aborted(l), Aborted(r)) => r.id.set(l.id.get()),
 
-                // Becomes async, do nothing while we wait
-                (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_async(&mut self, _new: &'b VNode<'b>, _scope: ScopeId) {
-        //
-    }
-
     fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) {
         let id = self.next_null();
         p.id.set(Some(id));
@@ -139,9 +124,8 @@ impl<'b> VirtualDom {
             .dynamic_nodes
             .iter()
             .zip(right_template.dynamic_nodes.iter())
-            .enumerate()
-            .for_each(|(idx, (left_node, right_node))| {
-                self.diff_dynamic_node(left_node, right_node, right_template, idx);
+            .for_each(|(left_node, right_node)| {
+                self.diff_dynamic_node(left_node, right_node, right_template);
             });
 
         // Make sure the roots get transferred over while we're here
@@ -160,13 +144,12 @@ impl<'b> VirtualDom {
         left_node: &'b DynamicNode<'b>,
         right_node: &'b DynamicNode<'b>,
         node: &'b VNode<'b>,
-        idx: usize,
     ) {
         match (left_node, right_node) {
             (Text(left), Text(right)) => self.diff_vtext(left, right, node),
             (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right),
             (Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()),
-            (Component(left), Component(right)) => self.diff_vcomponent(left, right, node, idx),
+            (Component(left), Component(right)) => self.diff_vcomponent(left, right, node),
             (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right),
             (Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right),
             _ => todo!("This is an usual custom case for dynamic nodes. We don't know how to handle it yet."),
@@ -190,7 +173,6 @@ impl<'b> VirtualDom {
         left: &'b VComponent<'b>,
         right: &'b VComponent<'b>,
         right_template: &'b VNode<'b>,
-        idx: usize,
     ) {
         if std::ptr::eq(left, right) {
             return;
@@ -198,7 +180,7 @@ impl<'b> VirtualDom {
 
         // Replace components that have different render fns
         if left.render_fn != right.render_fn {
-            return self.replace_vcomponent(right_template, right, idx, left);
+            return self.replace_vcomponent(right_template, right, left);
         }
 
         // Make sure the new vcomponent has the right scopeid associated to it
@@ -235,10 +217,9 @@ impl<'b> VirtualDom {
         &mut self,
         right_template: &'b VNode<'b>,
         right: &'b VComponent<'b>,
-        idx: usize,
         left: &'b VComponent<'b>,
     ) {
-        let m = self.create_component_node(right_template, right, idx);
+        let m = self.create_component_node(right_template, right);
 
         let pre_edits = self.mutations.edits.len();
 
@@ -297,8 +278,7 @@ impl<'b> VirtualDom {
             None => self.replace(left, [right]),
             Some(components) => components
                 .into_iter()
-                .enumerate()
-                .for_each(|(idx, (l, r))| self.diff_vcomponent(l, r, right, idx)),
+                .for_each(|(l, r)| self.diff_vcomponent(l, r, right)),
         }
     }
 
@@ -735,7 +715,6 @@ impl<'b> VirtualDom {
                         match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
                             RenderReturn::Ready(node) => self.push_all_real_nodes(node),
                             RenderReturn::Aborted(_node) => todo!(),
-                            _ => todo!(),
                         }
                     }
                 }
@@ -937,7 +916,6 @@ impl<'b> VirtualDom {
         match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
             RenderReturn::Ready(t) => self.remove_node(t, gen_muts),
             RenderReturn::Aborted(placeholder) => self.remove_placeholder(placeholder, gen_muts),
-            _ => todo!(),
         };
 
         // Restore the props back to the vcomponent in case it gets rendered again

+ 2 - 3
packages/core/src/lib.rs

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

+ 0 - 30
packages/core/src/nodes.rs

@@ -7,7 +7,6 @@ use std::{
     any::{Any, TypeId},
     cell::{Cell, RefCell, UnsafeCell},
     fmt::{Arguments, Debug},
-    future::Future,
 };
 
 pub type TemplateId = &'static str;
@@ -28,9 +27,6 @@ pub enum RenderReturn<'a> {
     /// 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`]
-    Pending(BumpBox<'a, dyn Future<Output = Element<'a>> + 'a>),
 }
 
 impl<'a> Default for RenderReturn<'a> {
@@ -688,32 +684,6 @@ impl<T: Any + PartialEq + 'static> AnyValue for T {
     }
 }
 
-#[doc(hidden)]
-pub trait ComponentReturn<'a, A = ()> {
-    fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a>;
-}
-
-impl<'a> ComponentReturn<'a> for Element<'a> {
-    fn into_return(self, _cx: &ScopeState) -> RenderReturn<'a> {
-        match self {
-            Some(node) => RenderReturn::Ready(node),
-            None => RenderReturn::default(),
-        }
-    }
-}
-
-#[doc(hidden)]
-pub struct AsyncMarker;
-impl<'a, F> ComponentReturn<'a, AsyncMarker> for F
-where
-    F: Future<Output = Element<'a>> + 'a,
-{
-    fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a> {
-        let f: &mut dyn Future<Output = Element<'a>> = cx.bump().alloc(self);
-        RenderReturn::Pending(unsafe { BumpBox::from_raw(f) })
-    }
-}
-
 impl<'a> RenderReturn<'a> {
     pub(crate) unsafe fn extend_lifetime_ref<'c>(&self) -> &'c RenderReturn<'c> {
         unsafe { std::mem::transmute(self) }

+ 1 - 1
packages/core/src/properties.rs

@@ -70,6 +70,6 @@ impl EmptyBuilder {
 
 /// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern
 /// to initialize a component's props.
-pub fn fc_to_builder<'a, A, T: Properties + 'a>(_: fn(Scope<'a, T>) -> A) -> T::Builder {
+pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element<'a>) -> T::Builder {
     T::builder()
 }

+ 0 - 9
packages/core/src/scheduler/mod.rs

@@ -1,11 +1,9 @@
 use crate::ScopeId;
 use slab::Slab;
 
-mod suspense;
 mod task;
 mod wait;
 
-pub use suspense::*;
 pub use task::*;
 
 /// The type of message that can be sent to the scheduler.
@@ -18,9 +16,6 @@ pub(crate) enum SchedulerMsg {
 
     /// A task has woken and needs to be progressed
     TaskNotified(TaskId),
-
-    /// A task has woken and needs to be progressed
-    SuspenseNotified(SuspenseId),
 }
 
 use std::{cell::RefCell, rc::Rc};
@@ -30,9 +25,6 @@ pub(crate) struct Scheduler {
 
     /// Tasks created with cx.spawn
     pub tasks: RefCell<Slab<LocalTask>>,
-
-    /// Async components
-    pub leaves: RefCell<Slab<SuspenseLeaf>>,
 }
 
 impl Scheduler {
@@ -40,7 +32,6 @@ impl Scheduler {
         Rc::new(Scheduler {
             sender,
             tasks: RefCell::new(Slab::new()),
-            leaves: RefCell::new(Slab::new()),
         })
     }
 }

+ 3 - 29
packages/core/src/scheduler/suspense.rs

@@ -11,17 +11,10 @@ use std::{
     collections::HashSet,
 };
 
-/// An ID representing an ongoing suspended component
-#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
-pub(crate) struct SuspenseId(pub usize);
-
 /// A boundary in the VirtualDom that captures all suspended components below it
 pub struct SuspenseContext {
     pub(crate) id: ScopeId,
-    pub(crate) waiting_on: RefCell<HashSet<SuspenseId>>,
-    pub(crate) mutations: RefCell<Mutations<'static>>,
-    pub(crate) placeholder: Cell<Option<ElementId>>,
-    pub(crate) created_on_stack: Cell<usize>,
+    pub(crate) waiting_on: RefCell<HashSet<ScopeId>>,
 }
 
 impl SuspenseContext {
@@ -30,29 +23,10 @@ impl SuspenseContext {
         Self {
             id,
             waiting_on: Default::default(),
-            mutations: RefCell::new(Mutations::default()),
-            placeholder: Cell::new(None),
-            created_on_stack: Cell::new(0),
         }
     }
-}
-
-pub(crate) struct SuspenseLeaf {
-    pub(crate) scope_id: ScopeId,
-    pub(crate) notified: Cell<bool>,
-    pub(crate) task: *mut dyn Future<Output = Element<'static>>,
-    pub(crate) waker: Waker,
-}
-
-pub struct SuspenseHandle {
-    pub(crate) id: SuspenseId,
-    pub(crate) tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
-}
 
-impl ArcWake for SuspenseHandle {
-    fn wake_by_ref(arc_self: &Arc<Self>) {
-        _ = arc_self
-            .tx
-            .unbounded_send(SchedulerMsg::SuspenseNotified(arc_self.id));
+    pub fn mark_suspend(&self, id: ScopeId) {
+        self.waiting_on.borrow_mut().insert(id);
     }
 }

+ 2 - 83
packages/core/src/scheduler/wait.rs

@@ -1,16 +1,5 @@
-use futures_util::FutureExt;
-use std::{
-    rc::Rc,
-    task::{Context, Poll},
-};
-
-use crate::{
-    innerlude::{Mutation, Mutations, SuspenseContext},
-    nodes::RenderReturn,
-    ScopeId, TaskId, VNode, VirtualDom,
-};
-
-use super::SuspenseId;
+use crate::{TaskId, VirtualDom};
+use std::task::Context;
 
 impl VirtualDom {
     /// Handle notifications by tasks inside the scheduler
@@ -38,74 +27,4 @@ impl VirtualDom {
             tasks.try_remove(id.0);
         }
     }
-
-    pub(crate) fn acquire_suspense_boundary(&self, id: ScopeId) -> Rc<SuspenseContext> {
-        self.scopes[id]
-            .consume_context::<Rc<SuspenseContext>>()
-            .unwrap()
-    }
-
-    pub(crate) fn handle_suspense_wakeup(&mut self, id: SuspenseId) {
-        let leaves = self.scheduler.leaves.borrow_mut();
-        let leaf = leaves.get(id.0).unwrap();
-
-        let scope_id = leaf.scope_id;
-
-        // todo: cache the waker
-        let mut cx = Context::from_waker(&leaf.waker);
-
-        // Safety: the future is always pinned to the bump arena
-        let mut pinned = unsafe { std::pin::Pin::new_unchecked(&mut *leaf.task) };
-        let as_pinned_mut = &mut pinned;
-
-        // the component finished rendering and gave us nodes
-        // 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) {
-            let fiber = self.acquire_suspense_boundary(leaf.scope_id);
-
-            let scope = &self.scopes[scope_id];
-            let arena = scope.current_frame();
-
-            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::Ready(template) = ret {
-                let mutations_ref = &mut fiber.mutations.borrow_mut();
-                let mutations = &mut **mutations_ref;
-                let template: &VNode = unsafe { std::mem::transmute(template) };
-                let mutations: &mut Mutations = unsafe { std::mem::transmute(mutations) };
-
-                std::mem::swap(&mut self.mutations, mutations);
-
-                let place_holder_id = scope.placeholder.get().unwrap();
-                self.scope_stack.push(scope_id);
-
-                drop(leaves);
-
-                let created = self.create(template);
-                self.scope_stack.pop();
-                mutations.push(Mutation::ReplaceWith {
-                    id: place_holder_id,
-                    m: created,
-                });
-
-                for leaf in self.collected_leaves.drain(..) {
-                    fiber.waiting_on.borrow_mut().insert(leaf);
-                }
-
-                std::mem::swap(&mut self.mutations, mutations);
-
-                if fiber.waiting_on.borrow().is_empty() {
-                    self.finished_fibers.push(fiber.id);
-                }
-            }
-        }
-    }
 }

+ 12 - 65
packages/core/src/scope_arena.rs

@@ -2,18 +2,10 @@ use crate::{
     any_props::AnyProps,
     bump_frame::BumpFrame,
     innerlude::DirtyScope,
-    innerlude::{SuspenseHandle, SuspenseId, SuspenseLeaf},
     nodes::RenderReturn,
     scopes::{ScopeId, ScopeState},
     virtual_dom::VirtualDom,
 };
-use futures_util::FutureExt;
-use std::{
-    mem,
-    pin::Pin,
-    sync::Arc,
-    task::{Context, Poll},
-};
 
 impl VirtualDom {
     pub(super) fn new_scope(
@@ -33,10 +25,10 @@ impl VirtualDom {
             name,
             props: Some(props),
             tasks: self.scheduler.clone(),
-            placeholder: Default::default(),
             node_arena_1: BumpFrame::new(0),
             node_arena_2: BumpFrame::new(0),
             spawned_tasks: Default::default(),
+            suspended: Default::default(),
             render_cnt: Default::default(),
             hooks: Default::default(),
             hook_idx: Default::default(),
@@ -58,74 +50,21 @@ impl VirtualDom {
         // Remove all the outdated listeners
         self.ensure_drop_safety(scope_id);
 
-        let mut new_nodes = unsafe {
+        let new_nodes = unsafe {
             self.scopes[scope_id].previous_frame().bump_mut().reset();
 
             let scope = &self.scopes[scope_id];
+            scope.suspended.set(false);
 
             scope.hook_idx.set(0);
 
             // 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);
+            let props: &dyn AnyProps = std::mem::transmute(props);
 
             props.render(scope).extend_lifetime()
         };
 
-        // immediately resolve futures that can be resolved
-        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 = SuspenseLeaf {
-                scope_id,
-                task: task.as_mut(),
-                notified: Default::default(),
-                waker: futures_util::task::waker(Arc::new(SuspenseHandle {
-                    id: suspense_id,
-                    tx: self.scheduler.sender.clone(),
-                })),
-            };
-
-            let mut cx = Context::from_waker(&leaf.waker);
-
-            // safety: the task is already pinned in the bump arena
-            let mut pinned = unsafe { Pin::new_unchecked(task.as_mut()) };
-
-            // Keep polling until either we get a value or the future is not ready
-            loop {
-                match pinned.poll_unpin(&mut cx) {
-                    // If nodes are produced, then set it and we can break
-                    Poll::Ready(nodes) => {
-                        new_nodes = match nodes {
-                            Some(nodes) => RenderReturn::Ready(nodes),
-                            None => RenderReturn::default(),
-                        };
-
-                        break;
-                    }
-
-                    // If no nodes are produced but the future woke up immediately, then try polling it again
-                    // This circumvents things like yield_now, but is important is important when rendering
-                    // components that are just a stream of immediately ready futures
-                    _ if leaf.notified.get() => {
-                        leaf.notified.set(false);
-                        continue;
-                    }
-
-                    // If no nodes are produced, then we need to wait for the future to be woken up
-                    // Insert the future into fiber leaves and break
-                    _ => {
-                        entry.insert(leaf);
-                        self.collected_leaves.push(suspense_id);
-                        break;
-                    }
-                };
-            }
-        };
-
         let scope = &self.scopes[scope_id];
 
         // We write on top of the previous frame and then make it the current by pushing the generation forward
@@ -144,6 +83,14 @@ impl VirtualDom {
             id: scope.id,
         });
 
+        if matches!(allocated, RenderReturn::Aborted(_)) {
+            if scope.suspended.get() {
+                self.suspended_scopes.insert(scope.id);
+            } else if !self.suspended_scopes.is_empty() {
+                _ = self.suspended_scopes.remove(&scope.id);
+            }
+        }
+
         // rebind the lifetime now that its stored internally
         unsafe { allocated.extend_lifetime_ref() }
     }

+ 10 - 5
packages/core/src/scopes.rs

@@ -1,12 +1,11 @@
 use crate::{
     any_props::AnyProps,
     any_props::VProps,
-    arena::ElementId,
     bump_frame::BumpFrame,
     innerlude::{DynamicNode, EventHandler, VComponent, VText},
     innerlude::{ErrorBoundary, Scheduler, SchedulerMsg},
     lazynodes::LazyNodes,
-    nodes::{ComponentReturn, IntoAttributeValue, IntoDynNode, RenderReturn},
+    nodes::{IntoAttributeValue, IntoDynNode, RenderReturn},
     AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId,
 };
 use bumpalo::{boxed::Box as BumpBox, Bump};
@@ -169,6 +168,7 @@ pub struct ScopeState {
     pub(crate) id: ScopeId,
 
     pub(crate) height: u32,
+    pub(crate) suspended: Cell<bool>,
 
     pub(crate) hooks: RefCell<Vec<Box<UnsafeCell<dyn Any>>>>,
     pub(crate) hook_idx: Cell<usize>,
@@ -182,7 +182,6 @@ pub struct ScopeState {
     pub(crate) attributes_to_drop: RefCell<Vec<*const Attribute<'static>>>,
 
     pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
-    pub(crate) placeholder: Cell<Option<ElementId>>,
 }
 
 impl<'src> ScopeState {
@@ -574,9 +573,9 @@ impl<'src> ScopeState {
     /// fn(Scope<Props>) -> Element;
     /// async fn(Scope<Props<'_>>) -> Element;
     /// ```
-    pub fn component<P, A, F: ComponentReturn<'src, A>>(
+    pub fn component<P>(
         &'src self,
-        component: fn(Scope<'src, P>) -> F,
+        component: fn(Scope<'src, P>) -> Element<'src>,
         props: P,
         fn_name: &'static str,
     ) -> DynamicNode<'src>
@@ -655,6 +654,12 @@ impl<'src> ScopeState {
         None
     }
 
+    /// Mark this component as suspended and then return None
+    pub fn suspend(&self) -> Option<Element> {
+        self.suspended.set(true);
+        None
+    }
+
     /// Store a value between renders. The foundational hook for all other hooks.
     ///
     /// Accepts an `initializer` closure, which is run on the first use of the hook (typically the initial render). The return value of this closure is stored for the lifetime of the component, and a mutable reference to it is provided on every render as the return value of `use_hook`.

+ 22 - 95
packages/core/src/virtual_dom.rs

@@ -9,14 +9,13 @@ use crate::{
     mutations::Mutation,
     nodes::RenderReturn,
     nodes::{Template, TemplateId},
-    scheduler::SuspenseId,
     scopes::{ScopeId, ScopeState},
-    AttributeValue, Element, Event, Scope, SuspenseContext,
+    AttributeValue, Element, Event, Scope,
 };
 use futures_util::{pin_mut, StreamExt};
-use rustc_hash::FxHashMap;
+use rustc_hash::{FxHashMap, FxHashSet};
 use slab::Slab;
-use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
+use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
 
 /// A virtual node system that progresses user events and diffs UI trees.
 ///
@@ -186,11 +185,9 @@ pub struct VirtualDom {
 
     // While diffing we need some sort of way of breaking off a stream of suspended mutations.
     pub(crate) scope_stack: Vec<ScopeId>,
-    pub(crate) collected_leaves: Vec<SuspenseId>,
 
-    // Whenever a suspense tree is finished, we push its boundary onto this stack.
-    // When "render_with_deadline" is called, we pop the stack and return the mutations
-    pub(crate) finished_fibers: Vec<ScopeId>,
+    // Currently suspended scopes
+    pub(crate) suspended_scopes: FxHashSet<ScopeId>,
 
     pub(crate) rx: futures_channel::mpsc::UnboundedReceiver<SchedulerMsg>,
 
@@ -262,8 +259,7 @@ impl VirtualDom {
             elements: Default::default(),
             scope_stack: Vec::new(),
             dirty_scopes: BTreeSet::new(),
-            collected_leaves: Vec::new(),
-            finished_fibers: Vec::new(),
+            suspended_scopes: FxHashSet::default(),
             mutations: Mutations::default(),
         };
 
@@ -272,12 +268,6 @@ impl VirtualDom {
             "app",
         );
 
-        // The root component is always a suspense boundary for any async children
-        // This could be unexpected, so we might rethink this behavior later
-        //
-        // We *could* just panic if the suspense boundary is not found
-        root.provide_context(Rc::new(SuspenseContext::new(ScopeId(0))));
-
         // Unlike react, we provide a default error boundary that just renders the error as a string
         root.provide_context(Rc::new(ErrorBoundary::new(ScopeId(0))));
 
@@ -319,25 +309,6 @@ impl VirtualDom {
         }
     }
 
-    /// Determine whether or not a scope is currently in a suspended state
-    ///
-    /// This does not mean the scope is waiting on its own futures, just that the tree that the scope exists in is
-    /// currently suspended.
-    pub fn is_scope_suspended(&self, id: ScopeId) -> bool {
-        !self.scopes[id]
-            .consume_context::<Rc<SuspenseContext>>()
-            .unwrap()
-            .waiting_on
-            .borrow()
-            .is_empty()
-    }
-
-    /// Determine if the tree is at all suspended. Used by SSR and other outside mechanisms to determine if the tree is
-    /// ready to be rendered.
-    pub fn has_suspended_work(&self) -> bool {
-        !self.scheduler.leaves.borrow().is_empty()
-    }
-
     /// Call a listener inside the VirtualDom with data from outside the VirtualDom.
     ///
     /// This method will identify the appropriate element. The data must match up with the listener delcared. Note that
@@ -485,7 +456,6 @@ impl VirtualDom {
                 Some(msg) => match msg {
                     SchedulerMsg::Immediate(id) => self.mark_dirty(id),
                     SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task),
-                    SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id),
                 },
 
                 // If they're not ready, then we should wait for them to be ready
@@ -495,7 +465,7 @@ impl VirtualDom {
                         Ok(None) => return,
                         Err(_) => {
                             // If we have any dirty scopes, or finished fiber trees then we should exit
-                            if !self.dirty_scopes.is_empty() || !self.finished_fibers.is_empty() {
+                            if !self.dirty_scopes.is_empty() || !self.suspended_scopes.is_empty() {
                                 return;
                             }
 
@@ -513,7 +483,6 @@ impl VirtualDom {
             match msg {
                 SchedulerMsg::Immediate(id) => self.mark_dirty(id),
                 SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task),
-                SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id),
             }
         }
     }
@@ -574,7 +543,6 @@ impl VirtualDom {
             }
             // If an error occurs, we should try to render the default error component and context where the error occured
             RenderReturn::Aborted(_placeholder) => panic!("Cannot catch errors during rebuild"),
-            RenderReturn::Pending(_) => unreachable!("Root scope cannot be an async component"),
         }
 
         self.finalize()
@@ -598,6 +566,21 @@ impl VirtualDom {
         }
     }
 
+    /// Render the virtual dom, waiting for all suspense to be finished
+    ///
+    /// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content
+    pub async fn wait_for_suspense(&mut self) {
+        loop {
+            if self.suspended_scopes.is_empty() {
+                return;
+            }
+
+            self.wait_for_work().await;
+
+            _ = self.render_immediate();
+        }
+    }
+
     /// Render what you can given the timeline and then move on
     ///
     /// It's generally a good idea to put some sort of limit on the suspense process in case a future is having issues.
@@ -609,26 +592,6 @@ impl VirtualDom {
         self.process_events();
 
         loop {
-            // first, unload any complete suspense trees
-            for finished_fiber in self.finished_fibers.drain(..) {
-                let scope = &self.scopes[finished_fiber];
-                let context = scope.has_context::<Rc<SuspenseContext>>().unwrap();
-
-                self.mutations
-                    .templates
-                    .append(&mut context.mutations.borrow_mut().templates);
-
-                self.mutations
-                    .edits
-                    .append(&mut context.mutations.borrow_mut().edits);
-
-                // TODO: count how many nodes are on the stack?
-                self.mutations.push(Mutation::ReplaceWith {
-                    id: context.placeholder.get().unwrap(),
-                    m: 1,
-                })
-            }
-
             // Next, diff any dirty scopes
             // We choose not to poll the deadline since we complete pretty quickly anyways
             if let Some(dirty) = self.dirty_scopes.iter().next().cloned() {
@@ -639,40 +602,9 @@ impl VirtualDom {
                     continue;
                 }
 
-                // if the scope is currently suspended, then we should skip it, ignoring any tasks calling for an update
-                if self.is_scope_suspended(dirty.id) {
-                    continue;
-                }
-
-                // Save the current mutations length so we can split them into boundary
-                let mutations_to_this_point = self.mutations.edits.len();
-
                 // Run the scope and get the mutations
                 self.run_scope(dirty.id);
                 self.diff_scope(dirty.id);
-
-                // If suspended leaves are present, then we should find the boundary for this scope and attach things
-                // No placeholder necessary since this is a diff
-                if !self.collected_leaves.is_empty() {
-                    let mut boundary = self.scopes[dirty.id]
-                        .consume_context::<Rc<SuspenseContext>>()
-                        .unwrap();
-
-                    let boundary_mut = boundary.borrow_mut();
-
-                    // Attach mutations
-                    boundary_mut
-                        .mutations
-                        .borrow_mut()
-                        .edits
-                        .extend(self.mutations.edits.split_off(mutations_to_this_point));
-
-                    // Attach suspended leaves
-                    boundary
-                        .waiting_on
-                        .borrow_mut()
-                        .extend(self.collected_leaves.drain(..));
-                }
             }
 
             // If there's more work, then just continue, plenty of work to do
@@ -680,11 +612,6 @@ impl VirtualDom {
                 continue;
             }
 
-            // If there's no pending suspense, then we have no reason to wait for anything
-            if self.scheduler.leaves.borrow().is_empty() {
-                return self.finalize();
-            }
-
             // Poll the suspense leaves in the meantime
             let mut work = self.wait_for_work();
 

+ 0 - 7
packages/core/tests/safety.rs

@@ -1,9 +1,6 @@
 //! Tests related to safety of the library.
 
-use std::rc::Rc;
-
 use dioxus::prelude::*;
-use dioxus_core::SuspenseContext;
 
 /// Ensure no issues with not calling rebuild
 #[test]
@@ -17,8 +14,4 @@ fn root_node_isnt_null() {
 
     // The height should be 0
     assert_eq!(scope.height(), 0);
-
-    // There should be a default suspense context
-    // todo: there should also be a default error boundary
-    assert!(scope.has_context::<Rc<SuspenseContext>>().is_some());
 }

+ 20 - 82
packages/core/tests/suspense.rs

@@ -1,100 +1,38 @@
-use dioxus::core::ElementId;
-use dioxus::core::{Mutation::*, SuspenseContext};
 use dioxus::prelude::*;
-use std::future::IntoFuture;
-use std::rc::Rc;
-use std::time::Duration;
 
-#[test]
-fn it_works() {
+#[tokio::test]
+async fn it_works() {
     // wait just a moment, not enough time for the boundary to resolve
 
-    tokio::runtime::Builder::new_current_thread()
-        .enable_time()
-        .build()
-        .unwrap()
-        .block_on(async {
-            let mut dom = VirtualDom::new(app);
+    let mut dom = VirtualDom::new(app);
+    _ = dom.rebuild();
+    dom.wait_for_suspense().await;
+    let out = dioxus_ssr::pre_render(&dom);
 
-            {
-                let mutations = dom.rebuild().santize();
+    assert_eq!(out, "<div>Waiting for... child</div>");
 
-                // We should at least get the top-level template in before pausing for the children
-                // note: we dont test template edits anymore
-                // assert_eq!(
-                //     mutations.templates,
-                //     [
-                //         CreateElement { name: "div" },
-                //         CreateStaticText { value: "Waiting for child..." },
-                //         CreateStaticPlaceholder,
-                //         AppendChildren { m: 2 },
-                //         SaveTemplate { name: "template", m: 1 }
-                //     ]
-                // );
-
-                // And we should load it in and assign the placeholder properly
-                assert_eq!(
-                    mutations.edits,
-                    [
-                        LoadTemplate { name: "template", index: 0, id: ElementId(1) },
-                        // hmmmmmmmmm.... with suspense how do we guarantee that IDs increase linearly?
-                        // can we even?
-                        AssignId { path: &[1], id: ElementId(3) },
-                        AppendChildren { m: 1, id: ElementId(0) },
-                    ]
-                );
-            }
-
-            dom.wait_for_work().await;
-        });
+    dbg!(out);
 }
 
 fn app(cx: Scope) -> Element {
     cx.render(rsx!(
         div {
-            "Waiting for child..."
-            suspense_boundary {}
+            "Waiting for... "
+            suspended_child {}
         }
     ))
 }
 
-fn suspense_boundary(cx: Scope) -> Element {
-    cx.use_hook(|| {
-        cx.provide_context(Rc::new(SuspenseContext::new(cx.scope_id())));
-    });
-
-    // Ensure the right types are found
-    cx.has_context::<Rc<SuspenseContext>>().unwrap();
-
-    cx.render(rsx!(async_child {}))
-}
-
-async fn async_child(cx: Scope<'_>) -> Element {
-    use_future!(cx, || tokio::time::sleep(Duration::from_millis(10))).await;
-    cx.render(rsx!(async_text {}))
-}
-
-async fn async_text(cx: Scope<'_>) -> Element {
-    let username = use_future!(cx, || async {
-        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
-        "async child 1"
-    });
+fn suspended_child(cx: Scope) -> Element {
+    let val = use_state(cx, || 0);
 
-    let age = use_future!(cx, || async {
-        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
-        1234
-    });
-
-    let (_user, _age) = use_future!(cx, || async {
-        tokio::join!(
-            tokio::time::sleep(std::time::Duration::from_secs(1)),
-            tokio::time::sleep(std::time::Duration::from_secs(2))
-        );
-        ("async child 1", 1234)
-    })
-    .await;
-
-    let (username, age) = tokio::join!(username.into_future(), age.into_future());
+    if **val < 3 {
+        let mut val = val.clone();
+        cx.spawn(async move {
+            val += 1;
+        });
+        return cx.suspend()?;
+    }
 
-    cx.render(rsx!( div { "Hello! {username}, you are {age}, {_user} {_age}" } ))
+    render!("child")
 }