improve signal copy runtime
This commit is contained in:
1 parent
3d4c5d7c98
commit
04cdb14e5b
14 files changed
+878
-319
No files matched your search
@@ -91,7 +91,7 @@ impl VirtualDom {
|
||||
// Note: This will not remove any ids from the arena
|
||||
pub(crate) fn drop_scope(&mut self, id: ScopeId, recursive: bool) {
|
||||
self.dirty_scopes.remove(&DirtyScope {
|
||||
height: self.scopes[id.0].height,
|
||||
height: self.scopes[id.0].height(),
|
||||
id,
|
||||
});
|
||||
|
||||
@@ -110,10 +110,11 @@ impl VirtualDom {
|
||||
// Drop all the hooks once the children are dropped
|
||||
// this means we'll drop hooks bottom-up
|
||||
scope.hooks.get_mut().clear();
|
||||
let context = scope.context();
|
||||
|
||||
// Drop all the futures once the hooks are dropped
|
||||
for task_id in scope.spawned_tasks.borrow_mut().drain() {
|
||||
scope.tasks.remove(task_id);
|
||||
for task_id in context.spawned_tasks.borrow_mut().drain() {
|
||||
context.tasks.remove(task_id);
|
||||
}
|
||||
|
||||
self.scopes.remove(id.0);
|
||||
|
||||
@@ -66,9 +66,9 @@ impl<'b> VirtualDom {
|
||||
///
|
||||
/// This method pushes the ScopeID to the internal scopestack and returns the number of nodes created.
|
||||
pub(crate) fn create_scope(&mut self, scope: ScopeId, template: &'b VNode<'b>) -> usize {
|
||||
self.scope_stack.push(scope);
|
||||
self.runtime.scope_stack.borrow_mut().push(scope);
|
||||
let out = self.create(template);
|
||||
self.scope_stack.pop();
|
||||
self.runtime.scope_stack.borrow_mut().pop();
|
||||
out
|
||||
}
|
||||
|
||||
@@ -522,7 +522,7 @@ impl<'b> VirtualDom {
|
||||
.take()
|
||||
.map(|props| {
|
||||
let unbounded_props: Box<dyn AnyProps> = unsafe { std::mem::transmute(props) };
|
||||
self.new_scope(unbounded_props, component.name).id
|
||||
self.new_scope(unbounded_props, component.name).context().id
|
||||
})
|
||||
.unwrap_or_else(|| component.scope.get().unwrap())
|
||||
}
|
||||
|
||||
@@ -15,9 +15,8 @@ use DynamicNode::*;
|
||||
|
||||
impl<'b> VirtualDom {
|
||||
pub(super) fn diff_scope(&mut self, scope: ScopeId) {
|
||||
let scope_state = &mut self.scopes[scope.0];
|
||||
|
||||
self.scope_stack.push(scope);
|
||||
self.runtime.scope_stack.borrow_mut().push(scope);
|
||||
let scope_state = &mut self.get_scope(scope).unwrap();
|
||||
unsafe {
|
||||
// Load the old and new bump arenas
|
||||
let old = scope_state
|
||||
@@ -47,7 +46,7 @@ impl<'b> VirtualDom {
|
||||
(Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]),
|
||||
};
|
||||
}
|
||||
self.scope_stack.pop();
|
||||
self.runtime.scope_stack.borrow_mut().pop();
|
||||
}
|
||||
|
||||
fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) {
|
||||
@@ -210,7 +209,7 @@ impl<'b> VirtualDom {
|
||||
self.diff_scope(scope_id);
|
||||
|
||||
self.dirty_scopes.remove(&DirtyScope {
|
||||
height: self.scopes[scope_id.0].height,
|
||||
height: self.runtime.scope_contexts[scope_id.0].height,
|
||||
id: scope_id,
|
||||
});
|
||||
}
|
||||
@@ -714,7 +713,12 @@ impl<'b> VirtualDom {
|
||||
|
||||
Component(comp) => {
|
||||
let scope = comp.scope.get().unwrap();
|
||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||
match unsafe {
|
||||
self.get_scope(scope)
|
||||
.unwrap()
|
||||
.root_node()
|
||||
.extend_lifetime_ref()
|
||||
} {
|
||||
RenderReturn::Ready(node) => self.push_all_real_nodes(node),
|
||||
RenderReturn::Aborted(_node) => todo!(),
|
||||
}
|
||||
@@ -915,7 +919,12 @@ impl<'b> VirtualDom {
|
||||
.expect("VComponents to always have a scope");
|
||||
|
||||
// Remove the component from the dom
|
||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||
match unsafe {
|
||||
self.get_scope(scope)
|
||||
.unwrap()
|
||||
.root_node()
|
||||
.extend_lifetime_ref()
|
||||
} {
|
||||
RenderReturn::Ready(t) => self.remove_node(t, gen_muts),
|
||||
RenderReturn::Aborted(placeholder) => self.remove_placeholder(placeholder, gen_muts),
|
||||
};
|
||||
@@ -936,7 +945,12 @@ impl<'b> VirtualDom {
|
||||
Some(Placeholder(t)) => t.id.get().unwrap(),
|
||||
Some(Component(comp)) => {
|
||||
let scope = comp.scope.get().unwrap();
|
||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||
match unsafe {
|
||||
self.get_scope(scope)
|
||||
.unwrap()
|
||||
.root_node()
|
||||
.extend_lifetime_ref()
|
||||
} {
|
||||
RenderReturn::Ready(t) => self.find_first_element(t),
|
||||
_ => todo!("cannot handle nonstandard nodes"),
|
||||
}
|
||||
@@ -952,7 +966,12 @@ impl<'b> VirtualDom {
|
||||
Some(Placeholder(t)) => t.id.get().unwrap(),
|
||||
Some(Component(comp)) => {
|
||||
let scope = comp.scope.get().unwrap();
|
||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||
match unsafe {
|
||||
self.get_scope(scope)
|
||||
.unwrap()
|
||||
.root_node()
|
||||
.extend_lifetime_ref()
|
||||
} {
|
||||
RenderReturn::Ready(t) => self.find_last_element(t),
|
||||
_ => todo!("cannot handle nonstandard nodes"),
|
||||
}
|
||||
|
||||
@@ -14,8 +14,10 @@ mod lazynodes;
|
||||
mod mutations;
|
||||
mod nodes;
|
||||
mod properties;
|
||||
mod runtime;
|
||||
mod scheduler;
|
||||
mod scope_arena;
|
||||
mod scope_context;
|
||||
mod scopes;
|
||||
mod virtual_dom;
|
||||
|
||||
@@ -31,6 +33,7 @@ pub(crate) mod innerlude {
|
||||
pub use crate::nodes::*;
|
||||
pub use crate::properties::*;
|
||||
pub use crate::scheduler::*;
|
||||
pub use crate::scope_context::*;
|
||||
pub use crate::scopes::*;
|
||||
pub use crate::virtual_dom::*;
|
||||
|
||||
@@ -81,9 +84,11 @@ pub use crate::innerlude::{
|
||||
/// This includes types like [`Scope`], [`Element`], and [`Component`].
|
||||
pub mod prelude {
|
||||
pub use crate::innerlude::{
|
||||
fc_to_builder, AnyValue, Component, Element, Event, EventHandler, Fragment,
|
||||
IntoAttributeValue, LazyNodes, Properties, Scope, ScopeId, ScopeState, Scoped, TaskId,
|
||||
Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
|
||||
consume_context, current_scope_id, fc_to_builder, has_context, provide_context,
|
||||
provide_root_context, schedule_update_any, suspend, throw, AnyValue, Component, Element,
|
||||
Event, EventHandler, Fragment, IntoAttributeValue, LazyNodes, Properties, Scope, ScopeId,
|
||||
ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, Throw, VNode,
|
||||
VirtualDom,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use crate::{
|
||||
innerlude::{DirtyScope, Scheduler, SchedulerMsg},
|
||||
scope_context::ScopeContext,
|
||||
scopes::{ScopeId, ScopeState},
|
||||
};
|
||||
use rustc_hash::FxHashSet;
|
||||
use slab::Slab;
|
||||
use std::{collections::BTreeSet, rc::Rc};
|
||||
|
||||
thread_local! {
|
||||
static RUNTIMES: RefCell<Vec<Runtime>> = RefCell::new(vec![]);
|
||||
}
|
||||
|
||||
/// Pushes a new scope onto the stack
|
||||
pub(crate) fn push_runtime(runtime: Runtime) {
|
||||
RUNTIMES.with(|stack| stack.borrow_mut().push(runtime));
|
||||
}
|
||||
|
||||
/// Pops a scope off the stack
|
||||
pub(crate) fn pop_runtime() {
|
||||
RUNTIMES.with(|stack| stack.borrow_mut().pop());
|
||||
}
|
||||
|
||||
/// Runs a function with the current runtime
|
||||
pub(crate) fn with_runtime<F, R>(f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&Runtime) -> R,
|
||||
{
|
||||
RUNTIMES.with(|stack| {
|
||||
let stack = stack.borrow();
|
||||
stack.last().map(f)
|
||||
})
|
||||
}
|
||||
|
||||
/// Runs a function with the current scope
|
||||
pub(crate) fn with_current_scope<F, R>(f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&ScopeContext) -> R,
|
||||
{
|
||||
with_runtime(|runtime| {
|
||||
runtime
|
||||
.current_scope_id()
|
||||
.and_then(|scope| runtime.get_context(scope).map(f))
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub struct Runtime {
|
||||
pub(crate) scope_contexts: Slab<ScopeContext>,
|
||||
pub(crate) scheduler: Rc<Scheduler>,
|
||||
|
||||
// While diffing we need some sort of way of breaking off a stream of suspended mutations.
|
||||
pub(crate) scope_stack: RefCell<Vec<ScopeId>>,
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
pub(crate) fn new(scheduler: Rc<Scheduler>) -> Rc<Self> {
|
||||
Rc::new(Self {
|
||||
scheduler,
|
||||
|
||||
scope_contexts: Default::default(),
|
||||
|
||||
scope_stack: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the current scope id
|
||||
pub fn current_scope_id(&self) -> Option<ScopeId> {
|
||||
self.scope_stack.borrow().last().copied()
|
||||
}
|
||||
|
||||
/// 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 fn get_context(&self, id: ScopeId) -> Option<&ScopeContext> {
|
||||
self.scope_contexts.get(id.0).map(|f| &*f)
|
||||
}
|
||||
|
||||
/// Get the single scope at the top of the Runtime tree that will always be around
|
||||
///
|
||||
/// This scope has a ScopeId of 0 and is the root of the tree
|
||||
pub fn base_context(&self) -> &ScopeContext {
|
||||
self.get_context(ScopeId(0)).unwrap()
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ impl VirtualDom {
|
||||
/// This is precise, meaning we won't poll every task, just tasks that have woken up as notified to use by the
|
||||
/// queue
|
||||
pub(crate) fn handle_task_wakeup(&mut self, id: TaskId) {
|
||||
let mut tasks = self.scheduler.tasks.borrow_mut();
|
||||
let mut tasks = self.runtime.scheduler.tasks.borrow_mut();
|
||||
|
||||
let task = match tasks.get(id.0) {
|
||||
Some(task) => task,
|
||||
@@ -20,8 +20,8 @@ impl VirtualDom {
|
||||
// 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
|
||||
let scope = &self.scopes[task.scope.0];
|
||||
scope.spawned_tasks.borrow_mut().remove(&id);
|
||||
let scope = &self.get_scope(task.scope).unwrap();
|
||||
scope.context().spawned_tasks.borrow_mut().remove(&id);
|
||||
|
||||
// Remove it from the scheduler
|
||||
tasks.try_remove(id.0);
|
||||
|
||||
@@ -13,35 +13,29 @@ impl VirtualDom {
|
||||
props: Box<dyn AnyProps<'static>>,
|
||||
name: &'static str,
|
||||
) -> &ScopeState {
|
||||
let parent = self.acquire_current_scope_raw();
|
||||
let parent_id = self.runtime.current_scope_id();
|
||||
let height = parent_id
|
||||
.and_then(|parent_id| self.get_scope(parent_id).map(|f| f.context().height + 1))
|
||||
.unwrap_or(0);
|
||||
let entry = self.scopes.vacant_entry();
|
||||
let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) };
|
||||
let id = ScopeId(entry.key());
|
||||
|
||||
entry.insert(Box::new(ScopeState {
|
||||
parent,
|
||||
id,
|
||||
height,
|
||||
name,
|
||||
entry.insert(ScopeState {
|
||||
runtime: self.runtime.clone(),
|
||||
context_id: id,
|
||||
|
||||
props: Some(props),
|
||||
tasks: self.scheduler.clone(),
|
||||
|
||||
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(),
|
||||
shared_contexts: Default::default(),
|
||||
|
||||
borrowed_props: Default::default(),
|
||||
attributes_to_drop: Default::default(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn acquire_current_scope_raw(&self) -> Option<*const ScopeState> {
|
||||
let id = self.scope_stack.last().copied()?;
|
||||
let scope = self.scopes.get(id.0)?;
|
||||
Some(scope.as_ref())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn {
|
||||
@@ -51,10 +45,10 @@ impl VirtualDom {
|
||||
self.ensure_drop_safety(scope_id);
|
||||
|
||||
let new_nodes = unsafe {
|
||||
self.scopes[scope_id.0].previous_frame().bump_mut().reset();
|
||||
|
||||
let scope = &self.scopes[scope_id.0];
|
||||
scope.suspended.set(false);
|
||||
scope.previous_frame().bump_mut().reset();
|
||||
|
||||
scope.context().suspended.set(false);
|
||||
|
||||
scope.hook_idx.set(0);
|
||||
|
||||
@@ -77,18 +71,19 @@ impl VirtualDom {
|
||||
// And move the render generation forward by one
|
||||
scope.render_cnt.set(scope.render_cnt.get() + 1);
|
||||
|
||||
let context = scope.context();
|
||||
// remove this scope from dirty scopes
|
||||
self.dirty_scopes.remove(&DirtyScope {
|
||||
height: scope.height,
|
||||
id: scope.id,
|
||||
height: context.height,
|
||||
id: context.id,
|
||||
});
|
||||
|
||||
if scope.suspended.get() {
|
||||
if context.suspended.get() {
|
||||
if matches!(allocated, RenderReturn::Aborted(_)) {
|
||||
self.suspended_scopes.insert(scope.id);
|
||||
self.suspended_scopes.insert(context.id);
|
||||
}
|
||||
} else if !self.suspended_scopes.is_empty() {
|
||||
_ = self.suspended_scopes.remove(&scope.id);
|
||||
_ = self.suspended_scopes.remove(&context.id);
|
||||
}
|
||||
|
||||
// rebind the lifetime now that its stored internally
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
use crate::{
|
||||
innerlude::{ErrorBoundary, Scheduler, SchedulerMsg},
|
||||
runtime::{with_current_scope, with_runtime},
|
||||
Element, ScopeId, TaskId,
|
||||
};
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::{Cell, RefCell},
|
||||
fmt::Debug,
|
||||
future::Future,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// A component's state separate from its props.
|
||||
///
|
||||
/// This struct exists to provide a common interface for all scopes without relying on generics.
|
||||
pub struct ScopeContext {
|
||||
pub(crate) name: &'static str,
|
||||
|
||||
pub(crate) id: ScopeId,
|
||||
pub(crate) parent_id: Option<ScopeId>,
|
||||
|
||||
pub(crate) height: u32,
|
||||
pub(crate) suspended: Cell<bool>,
|
||||
|
||||
pub(crate) shared_contexts: RefCell<Vec<(TypeId, Box<dyn Any>)>>,
|
||||
|
||||
pub(crate) tasks: Rc<Scheduler>,
|
||||
pub(crate) spawned_tasks: RefCell<FxHashSet<TaskId>>,
|
||||
}
|
||||
|
||||
impl ScopeContext {
|
||||
pub fn name(&self) -> &str {
|
||||
self.name
|
||||
}
|
||||
|
||||
pub fn height(&self) -> u32 {
|
||||
self.height
|
||||
}
|
||||
|
||||
pub fn parent_id(&self) -> Option<ScopeId> {
|
||||
self.parent_id
|
||||
}
|
||||
|
||||
pub fn scope_id(&self) -> ScopeId {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// Create a subscription that schedules a future render for the reference component
|
||||
///
|
||||
/// ## Notice: you should prefer using [`Self::schedule_update_any`] and [`Self::scope_id`]
|
||||
pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
|
||||
let (chan, id) = (self.tasks.sender.clone(), self.scope_id());
|
||||
Arc::new(move || drop(chan.unbounded_send(SchedulerMsg::Immediate(id))))
|
||||
}
|
||||
|
||||
/// Schedule an update for any component given its [`ScopeId`].
|
||||
///
|
||||
/// A component's [`ScopeId`] can be obtained from `use_hook` or the [`ScopeState::scope_id`] method.
|
||||
///
|
||||
/// This method should be used when you want to schedule an update for a component
|
||||
pub fn schedule_update_any(&self) -> Arc<dyn Fn(ScopeId) + Send + Sync> {
|
||||
let chan = self.tasks.sender.clone();
|
||||
Arc::new(move |id| {
|
||||
chan.unbounded_send(SchedulerMsg::Immediate(id)).unwrap();
|
||||
})
|
||||
}
|
||||
|
||||
/// Mark this scope as dirty, and schedule a render for it.
|
||||
pub fn needs_update(&self) {
|
||||
self.needs_update_any(self.scope_id());
|
||||
}
|
||||
|
||||
/// Get the [`ScopeId`] of a mounted component.
|
||||
///
|
||||
/// `ScopeId` is not unique for the lifetime of the [`crate::VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted.
|
||||
pub fn needs_update_any(&self, id: ScopeId) {
|
||||
self.tasks
|
||||
.sender
|
||||
.unbounded_send(SchedulerMsg::Immediate(id))
|
||||
.expect("Scheduler to exist if scope exists");
|
||||
}
|
||||
|
||||
/// Return any context of type T if it exists on this scope
|
||||
pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||
self.shared_contexts
|
||||
.borrow()
|
||||
.iter()
|
||||
.find(|(k, _)| *k == TypeId::of::<T>())
|
||||
.map(|(_, v)| v)?
|
||||
.downcast_ref::<T>()
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// Try to retrieve a shared state with type `T` from any parent scope.
|
||||
///
|
||||
/// Clones the state if it exists.
|
||||
pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||
if let Some(this_ctx) = self.has_context() {
|
||||
return Some(this_ctx);
|
||||
}
|
||||
|
||||
let mut search_parent = self.parent_id;
|
||||
with_runtime(|runtime| {
|
||||
let mut result = None;
|
||||
while let Some(parent_id) = search_parent {
|
||||
let parent = runtime.get_context(parent_id).unwrap();
|
||||
if let Some(shared) = parent
|
||||
.shared_contexts
|
||||
.borrow()
|
||||
.iter()
|
||||
.find(|(k, _)| *k == TypeId::of::<T>())
|
||||
{
|
||||
result = shared.1.downcast_ref::<T>().cloned();
|
||||
break;
|
||||
}
|
||||
search_parent = parent.parent_id;
|
||||
}
|
||||
result
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Expose state to children further down the [`crate::VirtualDom`] Tree. Requires `Clone` on the context to allow getting values down the tree.
|
||||
///
|
||||
/// This is a "fundamental" operation and should only be called during initialization of a hook.
|
||||
///
|
||||
/// For a hook that provides the same functionality, use `use_provide_context` and `use_context` instead.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// struct SharedState(&'static str);
|
||||
///
|
||||
/// static App: Component = |cx| {
|
||||
/// cx.use_hook(|| cx.provide_context(SharedState("world")));
|
||||
/// render!(Child {})
|
||||
/// }
|
||||
///
|
||||
/// static Child: Component = |cx| {
|
||||
/// let state = cx.consume_state::<SharedState>();
|
||||
/// render!(div { "hello {state.0}" })
|
||||
/// }
|
||||
/// ```
|
||||
pub fn provide_context<T: 'static + Clone>(&self, value: T) -> T {
|
||||
let mut contexts = self.shared_contexts.borrow_mut();
|
||||
|
||||
// If the context exists, swap it out for the new value
|
||||
for ctx in contexts.iter_mut() {
|
||||
// Swap the ptr directly
|
||||
if let Some(ctx) = ctx.1.downcast_mut::<T>() {
|
||||
std::mem::swap(ctx, &mut value.clone());
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// Else, just push it
|
||||
contexts.push((TypeId::of::<T>(), Box::new(value.clone())));
|
||||
|
||||
value
|
||||
}
|
||||
|
||||
/// Provide a context to the root and then consume it
|
||||
///
|
||||
/// This is intended for "global" state management solutions that would rather be implicit for the entire app.
|
||||
/// Things like signal runtimes and routers are examples of "singletons" that would benefit from lazy initialization.
|
||||
///
|
||||
/// Note that you should be checking if the context existed before trying to provide a new one. Providing a context
|
||||
/// when a context already exists will swap the context out for the new one, which may not be what you want.
|
||||
pub fn provide_root_context<T: 'static + Clone>(&self, context: T) -> T {
|
||||
with_runtime(|runtime| {
|
||||
// Walk upwards until there is no more parent - and tada we have the root
|
||||
let mut parent = runtime.get_context(self.scope_id()).unwrap();
|
||||
while let Some(next_parent) = parent.parent_id {
|
||||
parent = runtime.get_context(next_parent).unwrap();
|
||||
}
|
||||
debug_assert_eq!(parent.scope_id(), ScopeId(0));
|
||||
|
||||
parent.provide_context(context)
|
||||
})
|
||||
.expect("Runtime to exist")
|
||||
}
|
||||
|
||||
/// Pushes the future onto the poll queue to be polled after the component renders.
|
||||
pub fn push_future(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
||||
let id = self.tasks.spawn(self.id, fut);
|
||||
self.spawned_tasks.borrow_mut().insert(id);
|
||||
id
|
||||
}
|
||||
|
||||
/// Spawns the future but does not return the [`TaskId`]
|
||||
pub fn spawn(&self, fut: impl Future<Output = ()> + 'static) {
|
||||
self.push_future(fut);
|
||||
}
|
||||
|
||||
/// Spawn a future that Dioxus won't clean up when this component is unmounted
|
||||
///
|
||||
/// This is good for tasks that need to be run after the component has been dropped.
|
||||
pub fn spawn_forever(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
||||
// The root scope will never be unmounted so we can just add the task at the top of the app
|
||||
let id = self.tasks.spawn(ScopeId(0), fut);
|
||||
|
||||
// wake up the scheduler if it is sleeping
|
||||
self.tasks
|
||||
.sender
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(id))
|
||||
.expect("Scheduler should exist");
|
||||
|
||||
self.spawned_tasks.borrow_mut().insert(id);
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Informs the scheduler that this task is no longer needed and should be removed.
|
||||
///
|
||||
/// This drops the task immediately.
|
||||
pub fn remove_future(&self, id: TaskId) {
|
||||
self.tasks.remove(id);
|
||||
}
|
||||
|
||||
/// Inject an error into the nearest error boundary and quit rendering
|
||||
///
|
||||
/// The error doesn't need to implement Error or any specific traits since the boundary
|
||||
/// itself will downcast the error into a trait object.
|
||||
pub fn throw(&self, error: impl Debug + 'static) -> Option<()> {
|
||||
if let Some(cx) = self.consume_context::<Rc<ErrorBoundary>>() {
|
||||
cx.insert_error(self.scope_id(), Box::new(error));
|
||||
}
|
||||
|
||||
// Always return none during a throw
|
||||
None
|
||||
}
|
||||
|
||||
/// Mark this component as suspended and then return None
|
||||
pub fn suspend(&self) -> Option<Element> {
|
||||
self.suspended.set(true);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedule an update for any component given its [`ScopeId`].
|
||||
///
|
||||
/// A component's [`ScopeId`] can be obtained from `use_hook` or the [`ScopeState::scope_id`] method.
|
||||
///
|
||||
/// This method should be used when you want to schedule an update for a component
|
||||
pub fn schedule_update_any() -> Option<Arc<dyn Fn(ScopeId) + Send + Sync>> {
|
||||
with_current_scope(|cx| cx.schedule_update_any())
|
||||
}
|
||||
|
||||
/// Get the current scope id
|
||||
pub fn current_scope_id() -> Option<ScopeId> {
|
||||
with_runtime(|rt| rt.current_scope_id()).flatten()
|
||||
}
|
||||
|
||||
/// Consume context from the current scope
|
||||
pub fn consume_context<T: 'static + Clone>() -> Option<T> {
|
||||
with_current_scope(|cx| cx.consume_context::<T>()).flatten()
|
||||
}
|
||||
|
||||
/// Check if the current scope has a context
|
||||
pub fn has_context<T: 'static + Clone>() -> Option<T> {
|
||||
with_current_scope(|cx| cx.has_context::<T>()).flatten()
|
||||
}
|
||||
|
||||
/// Provide context to the current scope
|
||||
pub fn provide_context<T: 'static + Clone>(value: T) -> Option<T> {
|
||||
with_current_scope(|cx| cx.provide_context(value))
|
||||
}
|
||||
|
||||
/// Provide a context to the root scope
|
||||
pub fn provide_root_context<T: 'static + Clone>(value: T) -> Option<T> {
|
||||
with_current_scope(|cx| cx.provide_root_context(value))
|
||||
}
|
||||
|
||||
/// Suspends the current component
|
||||
pub fn suspend() -> Option<Element<'static>> {
|
||||
with_current_scope(|cx| {
|
||||
cx.suspend();
|
||||
});
|
||||
None
|
||||
}
|
||||
|
||||
/// Throw an error into the nearest error boundary
|
||||
pub fn throw(error: impl Debug + 'static) -> Option<()> {
|
||||
with_current_scope(|cx| cx.throw(error)).flatten()
|
||||
}
|
||||
+30
-101
@@ -2,16 +2,17 @@ use crate::{
|
||||
any_props::AnyProps,
|
||||
any_props::VProps,
|
||||
bump_frame::BumpFrame,
|
||||
innerlude::ErrorBoundary,
|
||||
innerlude::{DynamicNode, EventHandler, VComponent, VText},
|
||||
innerlude::{ErrorBoundary, Scheduler, SchedulerMsg},
|
||||
lazynodes::LazyNodes,
|
||||
nodes::{IntoAttributeValue, IntoDynNode, RenderReturn},
|
||||
runtime::Runtime,
|
||||
scope_context::ScopeContext,
|
||||
AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId,
|
||||
};
|
||||
use bumpalo::{boxed::Box as BumpBox, Bump};
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
any::Any,
|
||||
cell::{Cell, RefCell, UnsafeCell},
|
||||
fmt::{Arguments, Debug},
|
||||
future::Future,
|
||||
@@ -66,26 +67,17 @@ pub struct ScopeId(pub usize);
|
||||
///
|
||||
/// This struct exists to provide a common interface for all scopes without relying on generics.
|
||||
pub struct ScopeState {
|
||||
pub(crate) runtime: Rc<Runtime>,
|
||||
pub(crate) context_id: ScopeId,
|
||||
|
||||
pub(crate) render_cnt: Cell<usize>,
|
||||
pub(crate) name: &'static str,
|
||||
|
||||
pub(crate) node_arena_1: BumpFrame,
|
||||
pub(crate) node_arena_2: BumpFrame,
|
||||
|
||||
pub(crate) parent: Option<*const 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>,
|
||||
|
||||
pub(crate) shared_contexts: RefCell<Vec<(TypeId, Box<dyn Any>)>>,
|
||||
|
||||
pub(crate) tasks: Rc<Scheduler>,
|
||||
pub(crate) spawned_tasks: RefCell<FxHashSet<TaskId>>,
|
||||
|
||||
pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
|
||||
pub(crate) attributes_to_drop: RefCell<Vec<*const Attribute<'static>>>,
|
||||
|
||||
@@ -93,6 +85,10 @@ pub struct ScopeState {
|
||||
}
|
||||
|
||||
impl<'src> ScopeState {
|
||||
pub(crate) fn context(&self) -> &ScopeContext {
|
||||
self.runtime.get_context(self.context_id).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn current_frame(&self) -> &BumpFrame {
|
||||
match self.render_cnt.get() % 2 {
|
||||
0 => &self.node_arena_1,
|
||||
@@ -111,7 +107,7 @@ impl<'src> ScopeState {
|
||||
|
||||
/// Get the name of this component
|
||||
pub fn name(&self) -> &str {
|
||||
self.name
|
||||
self.context().name
|
||||
}
|
||||
|
||||
/// Get the current render since the inception of this component
|
||||
@@ -174,7 +170,7 @@ impl<'src> ScopeState {
|
||||
/// assert_eq!(base.height(), 0);
|
||||
/// ```
|
||||
pub fn height(&self) -> u32 {
|
||||
self.height
|
||||
self.context().height
|
||||
}
|
||||
|
||||
/// Get the Parent of this [`Scope`] within this Dioxus [`crate::VirtualDom`].
|
||||
@@ -193,9 +189,9 @@ impl<'src> ScopeState {
|
||||
///
|
||||
/// assert_eq!(base.parent(), None);
|
||||
/// ```
|
||||
pub fn parent(&self) -> Option<ScopeId> {
|
||||
pub fn parent_id(&self) -> Option<ScopeId> {
|
||||
// safety: the pointer to our parent is *always* valid thanks to the bump arena
|
||||
self.parent.map(|p| unsafe { &*p }.id)
|
||||
self.context().parent_id()
|
||||
}
|
||||
|
||||
/// Get the ID of this Scope within this Dioxus [`crate::VirtualDom`].
|
||||
@@ -212,15 +208,14 @@ impl<'src> ScopeState {
|
||||
/// assert_eq!(base.scope_id(), 0);
|
||||
/// ```
|
||||
pub fn scope_id(&self) -> ScopeId {
|
||||
self.id
|
||||
self.context().scope_id()
|
||||
}
|
||||
|
||||
/// Create a subscription that schedules a future render for the reference component
|
||||
///
|
||||
/// ## Notice: you should prefer using [`Self::schedule_update_any`] and [`Self::scope_id`]
|
||||
pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
|
||||
let (chan, id) = (self.tasks.sender.clone(), self.scope_id());
|
||||
Arc::new(move || drop(chan.unbounded_send(SchedulerMsg::Immediate(id))))
|
||||
self.context().schedule_update()
|
||||
}
|
||||
|
||||
/// Schedule an update for any component given its [`ScopeId`].
|
||||
@@ -229,61 +224,31 @@ impl<'src> ScopeState {
|
||||
///
|
||||
/// This method should be used when you want to schedule an update for a component
|
||||
pub fn schedule_update_any(&self) -> Arc<dyn Fn(ScopeId) + Send + Sync> {
|
||||
let chan = self.tasks.sender.clone();
|
||||
Arc::new(move |id| {
|
||||
chan.unbounded_send(SchedulerMsg::Immediate(id)).unwrap();
|
||||
})
|
||||
self.context().schedule_update_any()
|
||||
}
|
||||
|
||||
/// Mark this scope as dirty, and schedule a render for it.
|
||||
pub fn needs_update(&self) {
|
||||
self.needs_update_any(self.scope_id());
|
||||
self.context().needs_update()
|
||||
}
|
||||
|
||||
/// Get the [`ScopeId`] of a mounted component.
|
||||
///
|
||||
/// `ScopeId` is not unique for the lifetime of the [`crate::VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted.
|
||||
pub fn needs_update_any(&self, id: ScopeId) {
|
||||
self.tasks
|
||||
.sender
|
||||
.unbounded_send(SchedulerMsg::Immediate(id))
|
||||
.expect("Scheduler to exist if scope exists");
|
||||
self.context().needs_update_any(id)
|
||||
}
|
||||
|
||||
/// Return any context of type T if it exists on this scope
|
||||
pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||
self.shared_contexts
|
||||
.borrow()
|
||||
.iter()
|
||||
.find(|(k, _)| *k == TypeId::of::<T>())
|
||||
.map(|(_, v)| v)?
|
||||
.downcast_ref::<T>()
|
||||
.cloned()
|
||||
self.context().has_context()
|
||||
}
|
||||
|
||||
/// Try to retrieve a shared state with type `T` from any parent scope.
|
||||
///
|
||||
/// Clones the state if it exists.
|
||||
pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||
if let Some(this_ctx) = self.has_context() {
|
||||
return Some(this_ctx);
|
||||
}
|
||||
|
||||
let mut search_parent = self.parent;
|
||||
while let Some(parent_ptr) = search_parent {
|
||||
// safety: all parent pointers are valid thanks to the bump arena
|
||||
let parent = unsafe { &*parent_ptr };
|
||||
if let Some(shared) = parent
|
||||
.shared_contexts
|
||||
.borrow()
|
||||
.iter()
|
||||
.find(|(k, _)| *k == TypeId::of::<T>())
|
||||
{
|
||||
return shared.1.downcast_ref::<T>().cloned();
|
||||
}
|
||||
search_parent = parent.parent;
|
||||
}
|
||||
None
|
||||
self.context().consume_context()
|
||||
}
|
||||
|
||||
/// Expose state to children further down the [`crate::VirtualDom`] Tree. Requires `Clone` on the context to allow getting values down the tree.
|
||||
@@ -308,21 +273,7 @@ impl<'src> ScopeState {
|
||||
/// }
|
||||
/// ```
|
||||
pub fn provide_context<T: 'static + Clone>(&self, value: T) -> T {
|
||||
let mut contexts = self.shared_contexts.borrow_mut();
|
||||
|
||||
// If the context exists, swap it out for the new value
|
||||
for ctx in contexts.iter_mut() {
|
||||
// Swap the ptr directly
|
||||
if let Some(ctx) = ctx.1.downcast_mut::<T>() {
|
||||
std::mem::swap(ctx, &mut value.clone());
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// Else, just push it
|
||||
contexts.push((TypeId::of::<T>(), Box::new(value.clone())));
|
||||
|
||||
value
|
||||
self.context().provide_context(value)
|
||||
}
|
||||
|
||||
/// Provide a context to the root and then consume it
|
||||
@@ -333,52 +284,31 @@ impl<'src> ScopeState {
|
||||
/// Note that you should be checking if the context existed before trying to provide a new one. Providing a context
|
||||
/// when a context already exists will swap the context out for the new one, which may not be what you want.
|
||||
pub fn provide_root_context<T: 'static + Clone>(&self, context: T) -> T {
|
||||
let mut parent = self;
|
||||
|
||||
// Walk upwards until there is no more parent - and tada we have the root
|
||||
while let Some(next_parent) = parent.parent {
|
||||
parent = unsafe { &*next_parent };
|
||||
debug_assert_eq!(parent.scope_id(), ScopeId(0));
|
||||
}
|
||||
|
||||
parent.provide_context(context)
|
||||
self.context().provide_root_context(context)
|
||||
}
|
||||
|
||||
/// Pushes the future onto the poll queue to be polled after the component renders.
|
||||
pub fn push_future(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
||||
let id = self.tasks.spawn(self.id, fut);
|
||||
self.spawned_tasks.borrow_mut().insert(id);
|
||||
id
|
||||
self.context().push_future(fut)
|
||||
}
|
||||
|
||||
/// Spawns the future but does not return the [`TaskId`]
|
||||
pub fn spawn(&self, fut: impl Future<Output = ()> + 'static) {
|
||||
self.push_future(fut);
|
||||
self.context().spawn(fut);
|
||||
}
|
||||
|
||||
/// Spawn a future that Dioxus won't clean up when this component is unmounted
|
||||
///
|
||||
/// This is good for tasks that need to be run after the component has been dropped.
|
||||
pub fn spawn_forever(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
||||
// The root scope will never be unmounted so we can just add the task at the top of the app
|
||||
let id = self.tasks.spawn(ScopeId(0), fut);
|
||||
|
||||
// wake up the scheduler if it is sleeping
|
||||
self.tasks
|
||||
.sender
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(id))
|
||||
.expect("Scheduler should exist");
|
||||
|
||||
self.spawned_tasks.borrow_mut().insert(id);
|
||||
|
||||
id
|
||||
self.context().spawn_forever(fut)
|
||||
}
|
||||
|
||||
/// Informs the scheduler that this task is no longer needed and should be removed.
|
||||
///
|
||||
/// This drops the task immediately.
|
||||
pub fn remove_future(&self, id: TaskId) {
|
||||
self.tasks.remove(id);
|
||||
self.context().remove_future(id);
|
||||
}
|
||||
|
||||
/// Take a lazy [`crate::VNode`] structure and actually build it with the context of the efficient [`bumpalo::Bump`] allocator.
|
||||
@@ -481,7 +411,7 @@ impl<'src> ScopeState {
|
||||
/// fn(Scope<Props>) -> Element;
|
||||
/// async fn(Scope<Props<'_>>) -> Element;
|
||||
/// ```
|
||||
pub fn component<P>(
|
||||
pub fn component<P: 'static>(
|
||||
&'src self,
|
||||
component: fn(Scope<'src, P>) -> Element<'src>,
|
||||
props: P,
|
||||
@@ -564,8 +494,7 @@ impl<'src> ScopeState {
|
||||
|
||||
/// Mark this component as suspended and then return None
|
||||
pub fn suspend(&self) -> Option<Element> {
|
||||
self.suspended.set(true);
|
||||
None
|
||||
self.context().suspend()
|
||||
}
|
||||
|
||||
/// Store a value between renders. The foundational hook for all other hooks.
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::{
|
||||
mutations::Mutation,
|
||||
nodes::RenderReturn,
|
||||
nodes::{Template, TemplateId},
|
||||
runtime::Runtime,
|
||||
scopes::{ScopeId, ScopeState},
|
||||
AttributeValue, Element, Event, Scope,
|
||||
};
|
||||
@@ -174,24 +175,24 @@ use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
|
||||
/// }
|
||||
/// ```
|
||||
pub struct VirtualDom {
|
||||
pub(crate) scopes: Slab<ScopeState>,
|
||||
|
||||
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
|
||||
|
||||
// Maps a template path to a map of byteindexes to templates
|
||||
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template<'static>>>,
|
||||
pub(crate) scopes: Slab<Box<ScopeState>>,
|
||||
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
|
||||
pub(crate) scheduler: Rc<Scheduler>,
|
||||
|
||||
// Every element is actually a dual reference - one to the template and the other to the dynamic node in that template
|
||||
pub(crate) elements: Slab<ElementRef>,
|
||||
|
||||
// While diffing we need some sort of way of breaking off a stream of suspended mutations.
|
||||
pub(crate) scope_stack: Vec<ScopeId>,
|
||||
pub(crate) mutations: Mutations<'static>,
|
||||
|
||||
pub(crate) runtime: Rc<Runtime>,
|
||||
|
||||
// Currently suspended scopes
|
||||
pub(crate) suspended_scopes: FxHashSet<ScopeId>,
|
||||
|
||||
pub(crate) rx: futures_channel::mpsc::UnboundedReceiver<SchedulerMsg>,
|
||||
|
||||
pub(crate) mutations: Mutations<'static>,
|
||||
}
|
||||
|
||||
impl VirtualDom {
|
||||
@@ -251,16 +252,16 @@ impl VirtualDom {
|
||||
/// ```
|
||||
pub fn new_with_props<P: 'static>(root: fn(Scope<P>) -> Element, root_props: P) -> Self {
|
||||
let (tx, rx) = futures_channel::mpsc::unbounded();
|
||||
let scheduler = Scheduler::new(tx);
|
||||
let mut dom = Self {
|
||||
rx,
|
||||
scheduler: Scheduler::new(tx),
|
||||
templates: Default::default(),
|
||||
runtime: Runtime::new(scheduler),
|
||||
scopes: Default::default(),
|
||||
dirty_scopes: Default::default(),
|
||||
templates: Default::default(),
|
||||
elements: Default::default(),
|
||||
scope_stack: Vec::new(),
|
||||
dirty_scopes: BTreeSet::new(),
|
||||
suspended_scopes: FxHashSet::default(),
|
||||
mutations: Mutations::default(),
|
||||
suspended_scopes: Default::default(),
|
||||
};
|
||||
|
||||
let root = dom.new_scope(
|
||||
@@ -281,7 +282,7 @@ impl VirtualDom {
|
||||
///
|
||||
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
|
||||
pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
|
||||
self.scopes.get(id.0).map(|f| f.as_ref())
|
||||
self.scopes.get(id.0)
|
||||
}
|
||||
|
||||
/// Get the single scope at the top of the VirtualDom tree that will always be around
|
||||
@@ -301,10 +302,10 @@ impl VirtualDom {
|
||||
|
||||
/// Manually mark a scope as requiring a re-render
|
||||
///
|
||||
/// Whenever the VirtualDom "works", it will re-render this scope
|
||||
/// Whenever the Runtime "works", it will re-render this scope
|
||||
pub fn mark_dirty(&mut self, id: ScopeId) {
|
||||
if let Some(scope) = self.get_scope(id) {
|
||||
let height = scope.height;
|
||||
let height = scope.height();
|
||||
self.dirty_scopes.insert(DirtyScope { height, id });
|
||||
}
|
||||
}
|
||||
@@ -501,10 +502,11 @@ impl VirtualDom {
|
||||
if sync.template.get().name.rsplit_once(':').unwrap().0
|
||||
== template.name.rsplit_once(':').unwrap().0
|
||||
{
|
||||
let height = scope.height;
|
||||
let context = scope.context();
|
||||
let height = context.height;
|
||||
self.dirty_scopes.insert(DirtyScope {
|
||||
height,
|
||||
id: scope.id,
|
||||
id: context.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,3 +9,4 @@ edition = "2018"
|
||||
[dependencies]
|
||||
dioxus-core = { workspace = true }
|
||||
slab = { workspace = true }
|
||||
bumpalo = { version = "3.6" }
|
||||
@@ -0,0 +1,301 @@
|
||||
use std::{
|
||||
cell::{Cell, Ref, RefCell, RefMut},
|
||||
marker::PhantomData,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use bumpalo::Bump;
|
||||
|
||||
/// # Example
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// let data = String::from("hello world");
|
||||
/// let store = Store::default();
|
||||
/// let owner = store.owner();
|
||||
/// let key = owner.insert(&data);
|
||||
/// drop(data);
|
||||
/// assert_eq!(*key.read(), "hello world");
|
||||
/// ```
|
||||
#[allow(unused)]
|
||||
fn compile_fail() {}
|
||||
|
||||
#[test]
|
||||
fn reused() {
|
||||
let store = Store::default();
|
||||
let first_ptr;
|
||||
{
|
||||
let owner = store.owner();
|
||||
first_ptr = owner.insert(1).raw.data.as_ptr();
|
||||
drop(owner);
|
||||
}
|
||||
{
|
||||
let owner = store.owner();
|
||||
let second_ptr = owner.insert(1234).raw.data.as_ptr();
|
||||
assert_eq!(first_ptr, second_ptr);
|
||||
drop(owner);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaking_is_ok() {
|
||||
let data = String::from("hello world");
|
||||
let store = Store::default();
|
||||
let key;
|
||||
{
|
||||
// create an owner
|
||||
let owner = store.owner();
|
||||
// insert data into the store
|
||||
key = owner.insert(data);
|
||||
// don't drop the owner
|
||||
std::mem::forget(owner);
|
||||
}
|
||||
assert_eq!(key.try_read().as_deref(), Some(&"hello world".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drops() {
|
||||
let data = String::from("hello world");
|
||||
let store = Store::default();
|
||||
let key;
|
||||
{
|
||||
// create an owner
|
||||
let owner = store.owner();
|
||||
// insert data into the store
|
||||
key = owner.insert(data);
|
||||
// drop the owner
|
||||
}
|
||||
assert!(key.try_read().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn works() {
|
||||
let store = Store::default();
|
||||
let owner = store.owner();
|
||||
let key = owner.insert(1);
|
||||
|
||||
assert_eq!(*key.read(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_while_reading() {
|
||||
let store = Store::default();
|
||||
let owner = store.owner();
|
||||
let key;
|
||||
{
|
||||
let data: String = "hello world".to_string();
|
||||
key = owner.insert(data);
|
||||
}
|
||||
let value = key.read();
|
||||
owner.insert(&1);
|
||||
assert_eq!(*value, "hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn panics() {
|
||||
let store = Store::default();
|
||||
let owner = store.owner();
|
||||
let key = owner.insert(1);
|
||||
drop(owner);
|
||||
|
||||
assert_eq!(*key.read(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fuzz() {
|
||||
fn maybe_owner_scope(
|
||||
store: &Store,
|
||||
valid_keys: &mut Vec<CopyHandle<String>>,
|
||||
invalid_keys: &mut Vec<CopyHandle<String>>,
|
||||
path: &mut Vec<u8>,
|
||||
) {
|
||||
let branch_cutoff = 5;
|
||||
let children = if path.len() < branch_cutoff {
|
||||
rand::random::<u8>() % 4
|
||||
} else {
|
||||
rand::random::<u8>() % 2
|
||||
};
|
||||
|
||||
for i in 0..children {
|
||||
let owner = store.owner();
|
||||
let key = owner.insert(format!("hello world {path:?}"));
|
||||
valid_keys.push(key);
|
||||
path.push(i);
|
||||
// read all keys
|
||||
println!("{:?}", path);
|
||||
for key in valid_keys.iter() {
|
||||
let value = key.read();
|
||||
println!("{:?}", value);
|
||||
assert!(value.starts_with("hello world"));
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
for key in invalid_keys.iter() {
|
||||
assert!(!key.validate());
|
||||
}
|
||||
maybe_owner_scope(store, valid_keys, invalid_keys, path);
|
||||
invalid_keys.push(valid_keys.pop().unwrap());
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
|
||||
for _ in 0..10 {
|
||||
let store = Store::default();
|
||||
maybe_owner_scope(&store, &mut Vec::new(), &mut Vec::new(), &mut Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CopyHandle<T> {
|
||||
raw: MemoryLocation,
|
||||
#[cfg(debug_assertions)]
|
||||
generation: u32,
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: 'static> CopyHandle<T> {
|
||||
#[inline(always)]
|
||||
fn validate(&self) -> bool {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.raw.generation.get() == self.generation
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_read(&self) -> Option<Ref<'_, T>> {
|
||||
self.validate()
|
||||
.then(|| {
|
||||
Ref::filter_map(self.raw.data.borrow(), |any| {
|
||||
any.as_ref()?.downcast_ref::<T>()
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub fn read(&self) -> Ref<'_, T> {
|
||||
self.try_read().unwrap()
|
||||
}
|
||||
|
||||
pub fn try_write(&self) -> Option<RefMut<'_, T>> {
|
||||
self.validate()
|
||||
.then(|| {
|
||||
RefMut::filter_map(self.raw.data.borrow_mut(), |any| {
|
||||
any.as_mut()?.downcast_mut::<T>()
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub fn write(&self) -> RefMut<'_, T> {
|
||||
self.try_write().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Copy for CopyHandle<T> {}
|
||||
|
||||
impl<T> Clone for CopyHandle<T> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct MemoryLocation {
|
||||
data: &'static RefCell<Option<Box<dyn std::any::Any>>>,
|
||||
#[cfg(debug_assertions)]
|
||||
generation: &'static Cell<u32>,
|
||||
}
|
||||
|
||||
impl MemoryLocation {
|
||||
#[allow(unused)]
|
||||
fn drop(&self) {
|
||||
let old = self.data.borrow_mut().take();
|
||||
#[cfg(debug_assertions)]
|
||||
if old.is_some() {
|
||||
let new_generation = self.generation.get() + 1;
|
||||
self.generation.set(new_generation);
|
||||
}
|
||||
}
|
||||
|
||||
fn replace<T: 'static>(&mut self, value: T) -> CopyHandle<T> {
|
||||
let mut inner_mut = self.data.borrow_mut();
|
||||
|
||||
let raw = Box::new(value);
|
||||
let old = inner_mut.replace(raw);
|
||||
assert!(old.is_none());
|
||||
CopyHandle {
|
||||
raw: *self,
|
||||
#[cfg(debug_assertions)]
|
||||
generation: self.generation.get(),
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Store {
|
||||
bump: &'static Bump,
|
||||
recycled: Rc<RefCell<Vec<MemoryLocation>>>,
|
||||
}
|
||||
|
||||
impl Default for Store {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bump: Box::leak(Box::new(Bump::new())),
|
||||
recycled: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Store {
|
||||
fn recycle(&self, location: MemoryLocation) {
|
||||
location.drop();
|
||||
self.recycled.borrow_mut().push(location);
|
||||
}
|
||||
|
||||
fn claim(&self) -> MemoryLocation {
|
||||
if let Some(location) = self.recycled.borrow_mut().pop() {
|
||||
location
|
||||
} else {
|
||||
let data: &'static RefCell<_> = self.bump.alloc(RefCell::new(None));
|
||||
MemoryLocation {
|
||||
data,
|
||||
#[cfg(debug_assertions)]
|
||||
generation: self.bump.alloc(Cell::new(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn owner(&self) -> Owner {
|
||||
Owner {
|
||||
store: self.clone(),
|
||||
owned: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Owner {
|
||||
store: Store,
|
||||
owned: Rc<RefCell<Vec<MemoryLocation>>>,
|
||||
}
|
||||
|
||||
impl Owner {
|
||||
pub fn insert<T: 'static>(&self, value: T) -> CopyHandle<T> {
|
||||
let mut location = self.store.claim();
|
||||
let key = location.replace(value);
|
||||
self.owned.borrow_mut().push(location);
|
||||
key
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Owner {
|
||||
fn drop(&mut self) {
|
||||
for location in self.owned.borrow().iter() {
|
||||
self.store.recycle(*location)
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
-56
@@ -1,70 +1,65 @@
|
||||
use std::{
|
||||
cell::{Ref, RefMut},
|
||||
fmt::Display,
|
||||
marker::PhantomData,
|
||||
ops::{Add, Div, Mul, Sub},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
mod copy;
|
||||
mod rt;
|
||||
|
||||
use dioxus_core::ScopeState;
|
||||
pub use rt::*;
|
||||
|
||||
pub fn use_init_signal_rt(cx: &ScopeState) {
|
||||
cx.use_hook(|| {
|
||||
let rt = claim_rt(cx.schedule_update_any());
|
||||
cx.provide_context(rt);
|
||||
});
|
||||
}
|
||||
use dioxus_core::{
|
||||
prelude::{current_scope_id, schedule_update_any},
|
||||
ScopeId, ScopeState,
|
||||
};
|
||||
|
||||
pub fn use_signal<T: 'static>(cx: &ScopeState, f: impl FnOnce() -> T) -> Signal<T> {
|
||||
cx.use_hook(|| {
|
||||
let rt: &'static SignalRt = match cx.consume_context() {
|
||||
Some(rt) => rt,
|
||||
None => cx.provide_context(claim_rt(cx.schedule_update_any())),
|
||||
};
|
||||
|
||||
let id = rt.init(f());
|
||||
rt.subscribe(id, cx.scope_id());
|
||||
|
||||
struct SignalHook<T> {
|
||||
signal: Signal<T>,
|
||||
}
|
||||
|
||||
impl<T> Drop for SignalHook<T> {
|
||||
fn drop(&mut self) {
|
||||
self.signal.rt.remove(self.signal.id);
|
||||
}
|
||||
}
|
||||
|
||||
SignalHook {
|
||||
signal: Signal {
|
||||
id,
|
||||
rt,
|
||||
t: PhantomData,
|
||||
},
|
||||
}
|
||||
})
|
||||
.signal
|
||||
*cx.use_hook(|| Signal::new(f()))
|
||||
}
|
||||
|
||||
pub struct Signal<T> {
|
||||
id: usize,
|
||||
rt: &'static SignalRt,
|
||||
t: PhantomData<T>,
|
||||
struct SignalData<T> {
|
||||
subscribers: Vec<ScopeId>,
|
||||
update_any: Arc<dyn Fn(ScopeId)>,
|
||||
value: T,
|
||||
}
|
||||
|
||||
pub struct Signal<T: 'static> {
|
||||
inner: CopyValue<SignalData<T>>,
|
||||
}
|
||||
|
||||
impl<T: 'static> Signal<T> {
|
||||
pub fn new(value: T) -> Self {
|
||||
Self {
|
||||
inner: CopyValue::new(SignalData {
|
||||
subscribers: Vec::new(),
|
||||
update_any: schedule_update_any().expect("in a virtual dom"),
|
||||
value,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&self) -> Ref<T> {
|
||||
self.rt.read(self.id)
|
||||
if let Some(current_scope_id) = current_scope_id() {
|
||||
let mut inner = self.inner.write();
|
||||
if !inner.subscribers.contains(¤t_scope_id) {
|
||||
inner.subscribers.push(current_scope_id);
|
||||
}
|
||||
}
|
||||
Ref::map(self.inner.read(), |v| &v.value)
|
||||
}
|
||||
|
||||
pub fn write(&self) -> RefMut<T> {
|
||||
self.rt.write(self.id)
|
||||
let inner = self.inner.write();
|
||||
for &scope_id in &inner.subscribers {
|
||||
(inner.update_any)(scope_id);
|
||||
}
|
||||
|
||||
RefMut::map(inner, |v| &mut v.value)
|
||||
}
|
||||
|
||||
pub fn set(&mut self, value: T) {
|
||||
self.rt.set(self.id, value);
|
||||
*self.write() = value;
|
||||
}
|
||||
|
||||
pub fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||
@@ -72,23 +67,15 @@ impl<T: 'static> Signal<T> {
|
||||
f(&*write)
|
||||
}
|
||||
|
||||
pub fn update<O>(&self, _f: impl FnOnce(&mut T) -> O) -> O {
|
||||
pub fn update<O>(&self, f: impl FnOnce(&mut T) -> O) -> O {
|
||||
let mut write = self.write();
|
||||
_f(&mut *write)
|
||||
f(&mut *write)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + 'static> Signal<T> {
|
||||
pub fn get(&self) -> T {
|
||||
self.rt.get(self.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + 'static> std::ops::Deref for Signal<T> {
|
||||
type Target = dyn Fn() -> T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.rt.getter(self.id)
|
||||
self.read().clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +89,7 @@ impl<T> Copy for Signal<T> {}
|
||||
|
||||
impl<T: Display + 'static> Display for Signal<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.rt.with::<T, _>(self.id, |v| T::fmt(v, f))
|
||||
self.with(|v| Display::fmt(v, f))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+42
-98
@@ -1,121 +1,65 @@
|
||||
use std::{any::Any, cell::RefCell, sync::Arc};
|
||||
use std::cell::{Ref, RefMut};
|
||||
|
||||
use dioxus_core::ScopeId;
|
||||
use slab::Slab;
|
||||
use std::rc::Rc;
|
||||
|
||||
thread_local! {
|
||||
// we cannot drop these since any future might be using them
|
||||
static RUNTIMES: RefCell<Vec<&'static SignalRt>> = RefCell::new(Vec::new());
|
||||
}
|
||||
use dioxus_core::prelude::{consume_context, provide_root_context};
|
||||
|
||||
/// Provide the runtime for signals
|
||||
///
|
||||
/// This will reuse dead runtimes
|
||||
pub fn claim_rt(update_any: Arc<dyn Fn(ScopeId)>) -> &'static SignalRt {
|
||||
RUNTIMES.with(|runtimes| {
|
||||
if let Some(rt) = runtimes.borrow_mut().pop() {
|
||||
return rt;
|
||||
use crate::copy::{CopyHandle, Owner, Store};
|
||||
|
||||
fn current_store() -> Store {
|
||||
match consume_context() {
|
||||
Some(rt) => rt,
|
||||
None => {
|
||||
let store = Store::default();
|
||||
provide_root_context(store).expect("in a virtual dom")
|
||||
}
|
||||
|
||||
Box::leak(Box::new(SignalRt {
|
||||
signals: RefCell::new(Slab::new()),
|
||||
update_any,
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Push this runtime into the global runtime list
|
||||
pub fn reclam_rt(_rt: &'static SignalRt) {
|
||||
RUNTIMES.with(|runtimes| {
|
||||
runtimes.borrow_mut().push(_rt);
|
||||
});
|
||||
fn current_owner() -> Rc<Owner> {
|
||||
match consume_context() {
|
||||
Some(rt) => rt,
|
||||
None => {
|
||||
let owner = Rc::new(current_store().owner());
|
||||
provide_root_context(owner).expect("in a virtual dom")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SignalRt {
|
||||
pub(crate) signals: RefCell<Slab<Inner>>,
|
||||
pub(crate) update_any: Arc<dyn Fn(ScopeId)>,
|
||||
impl<T> Copy for CopyValue<T> {}
|
||||
|
||||
impl<T> Clone for CopyValue<T> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl SignalRt {
|
||||
pub fn init<T: 'static>(&'static self, val: T) -> usize {
|
||||
self.signals.borrow_mut().insert(Inner {
|
||||
value: Box::new(val),
|
||||
subscribers: Vec::new(),
|
||||
getter: None,
|
||||
})
|
||||
}
|
||||
pub struct CopyValue<T: 'static> {
|
||||
pub value: CopyHandle<T>,
|
||||
}
|
||||
|
||||
pub fn subscribe(&self, id: usize, subscriber: ScopeId) {
|
||||
self.signals.borrow_mut()[id].subscribers.push(subscriber);
|
||||
}
|
||||
impl<T: 'static> CopyValue<T> {
|
||||
pub fn new(value: T) -> Self {
|
||||
let owner = current_owner();
|
||||
|
||||
pub fn get<T: Clone + 'static>(&self, id: usize) -> T {
|
||||
self.signals.borrow()[id]
|
||||
.value
|
||||
.downcast_ref::<T>()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn set<T: 'static>(&self, id: usize, value: T) {
|
||||
let mut signals = self.signals.borrow_mut();
|
||||
let inner = &mut signals[id];
|
||||
inner.value = Box::new(value);
|
||||
|
||||
for subscriber in inner.subscribers.iter() {
|
||||
(self.update_any)(*subscriber);
|
||||
Self {
|
||||
value: owner.insert(value),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&self, id: usize) {
|
||||
self.signals.borrow_mut().remove(id);
|
||||
pub fn try_read(&self) -> Option<Ref<'_, T>> {
|
||||
self.value.try_read()
|
||||
}
|
||||
|
||||
pub fn with<T: 'static, O>(&self, id: usize, f: impl FnOnce(&T) -> O) -> O {
|
||||
let signals = self.signals.borrow();
|
||||
let inner = &signals[id];
|
||||
let inner = inner.value.downcast_ref::<T>().unwrap();
|
||||
f(inner)
|
||||
pub fn read(&self) -> Ref<'_, T> {
|
||||
self.value.read()
|
||||
}
|
||||
|
||||
pub(crate) fn read<T: 'static>(&self, id: usize) -> std::cell::Ref<T> {
|
||||
let signals = self.signals.borrow();
|
||||
std::cell::Ref::map(signals, |signals| {
|
||||
signals[id].value.downcast_ref::<T>().unwrap()
|
||||
})
|
||||
pub fn try_write(&self) -> Option<RefMut<'_, T>> {
|
||||
self.value.try_write()
|
||||
}
|
||||
|
||||
pub(crate) fn write<T: 'static>(&self, id: usize) -> std::cell::RefMut<T> {
|
||||
let signals = self.signals.borrow_mut();
|
||||
std::cell::RefMut::map(signals, |signals| {
|
||||
signals[id].value.downcast_mut::<T>().unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn getter<T: 'static + Clone>(&self, id: usize) -> &dyn Fn() -> T {
|
||||
let mut signals = self.signals.borrow_mut();
|
||||
let inner = &mut signals[id];
|
||||
let r = inner.getter.as_mut();
|
||||
|
||||
if r.is_none() {
|
||||
let rt = self;
|
||||
let r = move || rt.get::<T>(id);
|
||||
let getter: Box<dyn Fn() -> T> = Box::new(r);
|
||||
let getter: Box<dyn Fn()> = unsafe { std::mem::transmute(getter) };
|
||||
|
||||
inner.getter = Some(getter);
|
||||
}
|
||||
|
||||
let r = inner.getter.as_ref().unwrap();
|
||||
|
||||
unsafe { std::mem::transmute::<&dyn Fn(), &dyn Fn() -> T>(r) }
|
||||
pub fn write(&self) -> RefMut<'_, T> {
|
||||
self.value.write()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Inner {
|
||||
pub value: Box<dyn Any>,
|
||||
pub subscribers: Vec<ScopeId>,
|
||||
|
||||
// todo: this has a soundness hole in it that you might not run into
|
||||
pub getter: Option<Box<dyn Fn()>>,
|
||||
}
|
||||
Reference in new issue
Block a user