use dioxus_core::{ReactiveContext, SubscriberList, Subscribers}; use dioxus_signals::{CopyValue, ReadableExt, SyncStorage, Writable, WritableExt}; use std::fmt::Debug; use std::hash::BuildHasher; use std::{ collections::{HashMap, HashSet}, 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. #[derive(Clone, Default)] pub(crate) struct SelectorNode { subscribers: HashSet, root: HashMap, } impl SelectorNode { /// Get an existing selector node by its path. fn get(&self, path: &[PathKey]) -> Option<&SelectorNode> { let [first, rest @ ..] = path else { return Some(self); }; self.root.get(first).and_then(|child| child.get(rest)) } /// Get an existing selector node by its path mutably. fn get_mut(&mut self, path: &[PathKey]) -> Option<&mut SelectorNode> { let [first, rest @ ..] = path else { return Some(self); }; self.root .get_mut(first) .and_then(|child| child.get_mut(rest)) } /// Get a selector mutably or create one if it doesn't exist. This is used when subscribing to /// a path that may not exist yet. fn get_mut_or_default(&mut self, path: &[PathKey]) -> &mut SelectorNode { let [first, rest @ ..] = path else { return self; }; self.root .entry(*first) .or_default() .get_mut_or_default(rest) } /// Get the path to each node under this node /// /// This is used to mark nodes dirty recursively when a Store is written to. fn paths_under(&self, current_path: &[PathKey], paths: &mut Vec>) { paths.push(current_path.into()); for (i, child) in self.root.iter() { let mut child_path: Vec = current_path.into(); child_path.push(*i); child.paths_under(&child_path, paths); } } /// Get paths to only children before 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. fn paths_at_and_after_index( &self, path: &[PathKey], index: usize, paths: &mut Vec>, ) { let Some(node) = self.get(path) else { return; }; // Mark the nodes before the index as dirty for (i, child) in node.root.iter() { if *i as usize >= index { let mut child_path: Vec = path.into(); child_path.push(*i); child.paths_under(&child_path, paths); } } } /// Remove a path from the subscription tree fn remove(&mut self, path: &[PathKey]) { let [first, rest @ ..] = path else { return; }; if let Some(node) = self.root.get_mut(first) { if rest.is_empty() { self.root.remove(first); } else { node.remove(rest); } } } } pub(crate) type PathKey = u16; #[cfg(feature = "large-path")] const PATH_LENGTH: usize = 32; #[cfg(not(feature = "large-path"))] const PATH_LENGTH: usize = 16; #[derive(Copy, Clone, PartialEq)] pub(crate) struct TinyVec { length: usize, path: [PathKey; PATH_LENGTH], } impl Default for TinyVec { fn default() -> Self { Self::new() } } impl Debug for TinyVec { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("TinyVec") .field("path", &&self.path[..self.length]) .finish() } } impl TinyVec { pub(crate) const fn new() -> Self { Self { length: 0, path: [0; PATH_LENGTH], } } pub(crate) const fn push(&mut self, index: u16) { if self.length < self.path.len() { self.path[self.length] = index; self.length += 1; } else { panic!("SelectorPath is full"); } } } impl Deref for TinyVec { type Target = [u16]; fn deref(&self) -> &Self::Target { &self.path[..self.length] } } #[derive(Default)] pub(crate) struct StoreSubscriptionsInner { root: SelectorNode, hasher: std::collections::hash_map::RandomState, } #[derive(Default)] pub(crate) struct StoreSubscriptions { inner: CopyValue, } impl Clone for StoreSubscriptions { fn clone(&self) -> Self { *self } } impl Copy for StoreSubscriptions {} impl PartialEq for StoreSubscriptions { fn eq(&self, other: &Self) -> bool { self.inner == other.inner } } impl StoreSubscriptions { /// Create a new instance of StoreSubscriptions. pub(crate) fn new() -> Self { Self { inner: CopyValue::new_maybe_sync(StoreSubscriptionsInner { root: SelectorNode::default(), hasher: std::collections::hash_map::RandomState::new(), }), } } /// Hash an index into a PathKey using the hasher. The hash should be consistent /// across calls pub(crate) fn hash(&self, index: &impl Hash) -> PathKey { (self.inner.write_unchecked().hasher.hash_one(index) % PathKey::MAX as u64) as PathKey } /// Subscribe to a specific path in the store. pub(crate) fn track(&self, key: &[PathKey]) { if let Some(rc) = ReactiveContext::current() { let subscribers = self.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]) { 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); rc.subscribe(subscribers); } } } /// Mark the node and all its children as dirty pub(crate) fn mark_dirty(&self, key: &[PathKey]) { let 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); paths }; for path in paths { self.mark_dirty_shallow(&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 nodes 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(); let mut paths = Vec::new(); read.root.paths_at_and_after_index(key, index, &mut paths); paths }; for path in paths { self.mark_dirty_shallow(&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 { Arc::new(StoreSubscribers { subscriptions: *self, path: key.to_vec().into_boxed_slice(), }) .into() } } /// A subscriber list implementation that handles garbage collection of the subscription tree. struct StoreSubscribers { subscriptions: StoreSubscriptions, path: Box<[PathKey]>, } impl SubscriberList for StoreSubscribers { /// Add a subscriber to the subscription list for this path in the store, creating the node if it doesn't exist. fn add(&self, subscriber: ReactiveContext) { let Ok(mut write) = self.subscriptions.inner.try_write_unchecked() else { return; }; let node = write.root.get_mut_or_default(&self.path); node.subscribers.insert(subscriber); } /// Remove a subscriber from the subscription list for this path in the store. If the node has no subscribers left /// remove that node from the subscription tree. fn remove(&self, subscriber: &ReactiveContext) { let Ok(mut write) = self.subscriptions.inner.try_write_unchecked() else { return; }; let Some(node) = write.root.get_mut(&self.path) else { return; }; node.subscribers.remove(subscriber); if node.subscribers.is_empty() && node.root.is_empty() { write.root.remove(&self.path); } } /// Visit all subscribers for this path in the store, calling the provided function on each subscriber. fn visit(&self, f: &mut dyn FnMut(&ReactiveContext)) { let Ok(read) = self.subscriptions.inner.try_read() else { return; }; let Some(node) = read.root.get(&self.path) else { return; }; node.subscribers.iter().for_each(f); } }