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

Rip out async components in favor of a simpler suspense
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2023-07-18 18:10:11 -07:00
commit a6002a6c42
16 files changed
+100 -541

No files matched your search

+6 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
-30
View File
@@ -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
View File
@@ -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()
}
-9
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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();
-7
View File
@@ -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
View File
@@ -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())));
});
fn suspended_child(cx: Scope) -> Element {
let val = use_state(cx, || 0);
// Ensure the right types are found
cx.has_context::<Rc<SuspenseContext>>().unwrap();
if **val < 3 {
let mut val = val.clone();
cx.spawn(async move {
val += 1;
});
return cx.suspend()?;
}
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"
});
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")
}