Add flush_sync and pre_render methods to core

This commit is contained in:
Jonathan Kelley committed 2024-01-28 01:30:33 -08:00
1 parent 3ccf520205
commit 7b0dcb3206
17 files changed
+378 -282

No files matched your search

+72 -129
View File
@@ -13,12 +13,12 @@ use crate::{
nodes::{Template, TemplateId},
runtime::{Runtime, RuntimeGuard},
scopes::ScopeId,
AttributeValue, ComponentFunction, Element, Event, Mutations, Task,
AttributeValue, ComponentFunction, Element, Event, Mutations,
};
use futures_util::{pin_mut, StreamExt};
use futures_util::StreamExt;
use rustc_hash::{FxHashMap, FxHashSet};
use slab::Slab;
use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc};
use std::{any::Any, collections::BTreeSet, rc::Rc};
/// A virtual node system that progresses user events and diffs UI trees.
///
@@ -29,7 +29,7 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc};
/// ```rust
/// # use dioxus::prelude::*;
///
/// #[derive(Props, PartialEq)]
/// #[derive(Props, PartialEq, Clone)]
/// struct AppProps {
/// title: String
/// }
@@ -37,7 +37,7 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc};
/// fn app(cx: AppProps) -> Element {
/// rsx!(
/// div {"hello, {cx.title}"}
/// ))
/// )
/// }
/// ```
///
@@ -47,7 +47,7 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc};
/// # #![allow(unused)]
/// # use dioxus::prelude::*;
///
/// # #[derive(Props, PartialEq)]
/// # #[derive(Props, PartialEq, Clone)]
/// # struct AppProps {
/// # title: String
/// # }
@@ -60,26 +60,26 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc};
/// NavBar { routes: ROUTES }
/// Title { "{cx.title}" }
/// Footer {}
/// ))
/// )
/// }
///
/// #[component]
/// fn NavBar( routes: &'static str) -> Element {
/// rsx! {
/// div { "Routes: {routes}" }
/// })
/// }
/// }
///
/// #[component]
/// fn Footer() -> Element {
/// rsx! { div { "Footer" } })
/// rsx! { div { "Footer" } }
/// }
///
/// #[component]
/// fn Title<'a>( children: Element) -> Element {
/// fn Title( children: Element) -> Element {
/// rsx! {
/// div { id: "title", {children} }
/// })
/// }
/// }
/// ```
///
@@ -88,10 +88,10 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc};
///
/// ```rust
/// # use dioxus::prelude::*;
/// # fn app() -> Element { rsx! { div {} }) }
/// # fn app() -> Element { rsx! { div {} } }
///
/// let mut vdom = VirtualDom::new(app);
/// let edits = vdom.rebuild();
/// let edits = vdom.rebuild_to_vec();
/// ```
///
/// To call listeners inside the VirtualDom, call [`VirtualDom::handle_event`] with the appropriate event data.
@@ -130,7 +130,7 @@ use std::{any::Any, collections::BTreeSet, future::Future, rc::Rc};
/// fn app() -> Element {
/// rsx! {
/// div { "Hello World" }
/// })
/// }
/// }
///
/// let dom = VirtualDom::new(app);
@@ -247,7 +247,7 @@ impl VirtualDom {
/// }
///
/// fn Example(cx: SomeProps) -> Element {
/// rsx!{ div{ "hello {cx.name}" } })
/// rsx!{ div { "hello {cx.name}" } }
/// }
///
/// let dom = VirtualDom::new(Example);
@@ -291,7 +291,7 @@ impl VirtualDom {
/// }
///
/// fn Example(cx: SomeProps) -> Element {
/// rsx!{ div{ "hello {cx.name}" } })
/// rsx!{ div{ "hello {cx.name}" } }
/// }
///
/// let dom = VirtualDom::new(Example);
@@ -321,7 +321,7 @@ impl VirtualDom {
let root = dom.new_scope(Box::new(root), "app");
// Unlike react, we provide a default error boundary that just renders the error as a string
root.context()
root.state()
.provide_context(Rc::new(ErrorBoundary::new_in_scope(ScopeId::ROOT)));
// the root element is always given element ID 0 since it's the container for the entire tree
@@ -354,7 +354,7 @@ impl VirtualDom {
///
/// This is useful for what is essentially dependency injection when building the app
pub fn with_root_context<T: Clone + 'static>(self, context: T) -> Self {
self.base_scope().context().provide_context(context);
self.base_scope().state().provide_context(context);
self
}
@@ -362,14 +362,14 @@ impl VirtualDom {
///
/// This method is useful for when you want to provide a context in your app without knowing its type
pub fn insert_any_root_context(&mut self, context: Box<dyn Any>) {
self.base_scope().context().provide_any_context(context);
self.base_scope().state().provide_any_context(context);
}
/// Manually mark a scope as requiring a re-render
///
/// Whenever the Runtime "works", it will re-render this scope
pub fn mark_dirty(&mut self, id: ScopeId) {
if let Some(context) = self.runtime.get_context(id) {
if let Some(context) = self.runtime.get_state(id) {
let height = context.height();
tracing::trace!("Marking scope {:?} ({}) as dirty", id, context.name);
self.dirty_scopes.insert(DirtyScope { height, id });
@@ -417,52 +417,50 @@ impl VirtualDom {
///
/// ```rust, ignore
/// let dom = VirtualDom::new(app);
/// let sender = dom.get_scheduler_channel();
/// ```
pub async fn wait_for_work(&mut self) {
let mut some_msg = None;
// Ping tasks waiting on the flush table - they're waiting for sync stuff to be done before progressing
self.clear_flush_table();
loop {
// If a bunch of messages are ready in a sequence, try to pop them off synchronously
if let Some(msg) = some_msg.take() {
match msg {
SchedulerMsg::Immediate(id) => self.mark_dirty(id),
SchedulerMsg::TaskNotified(task) => self.queue_task_wakeup(task),
}
// Process all events - Scopes are marked dirty, etc
self.process_events();
// Now that we have collected all queued work, we should check if we have any dirty scopes. If there are not, then we can poll any queued futures
if !self.dirty_scopes.is_empty() || !self.suspended_scopes.is_empty() {
return;
}
// If they're not ready, then we should wait for them to be ready
match self.rx.try_next() {
Ok(Some(val)) => some_msg = Some(val),
Ok(None) => return,
Err(_) => {
// Now that we have collected all queued work, we should check if we have any dirty scopes. If there are not, then we can poll any queued futures
let has_dirty_scopes = !self.dirty_scopes.is_empty();
// Make sure we set the runtime since we're running user code
let _runtime = RuntimeGuard::new(self.runtime.clone());
if !has_dirty_scopes {
// If we have no dirty scopes, then we should poll any tasks that have been notified
while let Some(task) = self.runtime.take_queued_task() {
self.handle_task_wakeup(task);
}
}
match self.rx.next().await.expect("channel should never close") {
SchedulerMsg::Immediate(id) => self.mark_dirty(id),
SchedulerMsg::TaskNotified(id) => self.runtime.handle_task_wakeup(id),
};
}
}
// If we have any dirty scopes, or finished fiber trees then we should exit
if has_dirty_scopes || !self.suspended_scopes.is_empty() {
return;
}
fn clear_flush_table(&mut self) {
// Make sure we set the runtime since we're running user code
let _runtime = RuntimeGuard::new(self.runtime.clone());
some_msg = self.rx.next().await
}
}
// Manually flush tasks that called `flush().await`
// Tasks that might've been waiting for `flush` finally have a chance to run to their next await point
for task in self.runtime.flush_table.take() {
self.runtime.handle_task_wakeup(task);
}
}
/// Process all events in the queue until there are no more left
pub fn process_events(&mut self) {
let _runtime = RuntimeGuard::new(self.runtime.clone());
// Prevent a task from deadlocking the runtime by repeatedly queueing itself
while let Ok(Some(msg)) = self.rx.try_next() {
match msg {
SchedulerMsg::Immediate(id) => self.mark_dirty(id),
SchedulerMsg::TaskNotified(task) => self.queue_task_wakeup(task),
SchedulerMsg::TaskNotified(task) => self.runtime.handle_task_wakeup(task),
}
}
}
@@ -481,7 +479,7 @@ impl VirtualDom {
if sync.template.get().name.rsplit_once(':').unwrap().0
== template.name.rsplit_once(':').unwrap().0
{
let context = scope.context();
let context = scope.state();
let height = context.height;
self.dirty_scopes.insert(DirtyScope {
height,
@@ -542,18 +540,25 @@ impl VirtualDom {
/// suspended subtrees.
pub fn render_immediate(&mut self, to: &mut impl WriteMutations) {
self.flush_templates(to);
// Build a waker that won't wake up since our deadline is already expired when it's polled
let waker = futures_util::task::noop_waker();
let mut cx = std::task::Context::from_waker(&waker);
// Now run render with deadline but dont even try to poll any async tasks
let fut = self.render_with_deadline(std::future::ready(()), to);
pin_mut!(fut);
// Process any events that might be pending in the queue
self.process_events();
// The root component is not allowed to be async
match fut.poll(&mut cx) {
std::task::Poll::Ready(mutations) => mutations,
std::task::Poll::Pending => panic!("render_immediate should never return pending"),
// Next, diff any dirty scopes
// We choose not to poll the deadline since we complete pretty quickly anyways
while let Some(dirty) = self.dirty_scopes.pop_first() {
// If the scope doesn't exist for whatever reason, then we should skip it
if !self.scopes.contains(dirty.id.0) {
continue;
}
{
let _runtime = RuntimeGuard::new(self.runtime.clone());
// Run the scope and get the mutations
let new_nodes = self.run_scope(dirty.id);
self.diff_scope(to, dirty.id, new_nodes);
}
}
}
@@ -567,72 +572,24 @@ 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
///
/// Tasks waiting to be flushed are *cleared* here *without running them*
/// This behavior is subject to change, but configured this way so use_future/use_memo/use_future won't react on the server
pub async fn wait_for_suspense(&mut self) {
loop {
if self.suspended_scopes.is_empty() {
return;
}
// not sure if we should be doing this?
self.runtime.flush_table.borrow_mut().clear();
self.wait_for_work().await;
self.render_immediate(&mut NoOpMutations);
}
}
/// 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.
///
/// If no suspense trees are present
pub async fn render_with_deadline(
&mut self,
deadline: impl Future<Output = ()>,
to: &mut impl WriteMutations,
) {
self.flush_templates(to);
pin_mut!(deadline);
self.process_events();
loop {
// Next, diff any dirty scopes
// We choose not to poll the deadline since we complete pretty quickly anyways
while let Some(dirty) = self.dirty_scopes.pop_first() {
// If the scope doesn't exist for whatever reason, then we should skip it
if !self.scopes.contains(dirty.id.0) {
continue;
}
{
let _runtime = RuntimeGuard::new(self.runtime.clone());
// Run the scope and get the mutations
let new_nodes = self.run_scope(dirty.id);
self.diff_scope(to, dirty.id, new_nodes);
}
}
// Wait until the deadline is ready or we have work if there's no work ready
let work = self.wait_for_work();
pin_mut!(work);
use futures_util::future::{select, Either};
if let Either::Left((_, _)) = select(&mut deadline, &mut work).await {
return;
}
}
}
/// [`Self::render_with_deadline`] to a vector of mutations for testing purposes
pub async fn render_with_deadline_to_vec(
&mut self,
deadline: impl Future<Output = ()>,
) -> Mutations {
let mut mutations = Mutations::default();
self.render_with_deadline(deadline, &mut mutations).await;
mutations
}
/// Get the current runtime
pub fn runtime(&self) -> Rc<Runtime> {
self.runtime.clone()
@@ -645,20 +602,6 @@ impl VirtualDom {
}
}
/// Queue a task to be polled after all dirty scopes have been rendered
fn queue_task_wakeup(&mut self, id: Task) {
self.runtime.queued_tasks.borrow_mut().push_back(id);
}
/// Handle notifications by tasks inside the scheduler
///
/// This is precise, meaning we won't poll every task, just tasks that have woken up as notified to use by the
/// queue
fn handle_task_wakeup(&mut self, id: Task) {
let _runtime = RuntimeGuard::new(self.runtime.clone());
self.runtime.handle_task_wakeup(id);
}
/*
------------------------
The algorithm works by walking through the list of dynamic attributes, checking their paths, and breaking when
@@ -764,7 +707,7 @@ impl Drop for VirtualDom {
fn drop(&mut self) {
// Drop all scopes in order of height
let mut scopes = self.scopes.drain().collect::<Vec<_>>();
scopes.sort_by_key(|scope| scope.context().height);
scopes.sort_by_key(|scope| scope.state().height);
for scope in scopes.into_iter().rev() {
drop(scope);
}