reactive_graph/computed/selector.rs
1use crate::{
2 effect::RenderEffect,
3 signal::ArcRwSignal,
4 traits::{Track, Update},
5};
6use or_poisoned::OrPoisoned;
7use rustc_hash::FxHashMap;
8use std::{
9 hash::Hash,
10 sync::{Arc, RwLock},
11};
12
13/// A conditional signal that only notifies subscribers when a change
14/// in the source signal’s value changes whether the given function is true.
15///
16/// **You probably don’t need this,** but it can be a very useful optimization
17/// in certain situations (e.g., “set the class `selected` if `selected() == this_row_index`)
18/// because it reduces them from `O(n)` to `O(1)`.
19///
20/// ```
21/// # use reactive_graph::computed::*;
22/// # use reactive_graph::signal::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
23/// # use reactive_graph::prelude::*;
24/// # use reactive_graph::effect::Effect;
25/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
26/// # tokio_test::block_on(async move {
27/// # tokio::task::LocalSet::new().run_until(async move {
28/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
29/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
30/// let a = RwSignal::new(0);
31/// let is_selected = Selector::new(move || a.get());
32/// let total_notifications = StoredValue::new(0);
33/// Effect::new_isomorphic({
34/// let is_selected = is_selected.clone();
35/// move |_| {
36/// if is_selected.selected(&5) {
37/// total_notifications.update_value(|n| *n += 1);
38/// }
39/// }
40/// });
41///
42/// assert_eq!(is_selected.selected(&5), false);
43/// assert_eq!(total_notifications.get_value(), 0);
44/// a.set(5);
45/// # any_spawner::Executor::tick().await;
46///
47/// assert_eq!(is_selected.selected(&5), true);
48/// assert_eq!(total_notifications.get_value(), 1);
49/// a.set(5);
50/// # any_spawner::Executor::tick().await;
51///
52/// assert_eq!(is_selected.selected(&5), true);
53/// assert_eq!(total_notifications.get_value(), 1);
54/// a.set(4);
55///
56/// # any_spawner::Executor::tick().await;
57/// assert_eq!(is_selected.selected(&5), false);
58/// # }).await;
59/// # });
60/// ```
61#[derive(Clone)]
62pub struct Selector<T>
63where
64 T: PartialEq + Eq + Clone + Hash + 'static,
65{
66 subs: Arc<RwLock<FxHashMap<T, ArcRwSignal<bool>>>>,
67 v: Arc<RwLock<Option<T>>>,
68 #[allow(clippy::type_complexity)]
69 f: Arc<dyn Fn(&T, &T) -> bool + Send + Sync>,
70 // owning the effect keeps it alive, to keep updating the selector
71 #[allow(dead_code)]
72 effect: Arc<RenderEffect<T>>,
73}
74
75impl<T> Selector<T>
76where
77 T: PartialEq + Send + Sync + Eq + Clone + Hash + 'static,
78{
79 /// Creates a new selector that compares values using [`PartialEq`].
80 pub fn new(source: impl Fn() -> T + Send + Sync + Clone + 'static) -> Self {
81 Self::new_with_fn(source, PartialEq::eq)
82 }
83
84 /// Creates a new selector that compares values by returning `true` from a comparator function
85 /// if the values are the same.
86 pub fn new_with_fn(
87 source: impl Fn() -> T + Clone + Send + Sync + 'static,
88 f: impl Fn(&T, &T) -> bool + Send + Sync + Clone + 'static,
89 ) -> Self {
90 let subs: Arc<RwLock<FxHashMap<T, ArcRwSignal<bool>>>> =
91 Default::default();
92 let v: Arc<RwLock<Option<T>>> = Default::default();
93 let f = Arc::new(f) as Arc<dyn Fn(&T, &T) -> bool + Send + Sync>;
94
95 let effect = Arc::new(RenderEffect::new_isomorphic({
96 let subs = Arc::clone(&subs);
97 let f = Arc::clone(&f);
98 let v = Arc::clone(&v);
99 move |prev: Option<T>| {
100 let next_value = source();
101 *v.write().or_poisoned() = Some(next_value.clone());
102 if prev.as_ref() != Some(&next_value) {
103 for (key, signal) in &*subs.read().or_poisoned() {
104 if f(key, &next_value)
105 || (prev.is_some()
106 && f(key, prev.as_ref().unwrap()))
107 {
108 signal.update(|n| *n = true);
109 }
110 }
111 }
112 next_value
113 }
114 }));
115
116 Selector { subs, v, f, effect }
117 }
118
119 /// Reactively checks whether the given key is selected.
120 pub fn selected(&self, key: &T) -> bool {
121 let read = {
122 let sub = self.subs.read().or_poisoned().get(key).cloned();
123 sub.unwrap_or_else(|| {
124 self.subs
125 .write()
126 .or_poisoned()
127 .entry(key.clone())
128 .or_insert_with(|| ArcRwSignal::new(false))
129 .clone()
130 })
131 };
132 read.track();
133 (self.f)(key, self.v.read().or_poisoned().as_ref().unwrap())
134 }
135
136 /// Removes the listener for the given key.
137 pub fn remove(&self, key: &T) {
138 let mut subs = self.subs.write().or_poisoned();
139 subs.remove(key);
140 }
141
142 /// Clears the listeners for all keys.
143 pub fn clear(&self) {
144 let mut subs = self.subs.write().or_poisoned();
145 subs.clear();
146 }
147}