From cbd88bbcc3e15a9e8d6b0543874a6349434693e9 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 16:15:20 -0700 Subject: [PATCH 1/7] Rip out async components --- examples/dog_app.rs | 12 ++-- packages/core/src/any_props.rs | 28 ++++----- packages/core/src/create.rs | 19 ------ packages/core/src/diff.rs | 19 +----- packages/core/src/nodes.rs | 30 ---------- packages/core/src/properties.rs | 2 +- packages/core/src/scheduler/mod.rs | 7 --- packages/core/src/scheduler/suspense.rs | 20 ------- packages/core/src/scheduler/wait.rs | 79 +------------------------ packages/core/src/scope_arena.rs | 66 +-------------------- packages/core/src/scopes.rs | 6 +- packages/core/src/virtual_dom.rs | 11 +--- packages/core/tests/suspense.rs | 59 +++++++++--------- packages/hooks/src/usefuture.rs | 4 ++ 14 files changed, 62 insertions(+), 300 deletions(-) diff --git a/examples/dog_app.rs b/examples/dog_app.rs index 107251cd7..510550b99 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -10,7 +10,7 @@ struct ListBreeds { message: HashMap>, } -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.suspend()? { + 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.suspend()? { Ok(resp) => render! { div { button { diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index 7014a2771..e3a3bdaef 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -1,8 +1,8 @@ -use std::{marker::PhantomData, panic::AssertUnwindSafe}; +use std::{ panic::AssertUnwindSafe}; use crate::{ innerlude::Scoped, - nodes::{ComponentReturn, RenderReturn}, + nodes::RenderReturn, scopes::{Scope, ScopeState}, Element, }; @@ -18,19 +18,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, } -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 +34,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 +61,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(), } } } diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 80561e937..d31e5ab47 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -514,7 +514,6 @@ impl<'b> VirtualDom { 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), } } @@ -591,24 +590,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>, diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index a8b913440..c06e46d3f 100644 --- a/packages/core/src/diff.rs +++ b/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)); @@ -735,7 +720,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 +921,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 diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index dabb479cb..209c37c77 100644 --- a/packages/core/src/nodes.rs +++ b/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> + 'a>), } impl<'a> Default for RenderReturn<'a> { @@ -688,32 +684,6 @@ impl 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> + 'a, -{ - fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a> { - let f: &mut dyn Future> = 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) } diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs index f8be8a0dd..834b0cabd 100644 --- a/packages/core/src/properties.rs +++ b/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() } diff --git a/packages/core/src/scheduler/mod.rs b/packages/core/src/scheduler/mod.rs index 8fb476fbe..f5c540dd3 100644 --- a/packages/core/src/scheduler/mod.rs +++ b/packages/core/src/scheduler/mod.rs @@ -18,9 +18,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 +27,6 @@ pub(crate) struct Scheduler { /// Tasks created with cx.spawn pub tasks: RefCell>, - - /// Async components - pub leaves: RefCell>, } impl Scheduler { @@ -40,7 +34,6 @@ impl Scheduler { Rc::new(Scheduler { sender, tasks: RefCell::new(Slab::new()), - leaves: RefCell::new(Slab::new()), }) } } diff --git a/packages/core/src/scheduler/suspense.rs b/packages/core/src/scheduler/suspense.rs index 39e404198..ad3d525b7 100644 --- a/packages/core/src/scheduler/suspense.rs +++ b/packages/core/src/scheduler/suspense.rs @@ -36,23 +36,3 @@ impl SuspenseContext { } } } - -pub(crate) struct SuspenseLeaf { - pub(crate) scope_id: ScopeId, - pub(crate) notified: Cell, - pub(crate) task: *mut dyn Future>, - pub(crate) waker: Waker, -} - -pub struct SuspenseHandle { - pub(crate) id: SuspenseId, - pub(crate) tx: futures_channel::mpsc::UnboundedSender, -} - -impl ArcWake for SuspenseHandle { - fn wake_by_ref(arc_self: &Arc) { - _ = arc_self - .tx - .unbounded_send(SchedulerMsg::SuspenseNotified(arc_self.id)); - } -} diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index 77bea3a18..4e56f50f0 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/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::{innerlude::SuspenseContext, ScopeId, TaskId, VirtualDom}; +use std::{rc::Rc, task::Context}; impl VirtualDom { /// Handle notifications by tasks inside the scheduler @@ -44,68 +33,4 @@ impl VirtualDom { .consume_context::>() .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); - } - } - } - } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index a8a7319aa..96bf8e08b 100644 --- a/packages/core/src/scope_arena.rs +++ b/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( @@ -58,7 +50,7 @@ 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]; @@ -67,65 +59,11 @@ 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); + 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 diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index ff94c456d..02b9bb65a 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -6,7 +6,7 @@ use crate::{ 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}; @@ -574,9 +574,9 @@ impl<'src> ScopeState { /// fn(Scope) -> Element; /// async fn(Scope>) -> Element; /// ``` - pub fn component>( + pub fn component

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

Waiting for... child
"); - // 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 { - todo!() - // cx.use_hook(|| { - // cx.provide_context(Rc::new(SuspenseContext::new(cx.scope_id()))); - // }); +fn suspended_child(cx: Scope) -> Element { + let mut val = use_state(cx, || 0); - // // Ensure the right types are found - // cx.has_context::>().unwrap(); + if **val < 3 { + let mut val = val.clone(); + cx.spawn(async move { + val += 1; + }); + return cx.suspend()?; + } - // cx.render(rsx!(async_child {})) -} - -fn async_child(cx: Scope<'_>) -> Element { - todo!() - // use_future!(cx, || tokio::time::sleep(Duration::from_millis(10))).await; - // cx.render(rsx!(async_text {})) -} - -fn async_text(cx: Scope<'_>) -> Element { - todo!() - // let username = use_future!(cx, || async { - // tokio::time::sleep(std::time::Duration::from_secs(1)).await; - // "async child 1" - // }); - - // 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()); - - // cx.render(rsx!( div { "Hello! {username}, you are {age}, {_user} {_age}" } )) + render!("child") } From 0f5605b3db2a4148c67a40758df50fbcdbe4c8f1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 17:13:49 -0700 Subject: [PATCH 4/7] Apply clippy fixes --- packages/core/tests/safety.rs | 2 +- packages/core/tests/suspense.rs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/tests/safety.rs b/packages/core/tests/safety.rs index e5763674d..35bf47816 100644 --- a/packages/core/tests/safety.rs +++ b/packages/core/tests/safety.rs @@ -1,6 +1,6 @@ //! Tests related to safety of the library. -use std::rc::Rc; + use dioxus::prelude::*; diff --git a/packages/core/tests/suspense.rs b/packages/core/tests/suspense.rs index 2a64dc53c..e7c63cc98 100644 --- a/packages/core/tests/suspense.rs +++ b/packages/core/tests/suspense.rs @@ -1,8 +1,8 @@ -use dioxus::core::Mutation::*; + use dioxus::prelude::*; -use std::future::IntoFuture; -use std::rc::Rc; -use std::time::Duration; + + + #[tokio::test] async fn it_works() { @@ -28,7 +28,7 @@ fn app(cx: Scope) -> Element { } fn suspended_child(cx: Scope) -> Element { - let mut val = use_state(cx, || 0); + let val = use_state(cx, || 0); if **val < 3 { let mut val = val.clone(); From e751f63030e9be9d2b8059f0f32ffa9a5bc4011b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 17:17:08 -0700 Subject: [PATCH 5/7] Dont use suspense in dog app, remove from use_future --- examples/dog_app.rs | 4 ++-- packages/hooks/src/usefuture.rs | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/dog_app.rs b/examples/dog_app.rs index 510550b99..243e4bbca 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -21,7 +21,7 @@ fn app_root(cx: Scope<'_>) -> Element { .await }); - match breeds.suspend()? { + match breeds.value()? { Ok(breed_list) => cx.render(rsx! { div { height: "500px", h1 { "Select a dog breed!" } @@ -59,7 +59,7 @@ fn breed_pic(cx: Scope, breed: String) -> Element { .await }); - match fut.suspend()? { + match fut.value()? { Ok(resp) => render! { div { button { diff --git a/packages/hooks/src/usefuture.rs b/packages/hooks/src/usefuture.rs index 9d39e82de..18bf50310 100644 --- a/packages/hooks/src/usefuture.rs +++ b/packages/hooks/src/usefuture.rs @@ -167,10 +167,6 @@ impl UseFuture { (Some(_), None) => UseFutureState::Pending, } } - - pub fn suspend(&self) -> Option<&T> { - todo!() - } } impl<'a, T> IntoFuture for &'a UseFuture { From cc984fae5402340a5f94703c13437e2d36959e5c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 17:45:48 -0700 Subject: [PATCH 6/7] Only check suspense if the component aborted rendering --- packages/core/src/any_props.rs | 3 +-- packages/core/src/scope_arena.rs | 10 ++++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index e3a3bdaef..00ccda597 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -1,11 +1,10 @@ -use std::{ panic::AssertUnwindSafe}; - use crate::{ innerlude::Scoped, nodes::RenderReturn, scopes::{Scope, ScopeState}, Element, }; +use std::panic::AssertUnwindSafe; /// A trait that essentially allows VComponentProps to be used generically /// diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 39b39f8c4..d058e34de 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -83,10 +83,12 @@ impl VirtualDom { id: scope.id, }); - if scope.suspended.get() { - self.suspended_scopes.insert(scope.id); - } else if !self.suspended_scopes.is_empty() { - _ = self.suspended_scopes.remove(&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 From 68253b49d7eaebcf8b87bcda3e61537aded2de4e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 17:46:14 -0700 Subject: [PATCH 7/7] cargo fmt --- packages/core/src/lib.rs | 5 ++--- packages/core/tests/safety.rs | 2 -- packages/core/tests/suspense.rs | 4 ---- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 514999242..565ae12be 100644 --- a/packages/core/src/lib.rs +++ b/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, - 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 diff --git a/packages/core/tests/safety.rs b/packages/core/tests/safety.rs index 35bf47816..7a5c0dc2a 100644 --- a/packages/core/tests/safety.rs +++ b/packages/core/tests/safety.rs @@ -1,7 +1,5 @@ //! Tests related to safety of the library. - - use dioxus::prelude::*; /// Ensure no issues with not calling rebuild diff --git a/packages/core/tests/suspense.rs b/packages/core/tests/suspense.rs index e7c63cc98..861b609ca 100644 --- a/packages/core/tests/suspense.rs +++ b/packages/core/tests/suspense.rs @@ -1,9 +1,5 @@ - use dioxus::prelude::*; - - - #[tokio::test] async fn it_works() { // wait just a moment, not enough time for the boundary to resolve