Skip to main content

euv_core/reactive/signal/
impl.rs

1use crate::*;
2
3/// Implementation of reactive signal operations.
4impl<T> Signal<T>
5where
6    T: Clone + PartialEq + 'static,
7{
8    /// Creates a new `Signal` with the given initial value.
9    ///
10    /// # Arguments
11    ///
12    /// - `T` - The initial value of the signal.
13    ///
14    /// # Returns
15    ///
16    /// - `Self` - A handle to the newly created reactive signal.
17    pub fn create(value: T) -> Self {
18        let signal_inner: Rc<RefCell<SignalInner<T>>> =
19            Rc::new(RefCell::new(SignalInner::new(value, Vec::new(), true)));
20        let addr: usize = Rc::as_ptr(&signal_inner) as usize;
21        signal_inner_registry_mut().insert(addr, signal_inner as Rc<dyn Any>);
22        let mut signal: Self = Self::new(0, std::marker::PhantomData);
23        signal.set_inner(addr);
24        signal
25    }
26
27    /// Returns a reference to the inner `RefCell` for this signal.
28    ///
29    /// # Returns
30    ///
31    /// - `&'static RefCell<SignalInner<T>>` - A reference to the inner state.
32    fn inner_ref(&self) -> &'static RefCell<SignalInner<T>> {
33        get_signal_inner_ref(self.get_inner())
34    }
35
36    /// Returns the current value of the signal.
37    ///
38    /// Uses `try_borrow` to avoid panicking when the inner `RefCell` is
39    /// already mutably borrowed (e.g., during a `set()` notification cycle).
40    /// In that case, falls back to an unsafe direct read of the value,
41    /// which is safe in single-threaded WASM contexts because the value
42    /// itself is not being mutated at the point of read (only the `RefCell`
43    /// borrow flag is contested).
44    ///
45    /// If a tracking context is active (i.e., a DynamicNode is being rendered),
46    /// automatically registers the current dynamic node as a dependent of
47    /// this signal for precise reactive updates.
48    ///
49    /// # Returns
50    ///
51    /// - `T` - The current value of the signal.
52    pub fn get(&self) -> T {
53        let tracking_id: usize = CURRENT_TRACKING_DYNAMIC_ID.load(Ordering::Relaxed);
54        if tracking_id != usize::MAX {
55            self.add_dependent(tracking_id);
56        }
57        let inner_ref: &RefCell<SignalInner<T>> = self.inner_ref();
58        let Ok(inner) = inner_ref.try_borrow() else {
59            return unsafe { (*inner_ref.as_ptr()).get_value().clone() };
60        };
61        inner.get_value().clone()
62    }
63
64    /// Subscribes a callback to be invoked when the signal changes.
65    ///
66    /// If the inner `RefCell` is already borrowed, this is a no-op to avoid
67    /// panicking during re-entrant signal updates.
68    ///
69    /// # Arguments
70    ///
71    /// - `FnMut() + 'static` - The callback to invoke when the signal changes.
72    pub fn subscribe<F>(&self, callback: F)
73    where
74        F: FnMut() + 'static,
75    {
76        if let Ok(mut inner) = self.inner_ref().try_borrow_mut() {
77            inner.get_mut_listeners().push(Box::new(callback));
78        }
79    }
80
81    /// Replaces all listeners with a single new callback.
82    ///
83    /// Unlike `subscribe`, which appends a listener, this method clears any
84    /// existing listeners first and then adds the new one. If the inner
85    /// `RefCell` is already borrowed, this is a no-op.
86    ///
87    /// # Arguments
88    ///
89    /// - `FnMut() + 'static` - The callback to invoke when the signal changes.
90    pub(crate) fn replace_subscribe<F>(&self, callback: F)
91    where
92        F: FnMut() + 'static,
93    {
94        if let Ok(mut inner) = self.inner_ref().try_borrow_mut() {
95            let listeners: &mut Vec<Box<dyn FnMut()>> = inner.get_mut_listeners();
96            listeners.clear();
97            listeners.push(Box::new(callback));
98        }
99    }
100
101    /// Removes all subscribed listeners from this signal, clears its
102    /// dependent dynamic node list, and marks it as inactive.
103    /// If the inner `RefCell` is already borrowed, this is a no-op.
104    ///
105    /// NOTE: Does NOT remove from `SIGNAL_INNER_REGISTRY`. The registry
106    /// must keep the `Rc` alive because `Signal` is `Copy` and other copies
107    /// may still hold the raw address. Removing would cause use-after-free.
108    pub(crate) fn clear_listeners(&self) {
109        if let Ok(mut inner) = self.inner_ref().try_borrow_mut() {
110            inner.set_alive(false);
111            inner.get_mut_listeners().clear();
112            inner.get_mut_dependents().clear();
113        }
114    }
115
116    /// Core implementation of value update and listener notification.
117    ///
118    /// Returns `true` if the value was updated and listeners were notified.
119    /// Returns `false` if the inner `RefCell` is already mutably borrowed
120    /// (re-entrant access), the signal is inactive, or the value is unchanged.
121    fn update_and_notify(&self, value: T) -> bool {
122        let inner_ref: &RefCell<SignalInner<T>> = self.inner_ref();
123        let mut listeners: Vec<Box<dyn FnMut()>> = Vec::new();
124        let Ok(mut inner) = inner_ref.try_borrow_mut() else {
125            return false;
126        };
127        if !inner.get_alive() {
128            return false;
129        }
130        if *inner.get_value() == value {
131            return false;
132        }
133        inner.set_value(value);
134        swap(inner.get_mut_listeners(), &mut listeners);
135        drop(inner);
136        for mut listener in listeners {
137            listener();
138            if let Ok(mut inner) = inner_ref.try_borrow_mut() {
139                inner.get_mut_listeners().push(listener);
140            }
141        }
142        true
143    }
144
145    /// Registers a dynamic node ID as a dependent of this signal.
146    ///
147    /// When this signal changes, only its registered dependents will be
148    /// marked dirty for re-rendering, enabling precise updates instead
149    /// of broadcasting to all dynamic nodes.
150    ///
151    /// # Arguments
152    ///
153    /// - `usize` - The dynamic node ID to register as a dependent.
154    pub(crate) fn add_dependent(&self, dynamic_id: usize) {
155        if let Ok(mut inner) = self.inner_ref().try_borrow_mut() {
156            let deps: &mut Vec<usize> = inner.get_mut_dependents();
157            if !deps.contains(&dynamic_id) {
158                deps.push(dynamic_id);
159            }
160        }
161    }
162
163    /// Removes a dynamic node ID from the dependents list of this signal.
164    ///
165    /// Called during cleanup when a dynamic node is removed from the DOM
166    /// and its dependency relationships need to be severed.
167    ///
168    /// # Arguments
169    ///
170    /// - `usize` - The dynamic node ID to remove.
171    #[allow(dead_code)]
172    pub(crate) fn remove_dependent(&self, dynamic_id: usize) {
173        if let Ok(mut inner) = self.inner_ref().try_borrow_mut() {
174            inner.get_mut_dependents().retain(|id| *id != dynamic_id);
175        }
176    }
177
178    /// Returns the list of dependent dynamic node IDs for this signal.
179    ///
180    /// # Returns
181    ///
182    /// - `Vec<usize>` - Clone of the dependents list.
183    pub(crate) fn get_dependents(&self) -> Vec<usize> {
184        if let Ok(inner) = self.inner_ref().try_borrow() {
185            inner.get_dependents().clone()
186        } else {
187            Vec::new()
188        }
189    }
190
191    /// Sets the value of the signal and notifies listeners.
192    ///
193    /// Uses precise dirty marking: only dynamic nodes that depend on
194    /// this signal are marked dirty, avoiding full broadcast.
195    ///
196    /// # Arguments
197    ///
198    /// - `T` - The new value to assign to the signal.
199    pub fn set(&self, value: T) {
200        if self.update_and_notify(value) {
201            let dependents: Vec<usize> = self.get_dependents();
202            schedule_signal_update_targeted(&dependents);
203        }
204    }
205
206    /// Sets the value of the signal and notifies listeners without scheduling
207    /// a global DOM update dispatch.
208    ///
209    /// # Arguments
210    ///
211    /// - `T` - The new value to assign to the signal.
212    pub fn set_silent(&self, value: T) {
213        self.update_and_notify(value);
214    }
215
216    /// Sets the value of the signal without notifying listeners or scheduling
217    /// a DOM update. This is useful for breaking circular watch dependencies
218    /// where two signals watch each other and would otherwise recurse infinitely.
219    /// If the inner `RefCell` is already borrowed, this is a no-op.
220    ///
221    /// # Arguments
222    ///
223    /// - `T` - The new value to assign to the signal.
224    pub fn set_untracked(&self, value: T) {
225        let inner_ref: &RefCell<SignalInner<T>> = self.inner_ref();
226        if let Ok(mut inner) = inner_ref.try_borrow_mut() {
227            inner.set_value(value);
228        }
229    }
230}
231
232/// Clones the signal, sharing the same inner state.
233///
234/// Since `Signal` is `Copy`, this simply returns `*self`.
235///
236/// # Returns
237///
238/// - `Self`: A copy of the signal handle sharing the same inner state.
239impl<T> Clone for Signal<T>
240where
241    T: Clone + PartialEq + 'static,
242{
243    fn clone(&self) -> Self {
244        *self
245    }
246}
247
248/// Copies the signal, sharing the same inner state.
249///
250/// Safe because only the inner address (a `usize`) is copied;
251/// the actual `Rc` reference is held by the global signal registry.
252impl<T> Copy for Signal<T> where T: Clone + PartialEq + 'static {}
253
254/// Marks `SignalCell` as `Sync` for single-threaded WASM contexts.
255///
256/// SAFETY: `SignalCell` is only used in single-threaded WASM contexts.
257/// Concurrent access from multiple threads would be undefined behavior.
258unsafe impl<T> Sync for SignalCell<T> where T: Clone + PartialEq + 'static {}
259
260/// Implementation of SignalCell construction and access.
261impl<T> SignalCell<T>
262where
263    T: Clone + PartialEq + 'static,
264{
265    /// Creates a new `SignalCell` with no signal stored.
266    ///
267    /// # Returns
268    ///
269    /// - `Self`: An empty `SignalCell` with `None` stored in the inner `UnsafeCell`.
270    pub const fn none() -> Self {
271        Self {
272            inner: UnsafeCell::new(None),
273        }
274    }
275
276    /// Stores a signal into the cell.
277    ///
278    /// # Arguments
279    ///
280    /// - `Signal<T>` - The signal to store.
281    ///
282    /// # Panics
283    ///
284    /// Panics if a signal has already been stored.
285    pub fn set(&self, signal: Signal<T>) {
286        unsafe {
287            let ptr: &mut Option<Signal<T>> = &mut *self.get_inner().get();
288            if ptr.is_some() {
289                panic!("SignalCell::set called on an already-initialized cell");
290            }
291            *ptr = Some(signal);
292        }
293    }
294
295    /// Returns the signal stored in the cell.
296    ///
297    /// # Returns
298    ///
299    /// - `Signal<T>` - The stored signal.
300    ///
301    /// # Panics
302    ///
303    /// Panics if no signal has been stored via `set`.
304    pub fn get(&self) -> Signal<T> {
305        unsafe {
306            let ptr: &Option<Signal<T>> = &*self.get_inner().get();
307            match ptr {
308                Some(signal) => *signal,
309                None => panic!("SignalCell::get called on an uninitialized cell"),
310            }
311        }
312    }
313}
314
315/// Provides a default empty `SignalCell`.
316///
317/// Creates a `SignalCell` with `None` stored in the inner `UnsafeCell`.
318///
319/// # Returns
320///
321/// - `Self`: An empty `SignalCell` with no signal stored.
322impl<T> Default for SignalCell<T>
323where
324    T: Clone + PartialEq + 'static,
325{
326    fn default() -> Self {
327        Self {
328            inner: UnsafeCell::new(None),
329        }
330    }
331}
332
333/// Marks `SignalInnerRegistryCell` as `Sync` for single-threaded WASM contexts.
334///
335/// SAFETY: `SignalInnerRegistryCell` is only used in single-threaded WASM contexts.
336/// Concurrent access from multiple threads would be undefined behavior.
337unsafe impl Sync for SignalInnerRegistryCell {}