Fix: notify parent store when child is marked dirty (#5365)
* add tests * fix: include root node when store is marked dirty * fix tracking deep subscriptions. We need to walk all parents for deep subscriptions instead of relying on tracking all children recursively during the initial read since some children may not exist yet * avoid double notifying parents when children re-visit parents in deep mark dirty --------- Co-authored-by: Evan Almloff <evanalmloff@gmail.com>
This commit is contained in:
1 parent
35826114c5
commit
da445fdeb6
3 files changed
+737
-85
No files matched your search
@@ -151,7 +151,7 @@ impl<Lens> SelectorScope<Lens> {
|
||||
|
||||
/// Track this scope recursively.
|
||||
pub fn track(&self) {
|
||||
self.store.track_recursive(&self.path);
|
||||
self.store.track_deep(&self.path);
|
||||
}
|
||||
|
||||
/// Mark this scope as dirty recursively.
|
||||
@@ -161,7 +161,7 @@ impl<Lens> SelectorScope<Lens> {
|
||||
|
||||
/// Mark this scope as dirty shallowly.
|
||||
pub fn mark_dirty_shallow(&self) {
|
||||
self.store.mark_dirty_shallow(&self.path);
|
||||
self.store.mark_node_dirty(&self.path);
|
||||
}
|
||||
|
||||
/// Mark this scope as dirty at and after the given index.
|
||||
@@ -210,7 +210,7 @@ impl<Lens: Readable> Readable for SelectorScope<Lens> {
|
||||
}
|
||||
|
||||
fn subscribers(&self) -> Subscribers {
|
||||
self.store.subscribers(&self.path)
|
||||
self.store.shallow_subscribers(&self.path)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
use dioxus_core::{ReactiveContext, SubscriberList, Subscribers};
|
||||
use dioxus_signals::{CopyValue, ReadableExt, SyncStorage, Writable, WritableExt};
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Debug;
|
||||
use std::hash::BuildHasher;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
hash::Hash,
|
||||
ops::Deref,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::ops::BitOrAssign;
|
||||
use std::{collections::HashMap, hash::Hash, ops::Deref, sync::Arc};
|
||||
|
||||
/// A single node in the [`StoreSubscriptions`] tree. Each path is a specific view into the store
|
||||
/// and can be subscribed to and marked dirty separately. If the whole store is read or written to, all
|
||||
/// nodes in the subtree are subscribed to or marked as dirty.
|
||||
/// and can be subscribed to and marked dirty separately.
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct SelectorNode {
|
||||
subscribers: HashSet<ReactiveContext>,
|
||||
subscribers: HashMap<ReactiveContext, SubscriptionDepth>,
|
||||
root: HashMap<PathKey, SelectorNode>,
|
||||
}
|
||||
|
||||
@@ -61,7 +57,7 @@ impl SelectorNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get paths to only children before a certain index.
|
||||
/// Get paths to children at and after a certain index.
|
||||
///
|
||||
/// This is used when inserting a new item into a list.
|
||||
/// Items after the index that is inserted need to be marked dirty because the value that index points to may have changed.
|
||||
@@ -75,7 +71,7 @@ impl SelectorNode {
|
||||
return;
|
||||
};
|
||||
|
||||
// Mark the nodes before the index as dirty
|
||||
// Mark the nodes at and after the index as dirty
|
||||
for (i, child) in node.root.iter() {
|
||||
if *i as usize >= index {
|
||||
let mut child_path: Vec<PathKey> = path.into();
|
||||
@@ -85,18 +81,67 @@ impl SelectorNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a path from the subscription tree
|
||||
fn remove(&mut self, path: &[PathKey]) {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.subscribers.is_empty() && self.root.is_empty()
|
||||
}
|
||||
|
||||
fn prune_if_empty(&mut self, path: &[PathKey]) {
|
||||
let [first, rest @ ..] = path else {
|
||||
return;
|
||||
};
|
||||
if let Some(node) = self.root.get_mut(first) {
|
||||
if rest.is_empty() {
|
||||
let Some(child) = self.root.get_mut(first) else {
|
||||
return;
|
||||
};
|
||||
child.prune_if_empty(rest);
|
||||
if child.is_empty() {
|
||||
self.root.remove(first);
|
||||
} else {
|
||||
node.remove(rest);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_subscriber(&mut self, reactive_context: ReactiveContext, depth: SubscriptionDepth) {
|
||||
self.subscribers
|
||||
.entry(reactive_context)
|
||||
.and_modify(|existing_depth| {
|
||||
*existing_depth |= depth;
|
||||
})
|
||||
.or_insert(depth);
|
||||
}
|
||||
|
||||
fn remove_subscriber(&mut self, reactive_context: &ReactiveContext) {
|
||||
self.subscribers.remove(reactive_context);
|
||||
}
|
||||
|
||||
// ReactiveContext uses stable pointer/id-based Eq+Hash, so it is safe as a map key here.
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
fn take_subscribers(&mut self) -> HashMap<ReactiveContext, SubscriptionDepth> {
|
||||
std::mem::take(&mut self.subscribers)
|
||||
}
|
||||
|
||||
// ReactiveContext uses stable pointer/id-based Eq+Hash, so it is safe as a map key here.
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
fn restore_subscribers(&mut self, subscribers: HashMap<ReactiveContext, SubscriptionDepth>) {
|
||||
self.subscribers.extend(subscribers);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum SubscriptionDepth {
|
||||
Shallow,
|
||||
Deep,
|
||||
}
|
||||
|
||||
impl BitOrAssign for SubscriptionDepth {
|
||||
fn bitor_assign(&mut self, rhs: Self) {
|
||||
match (*self, rhs) {
|
||||
(SubscriptionDepth::Shallow, SubscriptionDepth::Shallow) => {}
|
||||
_ => *self = SubscriptionDepth::Deep,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SubscriptionDepth {
|
||||
fn includes_deep(self) -> bool {
|
||||
matches!(self, Self::Deep)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +179,14 @@ impl TinyVec {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_slice(path: &[PathKey]) -> Self {
|
||||
let mut out = Self::new();
|
||||
for key in path {
|
||||
out.push(*key);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub(crate) const fn push(&mut self, index: u16) {
|
||||
if self.length < self.path.len() {
|
||||
self.path[self.length] = index;
|
||||
@@ -194,77 +247,68 @@ impl StoreSubscriptions {
|
||||
(self.inner.write_unchecked().hasher.hash_one(index) % PathKey::MAX as u64) as PathKey
|
||||
}
|
||||
|
||||
/// Subscribe to a specific path in the store.
|
||||
/// Subscribe shallowly to a specific path in the store.
|
||||
///
|
||||
/// Shallow subscriptions rerun only when this exact path is written to.
|
||||
pub(crate) fn track(&self, key: &[PathKey]) {
|
||||
if let Some(rc) = ReactiveContext::current() {
|
||||
let subscribers = self.subscribers(key);
|
||||
let subscribers = self.shallow_subscribers(key);
|
||||
rc.subscribe(subscribers);
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe to a path and all of its children recursively. This should be called any time we give out
|
||||
/// a raw reference to a store, because the user could read any level of the store.
|
||||
pub(crate) fn track_recursive(&self, key: &[PathKey]) {
|
||||
/// Subscribe deeply to a specific path in the store.
|
||||
///
|
||||
/// Deep subscriptions rerun when this path or any descendant path is written to.
|
||||
pub(crate) fn track_deep(&self, key: &[PathKey]) {
|
||||
if let Some(rc) = ReactiveContext::current() {
|
||||
let mut paths = Vec::new();
|
||||
{
|
||||
let mut write = self.inner.write_unchecked();
|
||||
|
||||
let root = write.root.get_mut_or_default(key);
|
||||
let mut nodes = vec![(key.to_vec(), &*root)];
|
||||
while let Some((path, node)) = nodes.pop() {
|
||||
for (child_key, child_node) in &node.root {
|
||||
let mut new_path = path.clone();
|
||||
new_path.push(*child_key);
|
||||
nodes.push((new_path, child_node));
|
||||
}
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
for path in paths {
|
||||
let subscribers = self.subscribers(&path);
|
||||
let subscribers = self.deep_subscribers(key);
|
||||
rc.subscribe(subscribers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark the node and all its children as dirty
|
||||
/// Mark the written node and its descendants as dirty.
|
||||
///
|
||||
/// Deep subscribers on ancestor nodes are also notified because child writes
|
||||
/// change the value observed by a deep read of the parent.
|
||||
pub(crate) fn mark_dirty(&self, key: &[PathKey]) {
|
||||
let paths = {
|
||||
self.mark_node_dirty(key);
|
||||
let descendant_paths = {
|
||||
let read = &self.inner.read_unchecked();
|
||||
let Some(node) = read.root.get(key) else {
|
||||
return;
|
||||
};
|
||||
let mut paths = Vec::new();
|
||||
node.paths_under(key, &mut paths);
|
||||
if let Some(node) = read.root.get(key) {
|
||||
for (child_key, child_node) in &node.root {
|
||||
let mut child_path: Vec<PathKey> = key.into();
|
||||
child_path.push(*child_key);
|
||||
child_node.paths_under(&child_path, &mut paths);
|
||||
}
|
||||
}
|
||||
paths
|
||||
};
|
||||
for path in paths {
|
||||
self.mark_dirty_shallow(&path);
|
||||
for path in descendant_paths {
|
||||
self.mark_node_subscribers_dirty(&path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark a single node as dirty
|
||||
pub(crate) fn mark_dirty_shallow(&self, key: &[PathKey]) {
|
||||
// We cannot hold the subscribers lock while calling mark_dirty, because mark_dirty can run user code which may cause a new subscriber to be added. If we hold the lock, we will deadlock.
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let mut subscribers = {
|
||||
let mut write = self.inner.write_unchecked();
|
||||
let Some(node) = write.root.get_mut(key) else {
|
||||
return;
|
||||
};
|
||||
std::mem::take(&mut node.subscribers)
|
||||
};
|
||||
subscribers.retain(|reactive_context| reactive_context.mark_dirty());
|
||||
// Extend the subscribers list instead of overwriting it in case a subscriber is added while reactive contexts are marked dirty
|
||||
let mut write = self.inner.write_unchecked();
|
||||
let Some(node) = write.root.get_mut(key) else {
|
||||
return;
|
||||
};
|
||||
node.subscribers.extend(subscribers);
|
||||
/// Mark all subscribers on a single node as dirty.
|
||||
///
|
||||
/// Deep subscribers on ancestors are also notified because an exact write to
|
||||
/// this path changes any deep read of its parents.
|
||||
pub(crate) fn mark_node_dirty(&self, key: &[PathKey]) {
|
||||
for i in 0..key.len() {
|
||||
self.mark_ancestor_deep_subscribers_dirty(&key[..i]);
|
||||
}
|
||||
self.mark_node_subscribers_dirty(key);
|
||||
}
|
||||
|
||||
/// Mark all nodes after the index and their children as dirty
|
||||
/// Mark only deep subscribers for a single ancestor node as dirty.
|
||||
fn mark_ancestor_deep_subscribers_dirty(&self, key: &[PathKey]) {
|
||||
self.retain_subscribers(key, |reactive_context, depth| {
|
||||
!depth.includes_deep() || reactive_context.mark_dirty()
|
||||
});
|
||||
}
|
||||
|
||||
/// Mark all nodes at and after the index and their children as dirty.
|
||||
pub(crate) fn mark_dirty_at_and_after_index(&self, key: &[PathKey], index: usize) {
|
||||
let paths = {
|
||||
let read = self.inner.read_unchecked();
|
||||
@@ -273,25 +317,76 @@ impl StoreSubscriptions {
|
||||
paths
|
||||
};
|
||||
for path in paths {
|
||||
self.mark_dirty_shallow(&path);
|
||||
self.mark_node_dirty(&path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a subscriber list for a specific path in the store. This is used to subscribe to changes
|
||||
/// to a specific path in the store and remove the node from the subscription tree when it is no longer needed.
|
||||
pub(crate) fn subscribers(&self, key: &[PathKey]) -> Subscribers {
|
||||
/// Get the shallow subscribers for a specific path in the store.
|
||||
pub(crate) fn shallow_subscribers(&self, key: &[PathKey]) -> Subscribers {
|
||||
Arc::new(StoreSubscribers {
|
||||
subscriptions: *self,
|
||||
path: key.to_vec().into_boxed_slice(),
|
||||
path: TinyVec::from_slice(key),
|
||||
depth: SubscriptionDepth::Shallow,
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Get the deep subscribers for a specific path in the store.
|
||||
pub(crate) fn deep_subscribers(&self, key: &[PathKey]) -> Subscribers {
|
||||
Arc::new(StoreSubscribers {
|
||||
subscriptions: *self,
|
||||
path: TinyVec::from_slice(key),
|
||||
depth: SubscriptionDepth::Deep,
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
fn retain_subscribers(
|
||||
&self,
|
||||
key: &[PathKey],
|
||||
mut retain: impl FnMut(&ReactiveContext, SubscriptionDepth) -> bool,
|
||||
) {
|
||||
// We cannot hold the subscribers lock while calling mark_dirty, because
|
||||
// mark_dirty can run user code which may cause a new subscriber to be
|
||||
// added. If we hold the lock, we will deadlock.
|
||||
// ReactiveContext uses stable pointer/id-based Eq+Hash, so it is safe as a map key here.
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let mut subscribers = {
|
||||
let mut write = self.inner.write_unchecked();
|
||||
let Some(node) = write.root.get_mut(key) else {
|
||||
return;
|
||||
};
|
||||
node.take_subscribers()
|
||||
};
|
||||
subscribers.retain(|reactive_context, depth| retain(reactive_context, *depth));
|
||||
|
||||
// Extend the subscribers list instead of overwriting it in case a
|
||||
// subscriber is added while reactive contexts are marked dirty.
|
||||
let mut write = self.inner.write_unchecked();
|
||||
let Some(node) = write.root.get_mut(key) else {
|
||||
return;
|
||||
};
|
||||
node.restore_subscribers(subscribers);
|
||||
}
|
||||
|
||||
fn mark_node_subscribers_dirty(&self, key: &[PathKey]) {
|
||||
self.retain_subscribers(key, |reactive_context, _| reactive_context.mark_dirty());
|
||||
}
|
||||
}
|
||||
|
||||
/// A subscriber list implementation that handles garbage collection of the subscription tree.
|
||||
struct StoreSubscribers {
|
||||
subscriptions: StoreSubscriptions,
|
||||
path: Box<[PathKey]>,
|
||||
path: TinyVec,
|
||||
depth: SubscriptionDepth,
|
||||
}
|
||||
|
||||
impl StoreSubscribers {
|
||||
fn visible_prefixes(&self) -> impl Iterator<Item = &[PathKey]> {
|
||||
(0..=self.path.len())
|
||||
.rev()
|
||||
.map(move |len| &self.path[..len])
|
||||
}
|
||||
}
|
||||
|
||||
impl SubscriberList for StoreSubscribers {
|
||||
@@ -301,7 +396,7 @@ impl SubscriberList for StoreSubscribers {
|
||||
return;
|
||||
};
|
||||
let node = write.root.get_mut_or_default(&self.path);
|
||||
node.subscribers.insert(subscriber);
|
||||
node.add_subscriber(subscriber, self.depth);
|
||||
}
|
||||
|
||||
/// Remove a subscriber from the subscription list for this path in the store. If the node has no subscribers left
|
||||
@@ -310,12 +405,23 @@ impl SubscriberList for StoreSubscribers {
|
||||
let Ok(mut write) = self.subscriptions.inner.try_write_unchecked() else {
|
||||
return;
|
||||
};
|
||||
let Some(node) = write.root.get_mut(&self.path) else {
|
||||
return;
|
||||
let mut empty_prefixes = Vec::new();
|
||||
for prefix in self.visible_prefixes() {
|
||||
let Some(node) = write.root.get_mut(prefix) else {
|
||||
continue;
|
||||
};
|
||||
node.subscribers.remove(subscriber);
|
||||
if node.subscribers.is_empty() && node.root.is_empty() {
|
||||
write.root.remove(&self.path);
|
||||
let Some(depth) = node.subscribers.get(subscriber).copied() else {
|
||||
continue;
|
||||
};
|
||||
if prefix.len() == self.path.len() || depth.includes_deep() {
|
||||
node.remove_subscriber(subscriber);
|
||||
if node.is_empty() {
|
||||
empty_prefixes.push(prefix.to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
for prefix in empty_prefixes {
|
||||
write.root.prune_if_empty(&prefix);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,9 +430,72 @@ impl SubscriberList for StoreSubscribers {
|
||||
let Ok(read) = self.subscriptions.inner.try_read() else {
|
||||
return;
|
||||
};
|
||||
let Some(node) = read.root.get(&self.path) else {
|
||||
return;
|
||||
// ReactiveContext uses stable pointer/id-based Eq+Hash, so it is safe in this set.
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let mut seen = HashSet::new();
|
||||
for prefix in self.visible_prefixes() {
|
||||
let Some(node) = read.root.get(prefix) else {
|
||||
continue;
|
||||
};
|
||||
node.subscribers.iter().for_each(f);
|
||||
for (reactive_context, depth) in &node.subscribers {
|
||||
if (prefix.len() == self.path.len() || depth.includes_deep())
|
||||
&& seen.insert(*reactive_context)
|
||||
{
|
||||
f(reactive_context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use dioxus_core::{ScopeId, VNode, VirtualDom};
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
fn empty_app() -> dioxus_core::Element {
|
||||
VNode::empty()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn counting_context(counter: Arc<AtomicUsize>) -> ReactiveContext {
|
||||
ReactiveContext::new_with_callback(
|
||||
move || {
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
},
|
||||
ScopeId::ROOT,
|
||||
std::panic::Location::caller(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mark_dirty_marks_descendants_without_remarking_ancestors() {
|
||||
let dom = VirtualDom::new(empty_app);
|
||||
|
||||
dom.in_scope(ScopeId::ROOT, || {
|
||||
let subscriptions = StoreSubscriptions::new();
|
||||
|
||||
let root_count = Arc::new(AtomicUsize::new(0));
|
||||
let root = counting_context(root_count.clone());
|
||||
root.subscribe(subscriptions.deep_subscribers(&[]));
|
||||
|
||||
let child_count = Arc::new(AtomicUsize::new(0));
|
||||
let child = counting_context(child_count.clone());
|
||||
child.subscribe(subscriptions.deep_subscribers(&[1, 2]));
|
||||
|
||||
let leaf_count = Arc::new(AtomicUsize::new(0));
|
||||
let leaf = counting_context(leaf_count.clone());
|
||||
leaf.subscribe(subscriptions.shallow_subscribers(&[1, 2, 3]));
|
||||
|
||||
subscriptions.mark_dirty(&[1]);
|
||||
|
||||
assert_eq!(root_count.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(child_count.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(leaf_count.load(Ordering::Relaxed), 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core::Mutation::*;
|
||||
use dioxus_core::*;
|
||||
use dioxus_stores::GlobalStore;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
#[derive(Store, std::fmt::Debug, Clone, Default)]
|
||||
struct X {
|
||||
inner1: i32,
|
||||
inner2: i32,
|
||||
}
|
||||
|
||||
#[derive(Store, std::fmt::Debug, Clone, Default)]
|
||||
struct Y {
|
||||
outer: X,
|
||||
other: i32,
|
||||
}
|
||||
|
||||
#[derive(Store, std::fmt::Debug, Clone, Default)]
|
||||
struct Z {
|
||||
items: Vec<i32>,
|
||||
other: i32,
|
||||
}
|
||||
|
||||
#[derive(Store, std::fmt::Debug, Clone, Default)]
|
||||
struct W {
|
||||
nested: Y,
|
||||
other: i32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn children_see_parent_write() {
|
||||
fn default_x() -> X {
|
||||
X::default()
|
||||
}
|
||||
|
||||
static STORE: GlobalStore<X> = Global::new(default_x);
|
||||
|
||||
fn app() -> Element {
|
||||
let x = STORE.resolve();
|
||||
let inner1 = x.inner1();
|
||||
rsx! {
|
||||
"x = {inner1}"
|
||||
}
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
dom.rebuild_in_place();
|
||||
|
||||
dom.in_scope(ScopeId::APP, || {
|
||||
STORE.resolve().set(X {
|
||||
inner1: 1,
|
||||
inner2: 0,
|
||||
});
|
||||
});
|
||||
|
||||
let edits = dom.render_immediate_to_vec();
|
||||
assert_eq!(
|
||||
edits.edits,
|
||||
[SetText {
|
||||
value: "x = 1".into(),
|
||||
id: ElementId(1)
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
// https://github.com/DioxusLabs/dioxus/issues/5363
|
||||
// When a store is written through a child lens, readers of the root store
|
||||
// must be notified. A child node's subscriber visit must surface deep
|
||||
// subscriptions from ancestor nodes.
|
||||
#[test]
|
||||
fn parents_see_child_write() {
|
||||
fn default_x() -> X {
|
||||
X::default()
|
||||
}
|
||||
|
||||
static STORE: GlobalStore<X> = Global::new(default_x);
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
struct RunCounter {
|
||||
root: usize,
|
||||
inner1: usize,
|
||||
inner2: usize,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn inner1_reader(value: Store<i32>, counter: Rc<RefCell<RunCounter>>) -> Element {
|
||||
counter.borrow_mut().inner1 += 1;
|
||||
rsx! {
|
||||
"inner1 = {value}"
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn inner2_reader(value: Store<i32>, counter: Rc<RefCell<RunCounter>>) -> Element {
|
||||
counter.borrow_mut().inner2 += 1;
|
||||
rsx! {
|
||||
"inner2 = {value}"
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn root_reader(value: Store<X>, counter: Rc<RefCell<RunCounter>>) -> Element {
|
||||
counter.borrow_mut().root += 1;
|
||||
rsx! {
|
||||
"x = {value:?}"
|
||||
}
|
||||
}
|
||||
|
||||
let counter = Rc::new(RefCell::new(RunCounter::default()));
|
||||
let mut dom = VirtualDom::new_with_props(
|
||||
|counter: Rc<RefCell<RunCounter>>| {
|
||||
let x = STORE.resolve();
|
||||
rsx! {
|
||||
inner1_reader { value: x.inner1(), counter: counter.clone() }
|
||||
inner2_reader { value: x.inner2(), counter: counter.clone() }
|
||||
root_reader { value: x, counter: counter.clone() }
|
||||
}
|
||||
},
|
||||
counter.clone(),
|
||||
);
|
||||
dom.rebuild_in_place();
|
||||
|
||||
{
|
||||
let current_counter = counter.borrow();
|
||||
assert_eq!(current_counter.root, 1);
|
||||
assert_eq!(current_counter.inner1, 1);
|
||||
assert_eq!(current_counter.inner2, 1);
|
||||
}
|
||||
|
||||
dom.in_scope(ScopeId::APP, || {
|
||||
STORE.resolve().inner1().set(1);
|
||||
});
|
||||
|
||||
dom.render_immediate(&mut NoOpMutations);
|
||||
|
||||
{
|
||||
let current_counter = counter.borrow();
|
||||
assert_eq!(current_counter.root, 2);
|
||||
assert_eq!(current_counter.inner1, 2);
|
||||
assert_eq!(current_counter.inner2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_root_reader_sees_future_child_write() {
|
||||
fn default_x() -> X {
|
||||
X::default()
|
||||
}
|
||||
|
||||
static STORE: GlobalStore<X> = Global::new(default_x);
|
||||
|
||||
let runs = Rc::new(RefCell::new(0));
|
||||
let mut dom = VirtualDom::new_with_props(
|
||||
|runs: Rc<RefCell<usize>>| {
|
||||
*runs.borrow_mut() += 1;
|
||||
let x = STORE.resolve();
|
||||
rsx! {
|
||||
"x = {x:?}"
|
||||
}
|
||||
},
|
||||
runs.clone(),
|
||||
);
|
||||
dom.rebuild_in_place();
|
||||
|
||||
assert_eq!(*runs.borrow(), 1);
|
||||
|
||||
dom.in_scope(ScopeId::APP, || {
|
||||
STORE.resolve().inner1().set(1);
|
||||
});
|
||||
|
||||
dom.render_immediate(&mut NoOpMutations);
|
||||
|
||||
assert_eq!(*runs.borrow(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boxed_child_subscribers_remove_visited_parent_deep_subscriber() {
|
||||
fn default_x() -> X {
|
||||
X::default()
|
||||
}
|
||||
|
||||
static STORE: GlobalStore<X> = Global::new(default_x);
|
||||
|
||||
fn app() -> Element {
|
||||
let x = STORE.resolve();
|
||||
rsx! {
|
||||
"x = {x:?}"
|
||||
}
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
dom.rebuild_in_place();
|
||||
|
||||
dom.in_scope(ScopeId::APP, || {
|
||||
let boxed_child = ReadSignal::new(STORE.resolve().inner1());
|
||||
let subscribers = boxed_child.subscribers();
|
||||
|
||||
let mut visited = Vec::new();
|
||||
subscribers.visit(|subscriber| visited.push(*subscriber));
|
||||
assert_eq!(
|
||||
visited.len(),
|
||||
1,
|
||||
"expected exactly one visited parent deep subscriber, got {:?}",
|
||||
visited.len()
|
||||
);
|
||||
|
||||
subscribers.remove(&visited[0]);
|
||||
|
||||
let mut after_remove = Vec::new();
|
||||
subscribers.visit(|subscriber| after_remove.push(*subscriber));
|
||||
assert!(
|
||||
after_remove.is_empty(),
|
||||
"removing through boxed child subscribers should remove visited subscribers, got {} left",
|
||||
after_remove.len()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_parent_reader_sees_grandchild_write() {
|
||||
fn default_y() -> Y {
|
||||
Y::default()
|
||||
}
|
||||
|
||||
static STORE: GlobalStore<Y> = Global::new(default_y);
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
struct RunCounter {
|
||||
outer: usize,
|
||||
other: usize,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn outer_reader(value: Store<X>, counter: Rc<RefCell<RunCounter>>) -> Element {
|
||||
counter.borrow_mut().outer += 1;
|
||||
rsx! {
|
||||
"outer = {value:?}"
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn other_reader(value: Store<i32>, counter: Rc<RefCell<RunCounter>>) -> Element {
|
||||
counter.borrow_mut().other += 1;
|
||||
rsx! {
|
||||
"other = {value}"
|
||||
}
|
||||
}
|
||||
|
||||
let counter = Rc::new(RefCell::new(RunCounter::default()));
|
||||
let mut dom = VirtualDom::new_with_props(
|
||||
|counter: Rc<RefCell<RunCounter>>| {
|
||||
let y = STORE.resolve();
|
||||
rsx! {
|
||||
outer_reader { value: y.outer(), counter: counter.clone() }
|
||||
other_reader { value: y.other(), counter: counter.clone() }
|
||||
}
|
||||
},
|
||||
counter.clone(),
|
||||
);
|
||||
dom.rebuild_in_place();
|
||||
|
||||
{
|
||||
let current_counter = counter.borrow();
|
||||
assert_eq!(current_counter.outer, 1);
|
||||
assert_eq!(current_counter.other, 1);
|
||||
}
|
||||
|
||||
dom.in_scope(ScopeId::APP, || {
|
||||
STORE.resolve().outer().inner1().set(1);
|
||||
});
|
||||
|
||||
dom.render_immediate(&mut NoOpMutations);
|
||||
|
||||
{
|
||||
let current_counter = counter.borrow();
|
||||
assert_eq!(current_counter.outer, 2);
|
||||
assert_eq!(current_counter.other, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_grandparent_reader_sees_descendant_write() {
|
||||
fn default_w() -> W {
|
||||
W::default()
|
||||
}
|
||||
|
||||
static STORE: GlobalStore<W> = Global::new(default_w);
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
struct RunCounter {
|
||||
nested: usize,
|
||||
other: usize,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn grandparent_reader(value: Store<Y>, counter: Rc<RefCell<RunCounter>>) -> Element {
|
||||
counter.borrow_mut().nested += 1;
|
||||
rsx! {
|
||||
"nested = {value:?}"
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn other_reader(value: Store<i32>, counter: Rc<RefCell<RunCounter>>) -> Element {
|
||||
counter.borrow_mut().other += 1;
|
||||
rsx! {
|
||||
"other = {value}"
|
||||
}
|
||||
}
|
||||
|
||||
let counter = Rc::new(RefCell::new(RunCounter::default()));
|
||||
let mut dom = VirtualDom::new_with_props(
|
||||
|counter: Rc<RefCell<RunCounter>>| {
|
||||
let w = STORE.resolve();
|
||||
rsx! {
|
||||
grandparent_reader { value: w.nested(), counter: counter.clone() }
|
||||
other_reader { value: w.other(), counter: counter.clone() }
|
||||
}
|
||||
},
|
||||
counter.clone(),
|
||||
);
|
||||
dom.rebuild_in_place();
|
||||
|
||||
{
|
||||
let current_counter = counter.borrow();
|
||||
assert_eq!(current_counter.nested, 1);
|
||||
assert_eq!(current_counter.other, 1);
|
||||
}
|
||||
|
||||
dom.in_scope(ScopeId::APP, || {
|
||||
STORE.resolve().nested().outer().inner1().set(1);
|
||||
});
|
||||
|
||||
dom.render_immediate(&mut NoOpMutations);
|
||||
|
||||
{
|
||||
let current_counter = counter.borrow();
|
||||
assert_eq!(current_counter.nested, 2);
|
||||
assert_eq!(current_counter.other, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn len_is_shallow_but_push_marks_it_dirty() {
|
||||
fn initial_items() -> Vec<i32> {
|
||||
vec![1, 2, 3]
|
||||
}
|
||||
|
||||
static STORE: GlobalStore<Vec<i32>> = Global::new(initial_items);
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
struct RunCounter {
|
||||
len: usize,
|
||||
first: usize,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn len_reader(value: Store<Vec<i32>>, counter: Rc<RefCell<RunCounter>>) -> Element {
|
||||
counter.borrow_mut().len += 1;
|
||||
rsx! {
|
||||
"len = {value.len()}"
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn first_reader(value: Store<i32>, counter: Rc<RefCell<RunCounter>>) -> Element {
|
||||
counter.borrow_mut().first += 1;
|
||||
rsx! {
|
||||
"first = {value}"
|
||||
}
|
||||
}
|
||||
|
||||
let counter = Rc::new(RefCell::new(RunCounter::default()));
|
||||
let mut dom = VirtualDom::new_with_props(
|
||||
|counter: Rc<RefCell<RunCounter>>| {
|
||||
let items = STORE.resolve();
|
||||
rsx! {
|
||||
len_reader { value: items, counter: counter.clone() }
|
||||
first_reader { value: items.index(0), counter: counter.clone() }
|
||||
}
|
||||
},
|
||||
counter.clone(),
|
||||
);
|
||||
dom.rebuild_in_place();
|
||||
|
||||
{
|
||||
let current_counter = counter.borrow();
|
||||
assert_eq!(current_counter.len, 1);
|
||||
assert_eq!(current_counter.first, 1);
|
||||
}
|
||||
|
||||
dom.in_scope(ScopeId::APP, || {
|
||||
STORE.resolve().index(0).set(10);
|
||||
});
|
||||
|
||||
dom.render_immediate(&mut NoOpMutations);
|
||||
|
||||
{
|
||||
let current_counter = counter.borrow();
|
||||
assert_eq!(current_counter.len, 1);
|
||||
assert_eq!(current_counter.first, 2);
|
||||
}
|
||||
|
||||
dom.in_scope(ScopeId::APP, || {
|
||||
let mut items = STORE.resolve();
|
||||
items.push(4);
|
||||
});
|
||||
|
||||
dom.render_immediate(&mut NoOpMutations);
|
||||
|
||||
{
|
||||
let current_counter = counter.borrow();
|
||||
assert_eq!(current_counter.len, 2);
|
||||
assert_eq!(current_counter.first, 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_parent_reader_sees_nested_vec_push() {
|
||||
fn initial_z() -> Z {
|
||||
Z {
|
||||
items: vec![1, 2, 3],
|
||||
other: 0,
|
||||
}
|
||||
}
|
||||
|
||||
static STORE: GlobalStore<Z> = Global::new(initial_z);
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
struct RunCounter {
|
||||
collection: usize,
|
||||
other: usize,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn collection_reader(value: Store<Z>, counter: Rc<RefCell<RunCounter>>) -> Element {
|
||||
counter.borrow_mut().collection += 1;
|
||||
rsx! {
|
||||
"z = {value:?}"
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn other_reader(value: Store<i32>, counter: Rc<RefCell<RunCounter>>) -> Element {
|
||||
counter.borrow_mut().other += 1;
|
||||
rsx! {
|
||||
"other = {value}"
|
||||
}
|
||||
}
|
||||
|
||||
let counter = Rc::new(RefCell::new(RunCounter::default()));
|
||||
let mut dom = VirtualDom::new_with_props(
|
||||
|counter: Rc<RefCell<RunCounter>>| {
|
||||
let z = STORE.resolve();
|
||||
rsx! {
|
||||
collection_reader { value: z, counter: counter.clone() }
|
||||
other_reader { value: z.other(), counter: counter.clone() }
|
||||
}
|
||||
},
|
||||
counter.clone(),
|
||||
);
|
||||
dom.rebuild_in_place();
|
||||
|
||||
{
|
||||
let current_counter = counter.borrow();
|
||||
assert_eq!(current_counter.collection, 1);
|
||||
assert_eq!(current_counter.other, 1);
|
||||
}
|
||||
|
||||
dom.in_scope(ScopeId::APP, || {
|
||||
let mut items = STORE.resolve().items();
|
||||
items.push(4);
|
||||
});
|
||||
|
||||
dom.render_immediate(&mut NoOpMutations);
|
||||
|
||||
{
|
||||
let current_counter = counter.borrow();
|
||||
assert_eq!(current_counter.collection, 2);
|
||||
assert_eq!(current_counter.other, 1);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user