Add flush_sync and pre_render methods to core
This commit is contained in:
17 files changed
+378
-282
No files matched your search
@@ -14,12 +14,14 @@ rustc-hash = { workspace = true }
|
||||
longest-increasing-subsequence = "0.1.0"
|
||||
futures-util = { workspace = true, default-features = false, features = [
|
||||
"alloc",
|
||||
"std",
|
||||
] }
|
||||
slab = { workspace = true }
|
||||
futures-channel = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
dioxus = { workspace = true }
|
||||
|
||||
+2
-10
@@ -41,7 +41,7 @@ use dioxus::prelude::*;
|
||||
fn app() -> Element {
|
||||
rsx!{
|
||||
div { "hello world" }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@@ -49,13 +49,8 @@ fn main() {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
// The initial render of the dom will generate a stream of edits for the real dom to apply
|
||||
let mutations = dom.rebuild();
|
||||
|
||||
// Somehow, you can apply these edits to the real dom
|
||||
apply_edits_to_real_dom(mutations);
|
||||
let mutations = dom.rebuild_to_vec();
|
||||
}
|
||||
|
||||
# fn apply_edits_to_real_dom(mutations: Mutations) {}
|
||||
```
|
||||
|
||||
|
||||
@@ -68,9 +63,6 @@ We can then wait for any asynchronous components or pending futures using the `w
|
||||
# async fn wait(mut dom: VirtualDom) {
|
||||
// Wait for the dom to be marked dirty internally
|
||||
dom.wait_for_work().await;
|
||||
|
||||
// Or wait for a deadline and then collect edits
|
||||
let mutations = dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(16)));
|
||||
# }
|
||||
```
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ impl VirtualDom {
|
||||
pub(crate) fn drop_scope(&mut self, id: ScopeId) {
|
||||
let height = {
|
||||
let scope = self.scopes.remove(id.0);
|
||||
let context = scope.context();
|
||||
let context = scope.state();
|
||||
context.height
|
||||
};
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ impl VNode {
|
||||
tracing::trace!(
|
||||
"Memoized props for component {:#?} ({})",
|
||||
scope_id,
|
||||
old_scope.context().name
|
||||
old_scope.state().name
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -94,7 +94,7 @@ impl VNode {
|
||||
let new = dom.run_scope(scope_id);
|
||||
dom.diff_scope(to, scope_id, new);
|
||||
|
||||
let height = dom.runtime.get_context(scope_id).unwrap().height;
|
||||
let height = dom.runtime.get_state(scope_id).unwrap().height;
|
||||
dom.dirty_scopes.remove(&DirtyScope {
|
||||
height,
|
||||
id: scope_id,
|
||||
@@ -130,7 +130,7 @@ impl VNode {
|
||||
// Load up a ScopeId for this vcomponent. If it's already mounted, then we can just use that
|
||||
let scope = dom
|
||||
.new_scope(component.props.duplicate(), component.name)
|
||||
.context()
|
||||
.state()
|
||||
.id;
|
||||
|
||||
// Store the scope id for the next render
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::Future;
|
||||
|
||||
use crate::{runtime::Runtime, Element, ScopeId, Task};
|
||||
use futures_util::{future::poll_fn, Future};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Get the current scope id
|
||||
pub fn current_scope_id() -> Option<ScopeId> {
|
||||
@@ -30,7 +28,7 @@ pub fn consume_context<T: 'static + Clone>() -> T {
|
||||
/// Consume context from the current scope
|
||||
pub fn consume_context_from_scope<T: 'static + Clone>(scope_id: ScopeId) -> Option<T> {
|
||||
Runtime::with(|rt| {
|
||||
rt.get_context(scope_id)
|
||||
rt.get_state(scope_id)
|
||||
.and_then(|cx| cx.consume_context::<T>())
|
||||
})
|
||||
.flatten()
|
||||
@@ -53,9 +51,7 @@ pub fn provide_root_context<T: 'static + Clone>(value: T) -> Option<T> {
|
||||
|
||||
/// Suspends the current component
|
||||
pub fn suspend() -> Option<Element> {
|
||||
Runtime::with_current_scope(|cx| {
|
||||
cx.suspend();
|
||||
});
|
||||
Runtime::with_current_scope(|cx| cx.suspend());
|
||||
None
|
||||
}
|
||||
|
||||
@@ -87,7 +83,7 @@ pub fn remove_future(id: Task) {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use dioxus_core::ScopeState;
|
||||
/// use dioxus_core::use_hook;
|
||||
///
|
||||
/// // prints a greeting on the initial render
|
||||
/// pub fn use_hello_world() {
|
||||
@@ -98,6 +94,47 @@ pub fn use_hook<State: Clone + 'static>(initializer: impl FnOnce() -> State) ->
|
||||
Runtime::with_current_scope(|cx| cx.use_hook(initializer)).expect("to be in a dioxus runtime")
|
||||
}
|
||||
|
||||
/// Push a function to be run before the next render
|
||||
/// This is a hook and will always run, so you can't unschedule it
|
||||
/// Will run for every progression of suspense, though this might change in the future
|
||||
pub fn use_before_render(f: impl FnMut() + 'static) {
|
||||
Runtime::with_current_scope(|cx| cx.push_before_render(f));
|
||||
}
|
||||
|
||||
/// Wait for the virtualdom to finish its sync work before proceeding
|
||||
///
|
||||
/// This is useful if you've just triggered an update and want to wait for it to finish before proceeding with valid
|
||||
/// DOM nodes.
|
||||
pub async fn flush_sync() {
|
||||
let mut polled = false;
|
||||
|
||||
let _task =
|
||||
FlushKey(Runtime::with(|rt| rt.add_to_flush_table()).expect("to be in a dioxus runtime"));
|
||||
|
||||
// Poll without giving the waker to anyone
|
||||
// The runtime will manually wake this task up when it's ready
|
||||
poll_fn(|_| {
|
||||
if !polled {
|
||||
polled = true;
|
||||
futures_util::task::Poll::Pending
|
||||
} else {
|
||||
futures_util::task::Poll::Ready(())
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
// If the the future got polled, then we don't need to prevent it from being dropped
|
||||
// This would all be solved with generational indicies on tasks
|
||||
std::mem::forget(_task);
|
||||
|
||||
struct FlushKey(Task);
|
||||
impl Drop for FlushKey {
|
||||
fn drop(&mut self) {
|
||||
Runtime::with(|rt| rt.flush_table.borrow_mut().remove(&self.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current render since the inception of this component
|
||||
///
|
||||
/// This can be used as a helpful diagnostic when debugging hooks/renders, etc
|
||||
@@ -138,7 +175,7 @@ pub fn schedule_update_any() -> Arc<dyn Fn(ScopeId) + Send + Sync> {
|
||||
/// (created with [`use_effect`](crate::use_effect)).
|
||||
///
|
||||
/// Example:
|
||||
/// ```rust
|
||||
/// ```rust, ignore
|
||||
/// use dioxus::prelude::*;
|
||||
///
|
||||
/// fn app() -> Element {
|
||||
@@ -176,7 +213,7 @@ pub fn schedule_update_any() -> Arc<dyn Fn(ScopeId) + Send + Sync> {
|
||||
/// }
|
||||
/// });
|
||||
///
|
||||
/// use_on_destroy({
|
||||
/// use_drop({
|
||||
/// to_owned![original_scroll_position];
|
||||
/// /// restore scroll to the top of the page
|
||||
/// move || {
|
||||
|
||||
@@ -59,6 +59,7 @@ pub(crate) mod innerlude {
|
||||
/// example()
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// ## React-Style
|
||||
/// ```rust, ignore
|
||||
/// fn Example(cx: Props) -> Element {
|
||||
@@ -86,14 +87,14 @@ pub use crate::innerlude::{
|
||||
/// This includes types like [`Scope`], [`Element`], and [`Component`].
|
||||
pub mod prelude {
|
||||
pub use crate::innerlude::{
|
||||
consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, generation,
|
||||
has_context, needs_update, parent_scope, provide_context, provide_root_context,
|
||||
consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, flush_sync,
|
||||
generation, has_context, needs_update, parent_scope, provide_context, provide_root_context,
|
||||
remove_future, schedule_update, schedule_update_any, spawn, spawn_forever, suspend,
|
||||
try_consume_context, use_drop, use_error_boundary, use_hook, use_hook_with_cleanup,
|
||||
AnyValue, Attribute, Component, ComponentFunction, Element, ErrorBoundary, Event,
|
||||
EventHandler, Fragment, HasAttributes, IntoAttributeValue, IntoDynNode,
|
||||
OptionStringFromMarker, Properties, Runtime, RuntimeGuard, ScopeId, ScopeState, SuperFrom,
|
||||
SuperInto, Task, Template, TemplateAttribute, TemplateNode, Throw, VNode, VNodeInner,
|
||||
VirtualDom,
|
||||
try_consume_context, use_before_render, use_drop, use_error_boundary, use_hook,
|
||||
use_hook_with_cleanup, AnyValue, Attribute, Component, ComponentFunction, Element,
|
||||
ErrorBoundary, Event, EventHandler, Fragment, HasAttributes, IntoAttributeValue,
|
||||
IntoDynNode, OptionStringFromMarker, Properties, Runtime, RuntimeGuard, ScopeId,
|
||||
ScopeState, SuperFrom, SuperInto, Task, Template, TemplateAttribute, TemplateNode, Throw,
|
||||
VNode, VNodeInner, VirtualDom,
|
||||
};
|
||||
}
|
||||
@@ -105,7 +105,7 @@ pub struct VNodeInner {
|
||||
/// The inner list *must* be in the format [static named attributes, remaining dynamically named attributes].
|
||||
///
|
||||
/// For example:
|
||||
/// ```rust
|
||||
/// ```rust, ignore
|
||||
/// div {
|
||||
/// class: "{class}",
|
||||
/// ..attrs,
|
||||
@@ -116,7 +116,7 @@ pub struct VNodeInner {
|
||||
/// ```
|
||||
///
|
||||
/// Would be represented as:
|
||||
/// ```rust
|
||||
/// ```rust, ignore
|
||||
/// [
|
||||
/// [class, every attribute in attrs sorted by name], // Slot 0 in the template
|
||||
/// [color], // Slot 1 in the template
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use crate::{
|
||||
innerlude::{LocalTask, SchedulerMsg},
|
||||
scope_context::ScopeContext,
|
||||
scope_context::Scope,
|
||||
scopes::ScopeId,
|
||||
Task,
|
||||
};
|
||||
use std::{
|
||||
cell::{Cell, Ref, RefCell},
|
||||
collections::VecDeque,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
@@ -16,7 +17,7 @@ thread_local! {
|
||||
|
||||
/// A global runtime that is shared across all scopes that provides the async runtime and context API
|
||||
pub struct Runtime {
|
||||
pub(crate) scope_contexts: RefCell<Vec<Option<ScopeContext>>>,
|
||||
pub(crate) scope_states: RefCell<Vec<Option<Scope>>>,
|
||||
|
||||
// We use this to track the current scope
|
||||
pub(crate) scope_stack: RefCell<Vec<ScopeId>>,
|
||||
@@ -29,10 +30,10 @@ pub struct Runtime {
|
||||
/// Tasks created with cx.spawn
|
||||
pub(crate) tasks: RefCell<slab::Slab<Rc<LocalTask>>>,
|
||||
|
||||
/// Queued tasks that are waiting to be polled
|
||||
pub(crate) queued_tasks: Rc<RefCell<VecDeque<Task>>>,
|
||||
|
||||
pub(crate) sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||
|
||||
// Tasks waiting to be manually resumed when we call wait_for_work
|
||||
pub(crate) flush_table: RefCell<FxHashSet<Task>>,
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
@@ -40,11 +41,11 @@ impl Runtime {
|
||||
Rc::new(Self {
|
||||
sender,
|
||||
rendering: Cell::new(true),
|
||||
scope_contexts: Default::default(),
|
||||
scope_states: Default::default(),
|
||||
scope_stack: Default::default(),
|
||||
current_task: Default::default(),
|
||||
tasks: Default::default(),
|
||||
queued_tasks: Rc::new(RefCell::new(VecDeque::new())),
|
||||
flush_table: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -54,35 +55,39 @@ impl Runtime {
|
||||
}
|
||||
|
||||
/// Create a scope context. This slab is synchronized with the scope slab.
|
||||
pub(crate) fn create_context_at(&self, id: ScopeId, context: ScopeContext) {
|
||||
let mut contexts = self.scope_contexts.borrow_mut();
|
||||
if contexts.len() <= id.0 {
|
||||
contexts.resize_with(id.0 + 1, Default::default);
|
||||
pub(crate) fn create_scope(&self, context: Scope) {
|
||||
let id = context.id;
|
||||
let mut scopes = self.scope_states.borrow_mut();
|
||||
if scopes.len() <= id.0 {
|
||||
scopes.resize_with(id.0 + 1, Default::default);
|
||||
}
|
||||
contexts[id.0] = Some(context);
|
||||
scopes[id.0] = Some(context);
|
||||
}
|
||||
|
||||
pub(crate) fn remove_context(self: &Rc<Self>, id: ScopeId) {
|
||||
pub(crate) fn remove_scope(self: &Rc<Self>, id: ScopeId) {
|
||||
{
|
||||
let borrow = self.scope_contexts.borrow();
|
||||
let borrow = self.scope_states.borrow();
|
||||
if let Some(scope) = &borrow[id.0] {
|
||||
let _runtime_guard = RuntimeGuard::new(self.clone());
|
||||
// Manually drop tasks, hooks, and contexts inside of the runtime
|
||||
self.on_scope(id, || {
|
||||
// Drop all spawned tasks
|
||||
// Drop all spawned tasks - order doesn't matter since tasks don't rely on eachother
|
||||
// In theory nested tasks might not like this
|
||||
for id in scope.spawned_tasks.take() {
|
||||
self.remove_task(id);
|
||||
}
|
||||
|
||||
// Drop all hooks
|
||||
scope.hooks.take();
|
||||
// Drop all hooks in reverse order in case a hook depends on another hook.
|
||||
for hook in scope.hooks.take().drain(..).rev() {
|
||||
drop(hook);
|
||||
}
|
||||
|
||||
// Drop all contexts
|
||||
scope.shared_contexts.take();
|
||||
});
|
||||
}
|
||||
}
|
||||
self.scope_contexts.borrow_mut()[id.0].take();
|
||||
self.scope_states.borrow_mut()[id.0].take();
|
||||
}
|
||||
|
||||
/// Get the current scope id
|
||||
@@ -104,11 +109,11 @@ impl Runtime {
|
||||
o
|
||||
}
|
||||
|
||||
/// Get the context for any scope given its ID
|
||||
/// Get the state for any scope given its ID
|
||||
///
|
||||
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
|
||||
pub(crate) fn get_context(&self, id: ScopeId) -> Option<Ref<'_, ScopeContext>> {
|
||||
Ref::filter_map(self.scope_contexts.borrow(), |contexts| {
|
||||
pub(crate) fn get_state(&self, id: ScopeId) -> Option<Ref<'_, Scope>> {
|
||||
Ref::filter_map(self.scope_states.borrow(), |contexts| {
|
||||
contexts.get(id.0).and_then(|f| f.as_ref())
|
||||
})
|
||||
.ok()
|
||||
@@ -125,34 +130,22 @@ impl Runtime {
|
||||
}
|
||||
|
||||
/// Runs a function with the current runtime
|
||||
pub(crate) fn with<F, R>(f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&Runtime) -> R,
|
||||
{
|
||||
RUNTIMES.with(|stack| {
|
||||
let stack = stack.borrow();
|
||||
stack.last().map(|r| f(r))
|
||||
})
|
||||
pub(crate) fn with<R>(f: impl FnOnce(&Runtime) -> R) -> Option<R> {
|
||||
RUNTIMES.with(|stack| stack.borrow().last().map(|r| f(r)))
|
||||
}
|
||||
|
||||
/// Runs a function with the current scope
|
||||
pub(crate) fn with_current_scope<F, R>(f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&ScopeContext) -> R,
|
||||
{
|
||||
pub(crate) fn with_current_scope<R>(f: impl FnOnce(&Scope) -> R) -> Option<R> {
|
||||
Self::with(|rt| {
|
||||
rt.current_scope_id()
|
||||
.and_then(|scope| rt.get_context(scope).map(|sc| f(&sc)))
|
||||
.and_then(|scope| rt.get_state(scope).map(|sc| f(&sc)))
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Runs a function with the current scope
|
||||
pub(crate) fn with_scope<F, R>(scope: ScopeId, f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&ScopeContext) -> R,
|
||||
{
|
||||
Self::with(|rt| rt.get_context(scope).map(|sc| f(&sc))).flatten()
|
||||
pub(crate) fn with_scope<R>(scope: ScopeId, f: impl FnOnce(&Scope) -> R) -> Option<R> {
|
||||
Self::with(|rt| rt.get_state(scope).map(|sc| f(&sc))).flatten()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +175,9 @@ impl Runtime {
|
||||
/// }
|
||||
///
|
||||
/// fn Component(cx: ComponentProps) -> Element {
|
||||
/// cx.use_hook(|| RuntimeGuard::new(cx.runtime.clone()));
|
||||
/// use_hook(|| {
|
||||
/// let _guard = RuntimeGuard::new(cx.runtime.clone());
|
||||
/// });
|
||||
///
|
||||
/// rsx! { div {} }
|
||||
/// }
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
any_props::{AnyProps, BoxedAnyProps},
|
||||
innerlude::{DirtyScope, ScopeState},
|
||||
nodes::RenderReturn,
|
||||
scope_context::ScopeContext,
|
||||
scope_context::Scope,
|
||||
scopes::ScopeId,
|
||||
virtual_dom::VirtualDom,
|
||||
};
|
||||
@@ -11,7 +11,7 @@ impl VirtualDom {
|
||||
pub(super) fn new_scope(&mut self, props: BoxedAnyProps, name: &'static str) -> &ScopeState {
|
||||
let parent_id = self.runtime.current_scope_id();
|
||||
let height = parent_id
|
||||
.and_then(|parent_id| self.runtime.get_context(parent_id).map(|f| f.height + 1))
|
||||
.and_then(|parent_id| self.runtime.get_state(parent_id).map(|f| f.height + 1))
|
||||
.unwrap_or(0);
|
||||
let entry = self.scopes.vacant_entry();
|
||||
let id = ScopeId(entry.key());
|
||||
@@ -19,34 +19,43 @@ impl VirtualDom {
|
||||
let scope = entry.insert(ScopeState {
|
||||
runtime: self.runtime.clone(),
|
||||
context_id: id,
|
||||
|
||||
props,
|
||||
last_rendered_node: Default::default(),
|
||||
});
|
||||
|
||||
let context = ScopeContext::new(name, id, parent_id, height);
|
||||
self.runtime.create_context_at(id, context);
|
||||
self.runtime
|
||||
.create_scope(Scope::new(name, id, parent_id, height));
|
||||
|
||||
scope
|
||||
}
|
||||
|
||||
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> RenderReturn {
|
||||
self.runtime.scope_stack.borrow_mut().push(scope_id);
|
||||
debug_assert!(
|
||||
crate::Runtime::current().is_some(),
|
||||
"Must be in a dioxus runtime"
|
||||
);
|
||||
|
||||
self.runtime.scope_stack.borrow_mut().push(scope_id);
|
||||
let scope = &self.scopes[scope_id.0];
|
||||
let new_nodes = {
|
||||
let context = scope.context();
|
||||
let context = scope.state();
|
||||
|
||||
context.suspended.set(false);
|
||||
context.hook_index.set(0);
|
||||
|
||||
// Run all pre-render hooks
|
||||
for pre_run in context.before_render.borrow_mut().iter_mut() {
|
||||
pre_run();
|
||||
}
|
||||
|
||||
// safety: due to how we traverse the tree, we know that the scope is not currently aliased
|
||||
let props: &dyn AnyProps = &*scope.props;
|
||||
|
||||
let span = tracing::trace_span!("render", scope = %scope.context().name);
|
||||
let span = tracing::trace_span!("render", scope = %scope.state().name);
|
||||
span.in_scope(|| props.render())
|
||||
};
|
||||
|
||||
let context = scope.context();
|
||||
let context = scope.state();
|
||||
|
||||
// And move the render generation forward by one
|
||||
context.render_count.set(context.render_count.get() + 1);
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::{
|
||||
/// A component's state separate from its props.
|
||||
///
|
||||
/// This struct exists to provide a common interface for all scopes without relying on generics.
|
||||
pub(crate) struct ScopeContext {
|
||||
pub(crate) struct Scope {
|
||||
pub(crate) name: &'static str,
|
||||
pub(crate) id: ScopeId,
|
||||
pub(crate) parent_id: Option<ScopeId>,
|
||||
@@ -21,13 +21,12 @@ pub(crate) struct ScopeContext {
|
||||
// Note: the order of the hook and context fields is important. The hooks field must be dropped before the contexts field in case a hook drop implementation tries to access a context.
|
||||
pub(crate) hooks: RefCell<Vec<Box<dyn Any>>>,
|
||||
pub(crate) hook_index: Cell<usize>,
|
||||
|
||||
pub(crate) shared_contexts: RefCell<Vec<Box<dyn Any>>>,
|
||||
|
||||
pub(crate) spawned_tasks: RefCell<FxHashSet<Task>>,
|
||||
pub(crate) before_render: RefCell<Vec<Box<dyn FnMut()>>>,
|
||||
}
|
||||
|
||||
impl ScopeContext {
|
||||
impl Scope {
|
||||
pub(crate) fn new(
|
||||
name: &'static str,
|
||||
id: ScopeId,
|
||||
@@ -45,6 +44,7 @@ impl ScopeContext {
|
||||
spawned_tasks: RefCell::new(FxHashSet::default()),
|
||||
hooks: RefCell::new(vec![]),
|
||||
hook_index: Cell::new(0),
|
||||
before_render: RefCell::new(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ impl ScopeContext {
|
||||
let mut search_parent = self.parent_id;
|
||||
let cur_runtime = Runtime::with(|runtime| {
|
||||
while let Some(parent_id) = search_parent {
|
||||
let parent = runtime.get_context(parent_id).unwrap();
|
||||
let parent = runtime.get_state(parent_id).unwrap();
|
||||
tracing::trace!(
|
||||
"looking for context {} ({:?}) in {}",
|
||||
std::any::type_name::<T>(),
|
||||
@@ -212,7 +212,7 @@ impl ScopeContext {
|
||||
pub fn provide_root_context<T: 'static + Clone>(&self, context: T) -> T {
|
||||
Runtime::with(|runtime| {
|
||||
runtime
|
||||
.get_context(ScopeId::ROOT)
|
||||
.get_state(ScopeId::ROOT)
|
||||
.unwrap()
|
||||
.provide_context(context)
|
||||
})
|
||||
@@ -256,9 +256,10 @@ impl ScopeContext {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use dioxus::prelude::*;
|
||||
/// // prints a greeting on the initial render
|
||||
/// pub fn use_hello_world() {
|
||||
/// cx.use_hook(|| println!("Hello, world!"));
|
||||
/// use_hook(|| println!("Hello, world!"));
|
||||
/// }
|
||||
/// ```
|
||||
pub fn use_hook<State: Clone + 'static>(&self, initializer: impl FnOnce() -> State) -> State {
|
||||
@@ -287,6 +288,10 @@ impl ScopeContext {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn push_before_render(&self, f: impl FnMut() + 'static) {
|
||||
self.before_render.borrow_mut().push(Box::new(f));
|
||||
}
|
||||
|
||||
/// Get the current render since the inception of this component
|
||||
///
|
||||
/// This can be used as a helpful diagnostic when debugging hooks/renders, etc
|
||||
@@ -320,7 +325,7 @@ impl ScopeId {
|
||||
/// Consume context from the current scope
|
||||
pub fn consume_context_from_scope<T: 'static + Clone>(self, scope_id: ScopeId) -> Option<T> {
|
||||
Runtime::with(|rt| {
|
||||
rt.get_context(scope_id)
|
||||
rt.get_state(scope_id)
|
||||
.and_then(|cx| cx.consume_context::<T>())
|
||||
})
|
||||
.flatten()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
any_props::BoxedAnyProps, nodes::RenderReturn, runtime::Runtime, scope_context::ScopeContext,
|
||||
any_props::BoxedAnyProps, nodes::RenderReturn, runtime::Runtime, scope_context::Scope,
|
||||
};
|
||||
use std::{cell::Ref, fmt::Debug, rc::Rc};
|
||||
|
||||
@@ -39,7 +39,7 @@ pub struct ScopeState {
|
||||
|
||||
impl Drop for ScopeState {
|
||||
fn drop(&mut self) {
|
||||
self.runtime.remove_context(self.context_id);
|
||||
self.runtime.remove_scope(self.context_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ impl ScopeState {
|
||||
self.last_rendered_node.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) fn context(&self) -> Ref<'_, ScopeContext> {
|
||||
self.runtime.get_context(self.context_id).unwrap()
|
||||
pub(crate) fn state(&self) -> Ref<'_, Scope> {
|
||||
self.runtime.get_state(self.context_id).unwrap()
|
||||
}
|
||||
}
|
||||
+53
-34
@@ -1,10 +1,10 @@
|
||||
use crate::innerlude::{remove_future, spawn, Runtime};
|
||||
use crate::ScopeId;
|
||||
use futures_util::task::ArcWake;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::Waker;
|
||||
use std::{cell::Cell, future::Future};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
/// A task's unique identifier.
|
||||
@@ -34,6 +34,25 @@ impl Task {
|
||||
pub fn stop(self) {
|
||||
remove_future(self);
|
||||
}
|
||||
|
||||
/// Pause the task.
|
||||
pub fn pause(&self) {
|
||||
Runtime::with(|rt| rt.tasks.borrow()[self.0].active.set(false));
|
||||
}
|
||||
|
||||
/// Check if the task is paused.
|
||||
pub fn paused(&self) -> bool {
|
||||
Runtime::with(|rt| !rt.tasks.borrow()[self.0].active.get()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Resume the task.
|
||||
pub fn resume(&self) {
|
||||
Runtime::with(|rt| {
|
||||
// set the active flag, and then ping the scheduler to ensure the task gets queued
|
||||
rt.tasks.borrow()[self.0].active.set(true);
|
||||
_ = rt.sender.unbounded_send(SchedulerMsg::TaskNotified(*self));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
@@ -55,9 +74,10 @@ impl Runtime {
|
||||
let task_id = Task(entry.key());
|
||||
|
||||
let task = Rc::new(LocalTask {
|
||||
scope,
|
||||
active: Cell::new(true),
|
||||
parent: self.current_task(),
|
||||
task: RefCell::new(Box::pin(task)),
|
||||
scope,
|
||||
waker: futures_util::task::waker(Arc::new(LocalTaskHandle {
|
||||
id: task_id,
|
||||
tx: self.sender.clone(),
|
||||
@@ -73,18 +93,33 @@ impl Runtime {
|
||||
debug_assert!(self.tasks.try_borrow_mut().is_ok());
|
||||
debug_assert!(task.task.try_borrow_mut().is_ok());
|
||||
|
||||
let mut cx = std::task::Context::from_waker(&task.waker);
|
||||
|
||||
if !task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
|
||||
self.sender
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(task_id))
|
||||
.expect("Scheduler should exist");
|
||||
}
|
||||
self.sender
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(task_id))
|
||||
.expect("Scheduler should exist");
|
||||
|
||||
task_id
|
||||
}
|
||||
|
||||
/// Get the currently running task
|
||||
pub fn current_task(&self) -> Option<Task> {
|
||||
self.current_task.get()
|
||||
}
|
||||
|
||||
/// Get the parent task of the given task, if it exists
|
||||
pub fn parent_task(&self, task: Task) -> Option<Task> {
|
||||
self.tasks.borrow().get(task.0)?.parent
|
||||
}
|
||||
|
||||
/// Add this task to the queue of tasks that will manually get poked when the scheduler is flushed
|
||||
pub(crate) fn add_to_flush_table(&self) -> Task {
|
||||
let value = self.current_task().unwrap();
|
||||
self.flush_table.borrow_mut().insert(value);
|
||||
value
|
||||
}
|
||||
|
||||
pub(crate) fn handle_task_wakeup(&self, id: Task) {
|
||||
debug_assert!(Runtime::current().is_some(), "Must be in a dioxus runtime");
|
||||
|
||||
let task = self.tasks.borrow().get(id.0).cloned();
|
||||
|
||||
// The task was removed from the scheduler, so we can just ignore it
|
||||
@@ -92,6 +127,11 @@ impl Runtime {
|
||||
return;
|
||||
};
|
||||
|
||||
// If a task woke up but is paused, we can just ignore it
|
||||
if !task.active.get() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut cx = std::task::Context::from_waker(&task.waker);
|
||||
|
||||
// update the scope stack
|
||||
@@ -99,10 +139,9 @@ impl Runtime {
|
||||
self.rendering.set(false);
|
||||
self.current_task.set(Some(id));
|
||||
|
||||
// If the task completes...
|
||||
if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
|
||||
// Remove it from the scope so we dont try to double drop it when the scope dropes
|
||||
self.get_context(task.scope)
|
||||
self.get_state(task.scope)
|
||||
.unwrap()
|
||||
.spawned_tasks
|
||||
.borrow_mut()
|
||||
@@ -118,31 +157,11 @@ impl Runtime {
|
||||
self.current_task.set(None);
|
||||
}
|
||||
|
||||
/// Take a queued task from the scheduler
|
||||
pub(crate) fn take_queued_task(&self) -> Option<Task> {
|
||||
self.queued_tasks.borrow_mut().pop_front()
|
||||
}
|
||||
|
||||
/// Drop the future with the given TaskId
|
||||
///
|
||||
/// This does not abort the task, so you'll want to wrap it in an abort handle if that's important to you
|
||||
pub(crate) fn remove_task(&self, id: Task) -> Option<Rc<LocalTask>> {
|
||||
let task = self.tasks.borrow_mut().try_remove(id.0);
|
||||
|
||||
// Remove the task from the queued tasks so we don't poll a different task with the same id
|
||||
self.queued_tasks.borrow_mut().retain(|t| *t != id);
|
||||
|
||||
task
|
||||
}
|
||||
|
||||
/// Get the currently running task
|
||||
pub fn current_task(&self) -> Option<Task> {
|
||||
self.current_task.get()
|
||||
}
|
||||
|
||||
/// Get the parent task of the given task, if it exists
|
||||
pub fn parent_task(&self, task: Task) -> Option<Task> {
|
||||
self.tasks.borrow().get(task.0)?.parent
|
||||
self.tasks.borrow_mut().try_remove(id.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +171,7 @@ pub(crate) struct LocalTask {
|
||||
parent: Option<Task>,
|
||||
task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
|
||||
waker: Waker,
|
||||
active: Cell<bool>,
|
||||
}
|
||||
|
||||
/// The type of message that can be sent to the scheduler.
|
||||
@@ -173,8 +193,7 @@ struct LocalTaskHandle {
|
||||
|
||||
impl ArcWake for LocalTaskHandle {
|
||||
fn wake_by_ref(arc_self: &Arc<Self>) {
|
||||
// This can fail if the scheduler has been dropped while the application is shutting down
|
||||
let _ = arc_self
|
||||
_ = arc_self
|
||||
.tx
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(arc_self.id));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+102
-12
@@ -1,11 +1,22 @@
|
||||
//! Verify that tasks get polled by the virtualdom properly, and that we escape wait_for_work safely
|
||||
|
||||
#[cfg(not(miri))]
|
||||
use std::{sync::atomic::AtomicUsize, time::Duration};
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
async fn run_vdom(app: fn() -> Element) {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||
|
||||
tokio::select! {
|
||||
_ = dom.wait_for_work() => {}
|
||||
_ = tokio::time::sleep(Duration::from_millis(500)) => {}
|
||||
};
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn it_works() {
|
||||
use dioxus::prelude::*;
|
||||
use std::{sync::atomic::AtomicUsize, time::Duration};
|
||||
|
||||
static POLL_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
fn app() -> Element {
|
||||
@@ -28,14 +39,7 @@ async fn it_works() {
|
||||
rsx!({ () })
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||
|
||||
tokio::select! {
|
||||
_ = dom.wait_for_work() => {}
|
||||
_ = tokio::time::sleep(Duration::from_millis(500)) => {}
|
||||
};
|
||||
run_vdom(app).await;
|
||||
|
||||
// By the time the tasks are finished, we should've accumulated ticks from two tasks
|
||||
// Be warned that by setting the delay to too short, tokio might not schedule in the tasks
|
||||
@@ -44,3 +48,89 @@ async fn it_works() {
|
||||
135
|
||||
);
|
||||
}
|
||||
|
||||
/// Prove that yield_now doesn't cause a deadlock
|
||||
#[tokio::test]
|
||||
async fn yield_now_works() {
|
||||
thread_local! {
|
||||
static SEQUENCE: std::cell::RefCell<Vec<usize>> = std::cell::RefCell::new(Vec::new());
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
// these two tasks should yield to eachother
|
||||
use_hook(|| {
|
||||
spawn(async move {
|
||||
for x in 0..10 {
|
||||
tokio::task::yield_now().await;
|
||||
SEQUENCE.with(|s| s.borrow_mut().push(1));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
use_hook(|| {
|
||||
spawn(async move {
|
||||
for x in 0..10 {
|
||||
tokio::task::yield_now().await;
|
||||
SEQUENCE.with(|s| s.borrow_mut().push(2));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
rsx!({ () })
|
||||
}
|
||||
|
||||
run_vdom(app).await;
|
||||
|
||||
SEQUENCE.with(|s| assert_eq!(s.borrow().len(), 20));
|
||||
}
|
||||
|
||||
/// Ensure that calling wait_for_flush waits for dioxus to finish its syncrhonous work
|
||||
#[tokio::test]
|
||||
async fn flushing() {
|
||||
thread_local! {
|
||||
static SEQUENCE: std::cell::RefCell<Vec<usize>> = std::cell::RefCell::new(Vec::new());
|
||||
}
|
||||
|
||||
fn app() -> Element {
|
||||
use_hook(|| {
|
||||
spawn(async move {
|
||||
for x in 0..10 {
|
||||
flush_sync().await;
|
||||
SEQUENCE.with(|s| s.borrow_mut().push(1));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
use_hook(|| {
|
||||
spawn(async move {
|
||||
for x in 0..10 {
|
||||
flush_sync().await;
|
||||
SEQUENCE.with(|s| s.borrow_mut().push(2));
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
rsx!({ () })
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
|
||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||
|
||||
let fut = async {
|
||||
// Trigger the flush by waiting for work
|
||||
for _ in 0..40 {
|
||||
tokio::select! {
|
||||
_ = dom.wait_for_work() => {}
|
||||
_ = tokio::time::sleep(Duration::from_millis(1)) => {}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
_ = fut => {}
|
||||
_ = tokio::time::sleep(Duration::from_millis(500)) => {}
|
||||
};
|
||||
|
||||
SEQUENCE.with(|s| assert_eq!(s.borrow().len(), 20));
|
||||
}
|
||||
@@ -65,7 +65,7 @@ struct BoxProps {
|
||||
alpha: f32,
|
||||
}
|
||||
#[allow(non_snake_case)]
|
||||
fn Box(cx: Scope<BoxProps>) -> Element {
|
||||
fn Box(cx: ScopeState<BoxProps>) -> Element {
|
||||
let count = use_signal(|| 0);
|
||||
|
||||
let x = cx.props.x * 2;
|
||||
@@ -94,7 +94,7 @@ struct GridProps {
|
||||
update_count: usize,
|
||||
}
|
||||
#[allow(non_snake_case)]
|
||||
fn Grid(cx: Scope<GridProps>) -> Element {
|
||||
fn Grid(cx: ScopeState<GridProps>) -> Element {
|
||||
let size = cx.props.size;
|
||||
let count = use_signal(|| 0);
|
||||
let counts = use_signal(|| vec![0; size * size]);
|
||||
@@ -151,7 +151,7 @@ fn Grid(cx: Scope<GridProps>) -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
fn app(cx: Scope<GridProps>) -> Element {
|
||||
fn app(cx: ScopeState<GridProps>) -> Element {
|
||||
rsx! {
|
||||
div{
|
||||
width: "100%",
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
LiveViewError,
|
||||
};
|
||||
use dioxus_core::prelude::*;
|
||||
use dioxus_html::{EventData, HtmlEvent, PlatformEventData};
|
||||
use dioxus_html::{select, EventData, HtmlEvent, PlatformEventData};
|
||||
use dioxus_interpreter_js::MutationState;
|
||||
use futures_util::{pin_mut, SinkExt, StreamExt};
|
||||
use serde::Serialize;
|
||||
@@ -227,11 +227,14 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
|
||||
}
|
||||
}
|
||||
|
||||
vdom.render_with_deadline(
|
||||
tokio::time::sleep(Duration::from_millis(10)),
|
||||
&mut mutations,
|
||||
)
|
||||
.await;
|
||||
// wait for suspense to resolve in a 10ms window
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(Duration::from_millis(10)) => {}
|
||||
_ = vdom.wait_for_suspense() => {}
|
||||
}
|
||||
|
||||
// render the vdom
|
||||
vdom.render_immediate(&mut mutations);
|
||||
|
||||
if let Some(edits) = take_edits(&mut mutations) {
|
||||
ws.send(edits).await?;
|
||||
|
||||
@@ -151,7 +151,7 @@ where
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
/// A component that renders the current route.
|
||||
pub fn Router<R: Routable + Clone>(cx: Scope<RouterProps<R>>) -> Element
|
||||
pub fn Router<R: Routable + Clone>(cx: ScopeState<RouterProps<R>>) -> Element
|
||||
where
|
||||
<R as FromStr>::Err: std::fmt::Display,
|
||||
R: serde::Serialize + serde::de::DeserializeOwned,
|
||||
|
||||
Reference in new issue
Block a user