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}