From 4837d8e741343d26f31b55e4478a374dc761e538 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 15 Jul 2021 00:40:37 -0400 Subject: [PATCH] feat: suspense! --- README.md | 4 +- packages/core/src/context.rs | 69 +++++++----- packages/core/src/diff.rs | 108 ++---------------- packages/core/src/editor.rs | 173 +++++++++++++++++++++++++++++ packages/core/src/events.rs | 14 ++- packages/core/src/lib.rs | 5 +- packages/core/src/nodes.rs | 8 +- packages/core/src/serialize.rs | 66 ----------- packages/core/src/virtual_dom.rs | 18 +-- packages/web/examples/async_web.rs | 14 ++- 10 files changed, 260 insertions(+), 219 deletions(-) create mode 100644 packages/core/src/editor.rs delete mode 100644 packages/core/src/serialize.rs diff --git a/README.md b/README.md index 59c6f688c..4ff80ae93 100644 --- a/README.md +++ b/README.md @@ -163,13 +163,13 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an | Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs | | CSS/Inline Styles | ✅ | ✅ | syntax for inline styles/attribute groups | | Custom elements | ✅ | ✅ | Define new element primitives | -| Suspense | 🛠 | ✅ | schedule future render from future/promise | +| Suspense | ✅ | ✅ | schedule future render from future/promise | | Cooperative Scheduling | 🛠 | ✅ | Prioritize important events over non-important events | -| Fine-grained reactivity | 🛠 | ❓ | Skip diffing for fine-grain updates | | Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) | | 1st class global state | ✅ | ❓ | redux/recoil/mobx on top of context | | Subtree Memoization | ✅ | ❓ | skip diffing static element subtrees | | Compile-time correct | ✅ | ❓ | Throw errors on invalid template layouts | +| Fine-grained reactivity | 🛠 | ❓ | Skip diffing for fine-grain updates | | Heuristic Engine | 🛠 | ❓ | track component memory usage to minimize future allocations | | NodeRef | 🛠 | ✅ | gain direct access to nodes [1] | diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index 57d053143..5e916d80e 100644 --- a/packages/core/src/context.rs +++ b/packages/core/src/context.rs @@ -4,6 +4,7 @@ use bumpalo::Bump; use futures_util::FutureExt; use std::any::Any; +use std::cell::Cell; use std::marker::PhantomData; use std::{ @@ -355,8 +356,9 @@ Any function prefixed with "use" should not be called conditionally. pub(crate) struct SuspenseHook { pub value: Rc>>>, pub callback: SuspendedCallback, + pub dom_node_id: Rc>, } -type SuspendedCallback = Box Fn(SuspendedContext<'a>) -> VNode<'a>>; +type SuspendedCallback = Box Fn(Context<'a, ()>) -> VNode<'a>>; impl<'src, P> Context<'src, P> { /// Asynchronously render new nodes once the given future has completed. @@ -370,29 +372,48 @@ impl<'src, P> Context<'src, P> { /// /// pub fn use_suspense( - &'src self, + self, task_initializer: impl FnOnce() -> Fut, user_callback: Cb, ) -> VNode<'src> where Fut: Future + 'static, Out: 'static, - Cb: for<'a> Fn(SuspendedContext<'a>, &Out) -> VNode<'a> + 'static, + Cb: for<'a> Fn(Context<'a, ()>, &Out) -> VNode<'a> + 'static, { self.use_hook( move |hook_idx| { let value = Rc::new(RefCell::new(None)); + let dom_node_id = Rc::new(RealDomNode::empty_cell()); + let domnode = dom_node_id.clone(); + let slot = value.clone(); - let callback: SuspendedCallback = Box::new(move |ctx: SuspendedContext| { + + let callback: SuspendedCallback = Box::new(move |ctx: Context<()>| { let v: std::cell::Ref>> = slot.as_ref().borrow(); - let v: &dyn Any = v.as_ref().unwrap().as_ref(); - let real_val = v.downcast_ref::().unwrap(); - user_callback(ctx, real_val) + match v.as_ref() { + Some(a) => { + let v: &dyn Any = a.as_ref(); + let real_val = v.downcast_ref::().unwrap(); + user_callback(ctx, real_val) + } + None => { + // + VNode { + dom_id: RealDomNode::empty_cell(), + key: None, + kind: VNodeKind::Suspended { + node: domnode.clone(), + }, + } + } + } }); let originator = self.scope.arena_idx.clone(); let task_fut = task_initializer(); + let domnode = dom_node_id.clone(); let slot = value.clone(); self.submit_task(Box::pin(task_fut.then(move |output| async move { @@ -400,33 +421,25 @@ impl<'src, P> Context<'src, P> { // Dioxus will call the user's callback to generate new nodes outside of the diffing system *slot.borrow_mut() = Some(Box::new(output) as Box); EventTrigger { - event: VirtualEvent::SuspenseEvent { hook_idx }, + event: VirtualEvent::SuspenseEvent { hook_idx, domnode }, originator, priority: EventPriority::Low, real_node_id: None, } }))); - SuspenseHook { value, callback } + SuspenseHook { + value, + callback, + dom_node_id, + } }, move |hook| { - match hook.value.borrow().as_ref() { - Some(val) => { - let cx = SuspendedContext { - bump: &self.scope.cur_frame().bump, - }; - (&hook.callback)(cx) - } - None => { - // - VNode { - dom_id: RealDomNode::empty_cell(), - key: None, - kind: VNodeKind::Suspended, - } - } - } - // + let cx = Context { + scope: &self.scope, + props: &(), + }; + (&hook.callback)(cx) }, |_| {}, ) @@ -436,7 +449,3 @@ impl<'src, P> Context<'src, P> { pub struct TaskHandle<'src> { _p: PhantomData<&'src ()>, } -#[derive(Clone)] -pub struct SuspendedContext<'a> { - pub bump: &'a Bump, -} diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index a7e1b183d..1f857d7ed 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -70,100 +70,6 @@ pub trait RealDom<'a> { fn raw_node_as_any(&self) -> &mut dyn Any; } -pub struct DomEditor<'real, 'bump> { - edits: &'real mut Vec>, -} -use DomEdit::*; -impl<'real, 'bump> DomEditor<'real, 'bump> { - // Navigation - pub(crate) fn push(&mut self, root: RealDomNode) { - let id = root.as_u64(); - self.edits.push(PushRoot { id }); - } - pub(crate) fn pop(&mut self) { - self.edits.push(PopRoot {}); - } - - // Add Nodes to the dom - // add m nodes from the stack - pub(crate) fn append_children(&mut self, many: u32) { - self.edits.push(AppendChildren { many }); - } - - // replace the n-m node on the stack with the m nodes - // ends with the last element of the chain on the top of the stack - pub(crate) fn replace_with(&mut self, many: u32) { - self.edits.push(ReplaceWith { many }); - } - - // Remove Nodesfrom the dom - pub(crate) fn remove(&mut self) { - self.edits.push(Remove); - } - pub(crate) fn remove_all_children(&mut self) { - self.edits.push(RemoveAllChildren); - } - - // Create - pub(crate) fn create_text_node(&mut self, text: &'bump str, id: RealDomNode) { - let id = id.as_u64(); - self.edits.push(CreateTextNode { text, id }); - } - pub(crate) fn create_element( - &mut self, - tag: &'static str, - ns: Option<&'static str>, - id: RealDomNode, - ) { - let id = id.as_u64(); - match ns { - Some(ns) => self.edits.push(CreateElementNs { id, ns, tag }), - None => self.edits.push(CreateElement { id, tag }), - } - } - - // placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom - pub(crate) fn create_placeholder(&mut self, id: RealDomNode) { - let id = id.as_u64(); - self.edits.push(CreatePlaceholder { id }); - } - - // events - pub(crate) fn new_event_listener( - &mut self, - event: &'static str, - scope: ScopeIdx, - element_id: usize, - realnode: RealDomNode, - ) { - self.edits.push(NewEventListener { - scope, - event, - idx: element_id, - node: realnode.as_u64(), - }); - } - pub(crate) fn remove_event_listener(&mut self, event: &'static str) { - self.edits.push(RemoveEventListener { event }); - } - - // modify - pub(crate) fn set_text(&mut self, text: &'bump str) { - self.edits.push(SetText { text }); - } - pub(crate) fn set_attribute( - &mut self, - field: &'static str, - value: &'bump str, - ns: Option<&'static str>, - ) { - self.edits.push(SetAttribute { field, value, ns }); - } - pub(crate) fn remove_attribute(&mut self, name: &'static str) { - self.edits.push(RemoveAttribute { name }); - } -} - pub struct DiffMachine<'real, 'bump, Dom: RealDom<'bump>> { pub dom: &'real mut Dom, pub edits: DomEditor<'real, 'bump>, @@ -188,7 +94,7 @@ where task_queue: &'bump TaskQueue, ) -> Self { Self { - edits: DomEditor { edits }, + edits: DomEditor::new(edits), components, dom, cur_idx, @@ -364,8 +270,11 @@ where } // TODO - (VNodeKind::Suspended { .. }, _) => todo!(), - (_, VNodeKind::Suspended { .. }) => todo!(), + (VNodeKind::Suspended { node }, new) => todo!(), + (old, VNodeKind::Suspended { .. }) => { + // a node that was once real is now suspended + // + } } } } @@ -563,10 +472,11 @@ where CreateMeta::new(false, nodes_added) } - VNodeKind::Suspended => { + VNodeKind::Suspended { node: real_node } => { let id = self.dom.request_available_node(); self.edits.create_placeholder(id); node.dom_id.set(id); + real_node.set(id); CreateMeta::new(false, 1) } } @@ -1411,7 +1321,7 @@ impl<'a> Iterator for RealChildIterator<'a> { // Immediately abort suspended nodes - can't do anything with them yet // VNodeKind::Suspended => should_pop = true, - VNodeKind::Suspended => todo!(), + VNodeKind::Suspended { .. } => todo!(), // For components, we load their root and push them onto the stack VNodeKind::Component(sc) => { diff --git a/packages/core/src/editor.rs b/packages/core/src/editor.rs new file mode 100644 index 000000000..7e6cc457f --- /dev/null +++ b/packages/core/src/editor.rs @@ -0,0 +1,173 @@ +//! Serialization +//! ------------- +//! +//! +//! +//! +//! +//! + +use crate::{innerlude::ScopeIdx, RealDomNode}; + +/// The `DomEditor` provides an imperative interface for the Diffing algorithm to plan out its changes. +/// +/// However, the DomEditor only builds a change list - it does not apply them. In contrast with the "RealDom", the DomEditor +/// is cancellable and flushable. At any moment in time, Dioxus may choose to completely clear the edit list and start over. +/// +/// This behavior is used in the cooperative scheduling algorithm +pub struct DomEditor<'real, 'bump> { + pub edits: &'real mut Vec>, +} +use DomEdit::*; +impl<'real, 'bump> DomEditor<'real, 'bump> { + pub fn new(edits: &'real mut Vec>) -> Self { + Self { edits } + } + + // Navigation + pub(crate) fn push(&mut self, root: RealDomNode) { + let id = root.as_u64(); + self.edits.push(PushRoot { id }); + } + pub(crate) fn pop(&mut self) { + self.edits.push(PopRoot {}); + } + + // Add Nodes to the dom + // add m nodes from the stack + pub(crate) fn append_children(&mut self, many: u32) { + self.edits.push(AppendChildren { many }); + } + + // replace the n-m node on the stack with the m nodes + // ends with the last element of the chain on the top of the stack + pub(crate) fn replace_with(&mut self, many: u32) { + self.edits.push(ReplaceWith { many }); + } + + // Remove Nodesfrom the dom + pub(crate) fn remove(&mut self) { + self.edits.push(Remove); + } + pub(crate) fn remove_all_children(&mut self) { + self.edits.push(RemoveAllChildren); + } + + // Create + pub(crate) fn create_text_node(&mut self, text: &'bump str, id: RealDomNode) { + let id = id.as_u64(); + self.edits.push(CreateTextNode { text, id }); + } + pub(crate) fn create_element( + &mut self, + tag: &'static str, + ns: Option<&'static str>, + id: RealDomNode, + ) { + let id = id.as_u64(); + match ns { + Some(ns) => self.edits.push(CreateElementNs { id, ns, tag }), + None => self.edits.push(CreateElement { id, tag }), + } + } + + // placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom + pub(crate) fn create_placeholder(&mut self, id: RealDomNode) { + let id = id.as_u64(); + self.edits.push(CreatePlaceholder { id }); + } + + // events + pub(crate) fn new_event_listener( + &mut self, + event: &'static str, + scope: ScopeIdx, + element_id: usize, + realnode: RealDomNode, + ) { + self.edits.push(NewEventListener { + scope, + event, + idx: element_id, + node: realnode.as_u64(), + }); + } + pub(crate) fn remove_event_listener(&mut self, event: &'static str) { + self.edits.push(RemoveEventListener { event }); + } + + // modify + pub(crate) fn set_text(&mut self, text: &'bump str) { + self.edits.push(SetText { text }); + } + pub(crate) fn set_attribute( + &mut self, + field: &'static str, + value: &'bump str, + ns: Option<&'static str>, + ) { + self.edits.push(SetAttribute { field, value, ns }); + } + pub(crate) fn remove_attribute(&mut self, name: &'static str) { + self.edits.push(RemoveAttribute { name }); + } +} + +/// A `DomEdit` represents a serialzied form of the VirtualDom's trait-based API. This allows streaming edits across the +/// network or through FFI boundaries. +#[derive(Debug)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "type") +)] +pub enum DomEdit<'bump> { + PushRoot { + id: u64, + }, + PopRoot, + AppendChildren { + many: u32, + }, + ReplaceWith { + many: u32, + }, + Remove, + RemoveAllChildren, + CreateTextNode { + text: &'bump str, + id: u64, + }, + CreateElement { + tag: &'bump str, + id: u64, + }, + CreateElementNs { + tag: &'bump str, + id: u64, + ns: &'static str, + }, + CreatePlaceholder { + id: u64, + }, + NewEventListener { + event: &'static str, + scope: ScopeIdx, + node: u64, + idx: usize, + }, + RemoveEventListener { + event: &'static str, + }, + SetText { + text: &'bump str, + }, + SetAttribute { + field: &'static str, + value: &'bump str, + ns: Option<&'bump str>, + }, + RemoveAttribute { + name: &'static str, + }, +} diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 4d9055f61..85aad57c7 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -4,6 +4,11 @@ //! 3rd party renderers are responsible for converting their native events into these virtual event types. Events might //! be heavy or need to interact through FFI, so the events themselves are designed to be lazy. +use std::{ + cell::{Cell, RefCell}, + rc::Rc, +}; + use crate::innerlude::{RealDomNode, ScopeIdx}; #[derive(Debug)] @@ -108,10 +113,15 @@ pub enum VirtualEvent { // // Async events don't necessarily propagate into a scope being ran. It's up to the event itself // to force an update for itself. - AsyncEvent { hook_idx: usize }, + AsyncEvent { + hook_idx: usize, + }, // These are more intrusive than the rest - SuspenseEvent { hook_idx: usize }, + SuspenseEvent { + hook_idx: usize, + domnode: Rc>, + }, OtherEvent, } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 08c5dec24..8e65250a2 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -30,11 +30,11 @@ pub(crate) mod innerlude { pub use crate::component::*; pub use crate::context::*; pub use crate::diff::*; + pub use crate::editor::*; pub use crate::error::*; pub use crate::events::*; pub use crate::nodes::*; pub use crate::scope::*; - pub use crate::serialize::*; pub use crate::tasks::*; pub use crate::util::*; pub use crate::virtual_dom::*; @@ -53,13 +53,12 @@ pub mod bumpframe; pub mod component; pub mod context; pub mod diff; +pub mod editor; pub mod error; pub mod events; pub mod hooklist; pub mod nodes; pub mod scope; -#[cfg(feature = "serialize")] -pub mod serialize; pub mod signals; pub mod tasks; pub mod util; diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 142167f2d..f3585f63f 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -30,7 +30,7 @@ pub enum VNodeKind<'src> { Element(&'src VElement<'src>), Fragment(VFragment<'src>), Component(&'src VComponent<'src>), - Suspended, + Suspended { node: Rc> }, } pub struct VText<'src> { @@ -216,7 +216,9 @@ impl<'a> NodeFactory<'a> { VNode { dom_id: RealDomNode::empty_cell(), key: None, - kind: VNodeKind::Suspended, + kind: VNodeKind::Suspended { + node: Rc::new(RealDomNode::empty_cell()), + }, } } @@ -430,7 +432,7 @@ impl<'a> Clone for VNode<'a> { is_static: fragment.is_static, }), VNodeKind::Component(component) => VNodeKind::Component(component), - VNodeKind::Suspended => VNodeKind::Suspended, + VNodeKind::Suspended { node } => VNodeKind::Suspended { node: node.clone() }, }; VNode { kind, diff --git a/packages/core/src/serialize.rs b/packages/core/src/serialize.rs deleted file mode 100644 index 3cd9036dc..000000000 --- a/packages/core/src/serialize.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! Serialization -//! ------------- -//! -//! -//! -//! -//! -//! - -use crate::innerlude::ScopeIdx; -use serde::{Deserialize, Serialize}; - -/// A `DomEdit` represents a serialzied form of the VirtualDom's trait-based API. This allows streaming edits across the -/// network or through FFI boundaries. -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "type")] -pub enum DomEdit<'bump> { - PushRoot { - id: u64, - }, - PopRoot, - AppendChildren { - many: u32, - }, - ReplaceWith { - many: u32, - }, - Remove, - RemoveAllChildren, - CreateTextNode { - text: &'bump str, - id: u64, - }, - CreateElement { - tag: &'bump str, - id: u64, - }, - CreateElementNs { - tag: &'bump str, - id: u64, - ns: &'static str, - }, - CreatePlaceholder { - id: u64, - }, - NewEventListener { - event: &'static str, - scope: ScopeIdx, - node: u64, - idx: usize, - }, - RemoveEventListener { - event: &'static str, - }, - SetText { - text: &'bump str, - }, - SetAttribute { - field: &'static str, - value: &'bump str, - ns: Option<&'bump str>, - }, - RemoveAttribute { - name: &'static str, - }, -} diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 1e74a2c11..809daec23 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -301,24 +301,26 @@ impl VirtualDom { VirtualEvent::AsyncEvent { .. } => {} // Suspense Events! A component's suspended node is updated - VirtualEvent::SuspenseEvent { hook_idx } => { + VirtualEvent::SuspenseEvent { hook_idx, domnode } => { let scope = self.components.try_get_mut(trigger.originator).unwrap(); // safety: we are sure that there are no other references to the inner content of this hook let hook = unsafe { scope.hooks.get_mut::(*hook_idx) }.unwrap(); - let cx = SuspendedContext { - bump: &scope.cur_frame().bump, - }; + let cx = Context { scope, props: &() }; // generate the new node! - let callback: VNode<'s> = (&hook.callback)(cx); + let nodes: VNode<'s> = (&hook.callback)(cx); + let nodes = scope.cur_frame().bump.alloc(nodes); - // diff that node with the node that was originally suspended! + // push the old node's root onto the stack + diff_machine.edits.push(domnode.get()); - // hook.callback; + // push these new nodes onto the diff machines stack + let meta = diff_machine.create(&*nodes); - // + // replace the placeholder with the new nodes we just pushed on the stack + diff_machine.edits.replace_with(meta.added_to_stack); } // This is the "meat" of our cooperative scheduler diff --git a/packages/web/examples/async_web.rs b/packages/web/examples/async_web.rs index 08a2c67dd..a93dd3535 100644 --- a/packages/web/examples/async_web.rs +++ b/packages/web/examples/async_web.rs @@ -34,13 +34,15 @@ const ENDPOINT: &str = "https://dog.ceo/api/breeds/image/random/"; static App: FC<()> = |cx| { let state = use_state(cx, || 0); - let request = cx.use_task(|| surf::get(ENDPOINT).recv_json::()).1; + let dog_node = cx.use_suspense( + || surf::get(ENDPOINT).recv_json::(), + |cx, res| match res { + Ok(res) => rsx!(in cx, img { src: "{res.message}" }), + Err(err) => rsx!(in cx, div { "No doggos for you :(" }), + }, + ); - let dog_node = if let Some(Ok(res)) = request { - rsx!(in cx, img { src: "{res.message}" }) - } else { - rsx!(in cx, div { "No doggos for you :(" }) - }; + log::error!("RIP WE RAN THE COMPONENT"); cx.render(rsx! { div {