Browse Source

feat: suspense!

Jonathan Kelley 2 years ago
parent
commit
203935834d

+ 3 - 35
packages/core/src/create.rs

@@ -188,7 +188,6 @@ impl VirtualDom {
 
                 let render_ret = self.run_scope(id);
 
-                // shut up about lifetimes please, I know what I'm doing
                 let render_ret: &mut RenderReturn = unsafe { std::mem::transmute(render_ret) };
 
                 match render_ret {
@@ -204,53 +203,22 @@ impl VirtualDom {
                     RenderReturn::Sync(None) => {
                         let new_id = self.next_element(template);
                         placeholder.set(Some(new_id));
+                        self.scopes[id.0].placeholder.set(Some(new_id));
                         mutations.push(AssignId {
                             id: new_id,
                             path: &template.template.node_paths[idx][1..],
                         });
                         0
                     }
+
                     RenderReturn::Async(fut) => {
                         let new_id = self.next_element(template);
-
-                        let scope = self.scope_stack.last().unwrap();
-                        let scope = &self.scopes[scope.0];
-
-                        let boundary = scope.consume_context::<SuspenseContext>().unwrap();
-
-                        // try to poll the future once - many times it will be ready immediately or require little to no work
-
-                        todo!();
-
-                        // // move up the tree looking for the first suspense boundary
-                        // // our current component can not be a suspense boundary, so we skip it
-                        // for scope_id in self.scope_stack.iter().rev().skip(1) {
-                        //     if let Some(fiber) = &mut scope.suspense_boundary {
-                        //         // save the fiber leaf onto the fiber itself
-                        //         let detached: &mut FiberLeaf<'static> =
-                        //             unsafe { std::mem::transmute(fut) };
-
-                        //         // And save the fiber leaf using the placeholder node
-                        //         // this way, when we resume the fiber, we just need to "pick up placeholder"
-                        //         fiber.futures.insert(
-                        //             LeafLocation {
-                        //                 element: new_id,
-                        //                 scope: *scope_id,
-                        //             },
-                        //             detached,
-                        //         );
-
-                        //         self.suspended_scopes.insert(*scope_id);
-                        //         break;
-
-                        // }
-
                         placeholder.set(Some(new_id));
+                        self.scopes[id.0].placeholder.set(Some(new_id));
                         mutations.push(AssignId {
                             id: new_id,
                             path: &template.template.node_paths[idx][1..],
                         });
-
                         0
                     }
                 }

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

@@ -119,7 +119,7 @@ where
     fn as_return(self, cx: &'a ScopeState) -> RenderReturn<'a> {
         let f: &mut dyn Future<Output = Element<'a>> = cx.bump().alloc(self);
         let boxed = unsafe { BumpBox::from_raw(f) };
-        let pined: Pin<BumpBox<_>> = boxed.into();
+        let pined: BumpBox<_> = boxed.into();
         RenderReturn::Async(pined)
     }
 }
@@ -133,7 +133,7 @@ fn takes_it() {
 
 pub enum RenderReturn<'a> {
     Sync(Element<'a>),
-    Async(Pin<BumpBox<'a, dyn Future<Output = Element<'a>> + 'a>>),
+    Async(BumpBox<'a, dyn Future<Output = Element<'a>> + 'a>),
 }
 
 pub type FiberLeaf<'a> = Pin<BumpBox<'a, dyn Future<Output = Element<'a>> + 'a>>;

+ 27 - 31
packages/core/src/nodes.rs

@@ -47,6 +47,27 @@ impl<'a> VNode<'a> {
             },
         })
     }
+
+    pub fn single_text(
+        cx: &'a ScopeState,
+        text: &'static [TemplateNode<'static>],
+        id: &'static str,
+    ) -> Option<Self> {
+        Some(VNode {
+            node_id: Cell::new(ElementId(0)),
+            key: None,
+            parent: None,
+            root_ids: &[],
+            dynamic_nodes: &[],
+            dynamic_attrs: &[],
+            template: Template {
+                id,
+                roots: text,
+                node_paths: &[&[0]],
+                attr_paths: &[],
+            },
+        })
+    }
 }
 
 #[derive(Debug, Clone, Copy)]
@@ -62,25 +83,13 @@ impl<'a> std::hash::Hash for Template<'a> {
         self.id.hash(state);
     }
 }
-
+impl Eq for Template<'_> {}
 impl PartialEq for Template<'_> {
     fn eq(&self, other: &Self) -> bool {
         self.id == other.id
     }
 }
 
-impl Eq for Template<'_> {}
-impl PartialOrd for Template<'_> {
-    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
-        self.id.partial_cmp(other.id)
-    }
-}
-impl Ord for Template<'_> {
-    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
-        self.id.cmp(other.id)
-    }
-}
-
 /// A weird-ish variant of VNodes with way more limited types
 #[derive(Debug, Clone, Copy)]
 pub enum TemplateNode<'a> {
@@ -96,24 +105,17 @@ pub enum TemplateNode<'a> {
 }
 
 pub enum DynamicNode<'a> {
-    // Anything declared in component form
-    // IE in caps or with underscores
     Component {
         name: &'static str,
         static_props: bool,
         props: Cell<*mut dyn AnyProps<'a>>,
         placeholder: Cell<Option<ElementId>>,
     },
-
-    // Comes in with string interpolation or from format_args, include_str, etc
     Text {
         id: Cell<ElementId>,
         value: &'a str,
     },
-
-    // Anything that's coming in as an iterator
     Fragment(&'a [VNode<'a>]),
-
     Placeholder(Cell<ElementId>),
 }
 
@@ -153,8 +155,8 @@ impl<'a> std::fmt::Debug for AttributeValue<'a> {
             Self::Float(arg0) => f.debug_tuple("Float").field(arg0).finish(),
             Self::Int(arg0) => f.debug_tuple("Int").field(arg0).finish(),
             Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(),
-            Self::Listener(arg0) => f.debug_tuple("Listener").finish(),
-            Self::Any(arg0) => f.debug_tuple("Any").finish(),
+            Self::Listener(_) => f.debug_tuple("Listener").finish(),
+            Self::Any(_) => f.debug_tuple("Any").finish(),
             Self::None => write!(f, "None"),
         }
     }
@@ -167,7 +169,7 @@ impl<'a> PartialEq for AttributeValue<'a> {
             (Self::Float(l0), Self::Float(r0)) => l0 == r0,
             (Self::Int(l0), Self::Int(r0)) => l0 == r0,
             (Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
-            (Self::Listener(l0), Self::Listener(r0)) => true,
+            (Self::Listener(_), Self::Listener(_)) => true,
             (Self::Any(l0), Self::Any(r0)) => l0.any_cmp(*r0),
             _ => core::mem::discriminant(self) == core::mem::discriminant(other),
         }
@@ -186,20 +188,14 @@ impl<'a> AttributeValue<'a> {
             _ => return false,
         }
     }
-
-    fn is_listener(&self) -> bool {
-        matches!(self, AttributeValue::Listener(_))
-    }
 }
 
 pub trait AnyValue {
     fn any_cmp(&self, other: &dyn AnyValue) -> bool;
     fn our_typeid(&self) -> TypeId;
 }
-impl<T> AnyValue for T
-where
-    T: PartialEq + Any,
-{
+
+impl<T: PartialEq + Any> AnyValue for T {
     fn any_cmp(&self, other: &dyn AnyValue) -> bool {
         if self.type_id() != other.our_typeid() {
             return false;

+ 1 - 0
packages/core/src/scheduler/bumpslab.rs

@@ -0,0 +1 @@
+

+ 3 - 1
packages/core/src/scheduler/mod.rs

@@ -1,7 +1,9 @@
+use slab::Slab;
 use std::sync::Arc;
 
 use crate::ScopeId;
 
+mod bumpslab;
 mod handle;
 mod suspense;
 mod task;
@@ -9,9 +11,9 @@ mod wait;
 mod waker;
 
 pub use handle::*;
-use slab::Slab;
 pub use suspense::*;
 pub use task::*;
+pub use waker::RcWake;
 
 /// The type of message that can be sent to the scheduler.
 ///

+ 25 - 42
packages/core/src/scheduler/suspense.rs

@@ -1,21 +1,24 @@
-use std::{collections::HashSet, rc::Rc};
-
-use futures_task::{RawWaker, RawWakerVTable, Waker};
+use std::{
+    cell::{Cell, RefCell},
+    collections::HashSet,
+    pin::Pin,
+    rc::Rc,
+};
+
+use super::{waker::RcWake, SchedulerMsg};
+use crate::{innerlude::Mutation, Element, ScopeId};
+use futures_task::Waker;
 use futures_util::Future;
 
-use crate::{innerlude::Mutation, Element, Scope, ScopeId};
-
-use super::SchedulerMsg;
-
 #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
 pub struct SuspenseId(pub usize);
 
-pub type SuspenseContext = Rc<SuspenseBoundary>;
+pub type SuspenseContext = Rc<RefCell<SuspenseBoundary>>;
 /// Essentially a fiber in React
 pub struct SuspenseBoundary {
     pub id: ScopeId,
-    waiting_on: HashSet<SuspenseId>,
-    mutations: Vec<Mutation<'static>>,
+    pub waiting_on: HashSet<SuspenseId>,
+    pub mutations: Vec<Mutation<'static>>,
 }
 
 impl SuspenseBoundary {
@@ -28,44 +31,24 @@ impl SuspenseBoundary {
     }
 }
 
-/*
-
-
-many times the future will be ready every time it's polled, so we can spin on it until it doesnt wake us up immediately
-
-
-*/
 pub struct SuspenseLeaf {
     pub id: SuspenseId,
-    pub scope: ScopeId,
+    pub scope_id: ScopeId,
     pub boundary: ScopeId,
     pub tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
+    pub notified: Cell<bool>,
 
     pub task: *mut dyn Future<Output = Element<'static>>,
 }
 
-pub fn make_suspense_waker(task: &SuspenseLeaf) -> Waker {
-    let raw = RawWaker::new(task as *const SuspenseLeaf as *const _, task_vtable());
-    unsafe { Waker::from_raw(raw) }
-}
-
-fn task_vtable() -> &'static RawWakerVTable {
-    &RawWakerVTable::new(clone, wake, wake_by_ref, drop_task)
-}
-
-unsafe fn clone(data: *const ()) -> RawWaker {
-    RawWaker::new(data as *const (), task_vtable())
-}
-unsafe fn wake(data: *const ()) {
-    wake_by_ref(data);
-}
-unsafe fn wake_by_ref(data: *const ()) {
-    let task = &*(data as *const SuspenseLeaf);
-    task.tx
-        .unbounded_send(SchedulerMsg::SuspenseNotified(task.id))
-        .expect("Scheduler should exist");
-}
-
-unsafe fn drop_task(_data: *const ()) {
-    // doesnt do anything
+impl RcWake for SuspenseLeaf {
+    fn wake_by_ref(arc_self: &Rc<Self>) {
+        if arc_self.notified.get() {
+            return;
+        }
+        arc_self.notified.set(true);
+        _ = arc_self
+            .tx
+            .unbounded_send(SchedulerMsg::SuspenseNotified(arc_self.id));
+    }
 }

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

@@ -61,11 +61,6 @@ impl HandleInner {
     }
 }
 
-pub fn make_task_waker(task: Rc<LocalTask>) -> Waker {
-    let ptr = Rc::into_raw(task).cast::<()>();
-    super::waker::make_rc_waker(task)
-}
-
 impl RcWake for LocalTask {
     fn wake_by_ref(arc_self: &Rc<Self>) {
         _ = arc_self

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

@@ -1,9 +1,13 @@
 use futures_task::Context;
 use futures_util::{FutureExt, StreamExt};
 
-use crate::{innerlude::make_task_waker, VirtualDom};
+use crate::{
+    factory::RenderReturn,
+    innerlude::{Mutation, SuspenseContext},
+    VNode, VirtualDom,
+};
 
-use super::SchedulerMsg;
+use super::{waker::RcWake, SchedulerMsg, SuspenseLeaf};
 
 impl VirtualDom {
     /// Wait for futures internal to the virtualdom
@@ -22,7 +26,7 @@ impl VirtualDom {
 
                     // attach the waker to itself
                     // todo: don't make a new waker every time, make it once and then just clone it
-                    let waker = make_task_waker(local_task.clone());
+                    let waker = local_task.waker();
                     let mut cx = Context::from_waker(&waker);
 
                     // safety: the waker owns its task and everythig is single threaded
@@ -33,7 +37,57 @@ impl VirtualDom {
                     }
                 }
 
-                SchedulerMsg::SuspenseNotified(_) => todo!(),
+                SchedulerMsg::SuspenseNotified(id) => {
+                    let leaf = self
+                        .scheduler
+                        .handle
+                        .leaves
+                        .borrow_mut()
+                        .get(id.0)
+                        .unwrap()
+                        .clone();
+
+                    let scope_id = leaf.scope_id;
+
+                    // todo: cache the waker
+                    let waker = leaf.waker();
+                    let mut cx = Context::from_waker(&waker);
+
+                    let fut = unsafe { &mut *leaf.task };
+
+                    let mut pinned = unsafe { std::pin::Pin::new_unchecked(fut) };
+                    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 futures_task::Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx)
+                    {
+                        let boundary = &self.scopes[leaf.boundary.0]
+                            .consume_context::<SuspenseContext>()
+                            .unwrap();
+
+                        let mut fiber = boundary.borrow_mut();
+                        let scope = &mut self.scopes[scope_id.0];
+                        let arena = scope.current_arena();
+
+                        let ret = arena.bump.alloc(RenderReturn::Sync(new_nodes));
+                        arena.node.set(ret);
+
+                        if let RenderReturn::Sync(Some(template)) = ret {
+                            let mutations = &mut fiber.mutations;
+                            let template: &VNode = unsafe { std::mem::transmute(template) };
+                            let mutations: &mut Vec<Mutation> =
+                                unsafe { std::mem::transmute(mutations) };
+
+                            self.scope_stack.push(scope_id);
+                            self.create(mutations, template);
+                            self.scope_stack.pop();
+
+                            println!("{:?}", mutations);
+                        }
+                    }
+                }
             }
         }
     }

+ 30 - 48
packages/core/src/scheduler/waker.rs

@@ -1,54 +1,36 @@
-use std::{
-    cell::{RefCell, UnsafeCell},
-    marker::PhantomData,
-    mem::{self, MaybeUninit},
-    ops::DerefMut,
-    pin::Pin,
-    process::Output,
-    rc::Rc,
-    sync::Arc,
-};
+use futures_task::{RawWaker, RawWakerVTable, Waker};
+use std::{mem, rc::Rc};
+
+pub trait RcWake: Sized {
+    /// Create a waker from this self-wakening object
+    fn waker(self: &Rc<Self>) -> Waker {
+        unsafe fn rc_vtable<T: RcWake>() -> &'static RawWakerVTable {
+            &RawWakerVTable::new(
+                |data| {
+                    let arc = mem::ManuallyDrop::new(Rc::<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| {
+                    let arc = mem::ManuallyDrop::new(Rc::<T>::from_raw(data.cast::<T>()));
+                    RcWake::wake_by_ref(&arc);
+                },
+                |data| drop(Rc::<T>::from_raw(data.cast::<T>())),
+            )
+        }
+
+        unsafe {
+            Waker::from_raw(RawWaker::new(
+                Rc::into_raw(self.clone()).cast(),
+                rc_vtable::<Self>(),
+            ))
+        }
+    }
 
-use futures_task::{waker, RawWaker, RawWakerVTable, Waker};
+    fn wake_by_ref(arc_self: &Rc<Self>);
 
-pub trait RcWake {
     fn wake(self: Rc<Self>) {
         Self::wake_by_ref(&self)
     }
-    fn wake_by_ref(arc_self: &Rc<Self>);
-}
-
-pub fn make_rc_waker<T: RcWake>(rc: Rc<T>) -> Waker {
-    unsafe { Waker::from_raw(RawWaker::new(Rc::into_raw(rc).cast(), rc_vtable::<T>())) }
-}
-
-fn rc_vtable<T: RcWake>() -> &'static RawWakerVTable {
-    &RawWakerVTable::new(
-        clone_rc_raw::<T>,
-        wake_rc_raw::<T>,
-        wake_by_ref_rc_raw::<T>,
-        drop_rc_raw::<T>,
-    )
-}
-
-// FIXME: panics on Rc::clone / refcount changes could wreak havoc on the
-// code here. We should guard against this by aborting.
-unsafe fn clone_rc_raw<T: RcWake>(data: *const ()) -> RawWaker {
-    let arc = mem::ManuallyDrop::new(Rc::<T>::from_raw(data.cast::<T>()));
-    let _rc_clone: mem::ManuallyDrop<_> = arc.clone();
-    RawWaker::new(data, rc_vtable::<T>())
-}
-
-unsafe fn wake_rc_raw<T: RcWake>(data: *const ()) {
-    let arc: Rc<T> = Rc::from_raw(data.cast::<T>());
-    arc.wake();
-}
-
-unsafe fn wake_by_ref_rc_raw<T: RcWake>(data: *const ()) {
-    let arc = mem::ManuallyDrop::new(Rc::<T>::from_raw(data.cast::<T>()));
-    arc.wake();
-}
-
-unsafe fn drop_rc_raw<T: RcWake>(data: *const ()) {
-    drop(Rc::<T>::from_raw(data.cast::<T>()))
 }

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

@@ -1,14 +1,20 @@
-use std::task::Context;
-
-use futures_util::task::noop_waker_ref;
+use crate::{innerlude::SuspenseContext, scheduler::RcWake};
+use futures_util::{pin_mut, task::noop_waker_ref};
+use std::{
+    mem,
+    pin::Pin,
+    rc::Rc,
+    task::{Context, Poll},
+};
 
 use crate::{
     any_props::AnyProps,
     arena::ElementId,
     bump_frame::BumpFrame,
     factory::RenderReturn,
+    innerlude::{SuspenseId, SuspenseLeaf},
     scopes::{ScopeId, ScopeState},
-    virtualdom::VirtualDom, innerlude::SuspenseLeaf,
+    virtualdom::VirtualDom,
 };
 
 impl VirtualDom {
@@ -25,6 +31,7 @@ impl VirtualDom {
             id,
             height,
             props,
+            placeholder: None.into(),
             node_arena_1: BumpFrame::new(50),
             node_arena_2: BumpFrame::new(50),
             render_cnt: Default::default(),
@@ -52,46 +59,66 @@ impl VirtualDom {
             .and_then(|id| self.scopes.get_mut(id.0).map(|f| f as *mut ScopeState))
     }
 
-    pub fn run_scope(&mut self, id: ScopeId) -> &mut RenderReturn {
-        let scope = &mut self.scopes[id.0];
-        scope.hook_idx.set(0);
+    pub fn run_scope(&mut self, scope_id: ScopeId) -> &mut RenderReturn {
+        let mut new_nodes = unsafe {
+            let scope = &mut self.scopes[scope_id.0];
+            scope.hook_idx.set(0);
 
-        let mut new_nodes = {
-            let props = unsafe { &mut *scope.props };
-            let props: &mut dyn AnyProps = unsafe { std::mem::transmute(props) };
+            let props: &mut dyn AnyProps = mem::transmute(&mut *scope.props);
             let res: RenderReturn = props.render(scope);
-            let res: RenderReturn<'static> = unsafe { std::mem::transmute(res) };
+            let res: RenderReturn<'static> = mem::transmute(res);
             res
         };
 
-        // immediately resolve futures that can be resolved immediatelys
-        let res = match &mut new_nodes {
-            RenderReturn::Sync(_) => new_nodes,
-            RenderReturn::Async(fut) => {
-                // use futures_util::FutureExt;
-
-                let leaves = self.scheduler.handle.leaves.borrow_mut();
-
-                leaves.insert(Rc::new(SuspenseLeaf {
-                    id: todo!(),
-                    scope: todo!(),
-                    boundary: todo!(),
-                    tx: todo!(),
-                    task: todo!(),
-                }));
-
-                let waker = crate::scheduler::make_suspense_waker(task);
-
-                match fut.poll_unpin(&mut cx) {
-                    std::task::Poll::Ready(nodes) => {
-                        leaves.remove(key;)
-                        RenderReturn::Sync(nodes)
-                    },
-                    std::task::Poll::Pending => new_nodes,
-                }
+        // immediately resolve futures that can be resolved
+        if let RenderReturn::Async(task) = &mut new_nodes {
+            use futures_util::FutureExt;
+
+            let mut leaves = self.scheduler.handle.leaves.borrow_mut();
+            let entry = leaves.vacant_entry();
+            let key = entry.key();
+
+            let leaf = Rc::new(SuspenseLeaf {
+                scope_id,
+                task: task.as_mut(),
+                id: SuspenseId(key),
+                tx: self.scheduler.handle.sender.clone(),
+                boundary: ScopeId(0),
+                notified: false.into(),
+            });
+
+            let _leaf = leaf.clone();
+            let waker = leaf.waker();
+            let mut cx = Context::from_waker(&waker);
+            let mut pinned = unsafe { Pin::new_unchecked(task.as_mut()) };
+
+            loop {
+                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);
+                        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);
+                        break;
+                    }
+                };
             }
         };
 
+        let scope = &mut self.scopes[scope_id.0];
         let frame = match scope.render_cnt % 2 {
             0 => &mut scope.node_arena_1,
             1 => &mut scope.node_arena_2,
@@ -99,10 +126,10 @@ impl VirtualDom {
         };
 
         // set the head of the bump frame
-        let alloced = frame.bump.alloc(res);
+        let alloced = frame.bump.alloc(new_nodes);
         frame.node.set(alloced);
 
         // rebind the lifetime now that its stored internally
-        unsafe { std::mem::transmute(alloced) }
+        unsafe { mem::transmute(alloced) }
     }
 }

+ 1 - 0
packages/core/src/scopes.rs

@@ -72,6 +72,7 @@ pub struct ScopeState {
     pub tasks: SchedulerHandle,
 
     pub props: *mut dyn AnyProps<'static>,
+    pub placeholder: Cell<Option<ElementId>>,
 }
 
 impl ScopeState {

+ 4 - 2
packages/core/src/virtualdom.rs

@@ -13,9 +13,11 @@ use crate::{
 };
 use crate::{scheduler, Element, Scope};
 use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
-use scheduler::SuspenseContext;
+use scheduler::{SuspenseBoundary, SuspenseContext};
 use slab::Slab;
+use std::cell::RefCell;
 use std::collections::{BTreeSet, HashMap};
+use std::rc::Rc;
 
 pub struct VirtualDom {
     pub(crate) templates: HashMap<TemplateId, Template<'static>>,
@@ -47,7 +49,7 @@ impl VirtualDom {
         let root = res.new_scope(props);
 
         // the root component is always a suspense boundary for any async children
-        // res.scopes[root.0].provide_context(SuspenseContext::new(root));
+        res.scopes[root.0].provide_context(Rc::new(RefCell::new(SuspenseBoundary::new(root))));
 
         assert_eq!(root, ScopeId(0));
 

+ 5 - 2
packages/core/tests/suspend.rs

@@ -32,7 +32,10 @@ async fn async_child(cx: Scope<'_>) -> Element {
 
     fut.await;
 
-    println!("Future awaited");
+    println!("Future awaited and complete");
 
-    None
+    let dy = cx.component(async_child, (), "async_child");
+    VNode::single_component(&cx, dy, "app")
+
+    // VNode::single_text(&cx, &[TemplateNode::Text("it works!")], "beauty")
 }