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    /// Allocates `SignalInner<T>` on the heap via `Box`, stores the raw pointer
11    /// address, and registers it in the global registry for lifecycle tracking.
12    ///
13    /// # Arguments
14    ///
15    /// - `T` - The initial value of the signal.
16    ///
17    /// # Returns
18    ///
19    /// - `Self` - A handle to the newly created reactive signal.
20    pub fn create(value: T) -> Self {
21        let boxed: Box<SignalInner<T>> = Box::new(SignalInner::new(value, Vec::new(), true));
22        let ptr: *mut SignalInner<T> = Box::into_raw(boxed);
23        let addr: usize = ptr as usize;
24        signal_inner_registry_mut().insert(addr);
25        let mut signal: Self = Self::new(0, std::marker::PhantomData);
26        signal.set_inner(addr);
27        signal
28    }
29
30    /// Returns the current value of the signal.
31    ///
32    /// Directly reads the value from the heap-allocated inner state via raw
33    /// pointer dereference. No runtime borrow checking overhead.
34    ///
35    /// If the signal has been marked inactive (`alive == false`), returns the
36    /// last stored value without registering tracking dependencies. This
37    /// ensures that stale async callbacks (e.g., orphaned `setInterval`)
38    /// holding a `Signal` copy can still call `.get()` safely without
39    /// triggering side effects or panics.
40    ///
41    /// If a tracking context is active (i.e., a DynamicNode is being rendered),
42    /// automatically registers the current dynamic node as a dependent of
43    /// this signal for precise reactive updates.
44    ///
45    /// # Returns
46    ///
47    /// - `T` - The current value of the signal.
48    pub fn get(&self) -> T {
49        let inner: &mut SignalInner<T> = get_signal_inner_ref::<T>(self.get_inner());
50        if !inner.get_alive() {
51            return inner.get_value().clone();
52        }
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        inner.get_value().clone()
58    }
59
60    /// Subscribes a callback to be invoked when the signal changes.
61    ///
62    /// # Arguments
63    ///
64    /// - `FnMut() + 'static` - The callback to invoke when the signal changes.
65    pub fn subscribe<F>(&self, callback: F)
66    where
67        F: FnMut() + 'static,
68    {
69        get_signal_inner_ref::<T>(self.get_inner())
70            .get_mut_listeners()
71            .push(Box::new(callback));
72    }
73
74    /// Replaces all listeners with a single new callback.
75    ///
76    /// Unlike `subscribe`, which appends a listener, this method clears any
77    /// existing listeners first and then adds the new one.
78    ///
79    /// # Arguments
80    ///
81    /// - `FnMut() + 'static` - The callback to invoke when the signal changes.
82    pub(crate) fn replace_subscribe<F>(&self, callback: F)
83    where
84        F: FnMut() + 'static,
85    {
86        let listeners: &mut Vec<Box<dyn FnMut()>> =
87            get_signal_inner_ref::<T>(self.get_inner()).get_mut_listeners();
88        listeners.clear();
89        listeners.push(Box::new(callback));
90    }
91
92    /// Detaches this signal from the reactive system without freeing memory.
93    ///
94    /// Marks the signal inactive and clears its listeners and dependents, but
95    /// intentionally keeps the heap allocation alive.
96    ///
97    /// This is the only supported teardown path for a signal, and is used by
98    /// both DOM-bound subscribe closures (when their node is removed) and the
99    /// `use_signal` hook cleanup (when a component unmounts or a `match` arm
100    /// switches). Freeing the allocation is deliberately never done at these
101    /// points because `Signal<T>` is `Copy` (just a `usize` address): async
102    /// callbacks (`spawn_local` futures, `setTimeout` / `setInterval`
103    /// closures, Promise continuations) may still hold copies of the signal,
104    /// and freeing would turn their later `.get()` / `.set()` calls into a
105    /// use-after-free. Deactivating instead makes those stale calls safe
106    /// no-ops.
107    ///
108    /// The allocation remains valid until the page unloads. For SPAs this is
109    /// acceptable; a long-lived app could add a periodic sweep that frees
110    /// `alive == false` entries once no async references remain. This mirrors
111    /// the contract documented on `clear_signal_listeners_by_addr`.
112    pub(crate) fn deactivate(&self) {
113        let inner: &mut SignalInner<T> = get_signal_inner_ref::<T>(self.get_inner());
114        inner.set_alive(false);
115        inner.get_mut_listeners().clear();
116        inner.get_mut_dependents().clear();
117    }
118
119    /// Core implementation of value update and listener notification.
120    ///
121    /// Returns `true` if the value was updated and listeners were notified.
122    /// Returns `false` if the signal is inactive or the value is unchanged.
123    ///
124    /// Uses a swap-out pattern for listeners: moves all listeners into a local
125    /// `Vec`, drops the mutable reference to inner state, then invokes each
126    /// listener. After invocation, listeners are moved back. This prevents
127    /// issues with re-entrant access during listener callbacks.
128    fn update_and_notify(&self, value: T) -> bool {
129        let inner: &mut SignalInner<T> = get_signal_inner_ref::<T>(self.get_inner());
130        if !inner.get_alive() {
131            return false;
132        }
133        if *inner.get_value() == value {
134            return false;
135        }
136        inner.set_value(value);
137        let mut listeners: Vec<Box<dyn FnMut()>> = Vec::new();
138        swap(inner.get_mut_listeners(), &mut listeners);
139        for listener in listeners.iter_mut() {
140            listener();
141        }
142        // Move listeners back, but ONLY if the signal's allocation still exists.
143        //
144        // A listener may have detached this signal during its execution (e.g.
145        // a DOM-bound subscribe closure that called `deactivate` because its
146        // node was removed mid-dispatch). Although no current teardown path
147        // frees the allocation, we still probe the registry before re-borrowing
148        // `get_signal_inner_ref::<T>(self.get_inner())` so that a future GC sweep — or any code that does
149        // free — cannot turn this re-borrow into a use-after-free. If the
150        // signal is gone, skip the restore entirely.
151        if !is_signal_inner_alive(self.get_inner()) {
152            return true;
153        }
154        let inner: &mut SignalInner<T> = get_signal_inner_ref::<T>(self.get_inner());
155        if inner.get_alive() {
156            // Restore listeners by *prepending* the originals ahead of any
157            // listeners that were registered while we were iterating.
158            //
159            // A listener may itself call `subscribe` on this same signal,
160            // appending to `inner.listeners` (which is currently the empty Vec
161            // we swapped in). A plain `swap` back would discard those
162            // newly-registered listeners. Instead we splice the originals in
163            // front of the new ones so that both sets survive and the original
164            // ordering is preserved.
165            let new_listeners: &mut Vec<Box<dyn FnMut()>> = inner.get_mut_listeners();
166            if new_listeners.is_empty() {
167                swap(new_listeners, &mut listeners);
168            } else {
169                listeners.append(new_listeners);
170                swap(new_listeners, &mut listeners);
171            }
172        }
173        true
174    }
175
176    /// Registers a dynamic node ID as a dependent of this signal.
177    ///
178    /// When this signal changes, only its registered dependents will be
179    /// marked dirty for re-rendering, enabling precise updates instead
180    /// of broadcasting to all dynamic nodes.
181    ///
182    /// # Arguments
183    ///
184    /// - `usize` - The dynamic node ID to register as a dependent.
185    pub(crate) fn add_dependent(&self, dynamic_id: usize) {
186        let deps: &mut Vec<usize> =
187            get_signal_inner_ref::<T>(self.get_inner()).get_mut_dependents();
188        if !deps.contains(&dynamic_id) {
189            deps.push(dynamic_id);
190        }
191    }
192
193    /// Removes a dynamic node ID from the dependents list of this signal.
194    ///
195    /// Called during cleanup when a dynamic node is removed from the DOM
196    /// and its dependency relationships need to be severed.
197    ///
198    /// # Arguments
199    ///
200    /// - `usize` - The dynamic node ID to remove.
201    #[allow(dead_code)]
202    pub(crate) fn remove_dependent(&self, dynamic_id: usize) {
203        get_signal_inner_ref::<T>(self.get_inner())
204            .get_mut_dependents()
205            .retain(|id| *id != dynamic_id);
206    }
207
208    /// Returns the list of dependent dynamic node IDs for this signal.
209    ///
210    /// # Returns
211    ///
212    /// - `Vec<usize>` - Clone of the dependents list.
213    pub(crate) fn get_dependents(&self) -> Vec<usize> {
214        get_signal_inner_ref::<T>(self.get_inner())
215            .get_dependents()
216            .clone()
217    }
218
219    /// Sets the value of the signal and notifies listeners.
220    ///
221    /// Uses precise dirty marking: only dynamic nodes that depend on
222    /// this signal are marked dirty, avoiding full broadcast.
223    ///
224    /// # Arguments
225    ///
226    /// - `T` - The new value to assign to the signal.
227    pub fn set(&self, value: T) {
228        if self.update_and_notify(value) {
229            let dependents: Vec<usize> = self.get_dependents();
230            schedule_signal_update_targeted(&dependents);
231        }
232    }
233
234    /// Sets the value of the signal and notifies listeners without scheduling
235    /// a global DOM update dispatch.
236    ///
237    /// # Arguments
238    ///
239    /// - `T` - The new value to assign to the signal.
240    pub fn set_silent(&self, value: T) {
241        self.update_and_notify(value);
242    }
243
244    /// Sets the value of the signal without notifying listeners or scheduling
245    /// a DOM update. This is useful for breaking circular watch dependencies
246    /// where two signals watch each other and would otherwise recurse infinitely.
247    ///
248    /// If the signal has been marked inactive (`alive == false`), this is a
249    /// no-op, consistent with `set()` behavior for dead signals.
250    ///
251    /// # Arguments
252    ///
253    /// - `T` - The new value to assign to the signal.
254    pub fn set_untracked(&self, value: T) {
255        let inner: &mut SignalInner<T> = get_signal_inner_ref::<T>(self.get_inner());
256        if !inner.get_alive() {
257            return;
258        }
259        inner.set_value(value);
260    }
261}
262
263/// Provides a safe default for `Signal<T>` by creating a valid signal
264/// initialized with `T::default()`.
265///
266/// This prevents the creation of invalid signals with `inner = 0` (null
267/// pointer), which would cause a panic when `.get()` is called.
268///
269/// # Returns
270///
271/// - `Self`: A valid signal initialized with `T::default()`.
272impl<T> Default for Signal<T>
273where
274    T: Clone + Default + PartialEq + 'static,
275{
276    fn default() -> Self {
277        Self::create(T::default())
278    }
279}
280
281/// Clones the signal, sharing the same inner state.
282///
283/// Since `Signal` is `Copy`, this simply returns `*self`.
284///
285/// # Returns
286///
287/// - `Self`: A copy of the signal handle sharing the same inner state.
288impl<T> Clone for Signal<T>
289where
290    T: Clone + PartialEq + 'static,
291{
292    fn clone(&self) -> Self {
293        *self
294    }
295}
296
297/// Copies the signal, sharing the same inner state.
298///
299/// Safe because only the inner address (a `usize`) is copied;
300/// the actual heap allocation is owned by the global signal registry.
301impl<T> Copy for Signal<T> where T: Clone + PartialEq + 'static {}
302
303/// Marks `SignalCell` as `Sync` for single-threaded WASM contexts.
304///
305/// SAFETY: `SignalCell` is only used in single-threaded WASM contexts.
306/// Concurrent access from multiple threads would be undefined behavior.
307unsafe impl<T> Sync for SignalCell<T> where T: Clone + PartialEq + 'static {}
308
309/// Implementation of SignalCell construction and access.
310impl<T> SignalCell<T>
311where
312    T: Clone + PartialEq + 'static,
313{
314    /// Creates a new `SignalCell` with no signal stored.
315    ///
316    /// # Returns
317    ///
318    /// - `Self`: An empty `SignalCell` with `None` stored in the inner `UnsafeCell`.
319    pub const fn none() -> Self {
320        Self {
321            inner: UnsafeCell::new(None),
322        }
323    }
324
325    /// Stores a signal into the cell.
326    ///
327    /// # Arguments
328    ///
329    /// - `Signal<T>` - The signal to store.
330    ///
331    /// # Panics
332    ///
333    /// Panics if a signal has already been stored.
334    pub fn set(&self, signal: Signal<T>) {
335        unsafe {
336            let ptr: &mut Option<Signal<T>> = &mut *self.get_inner().get();
337            if ptr.is_some() {
338                panic!("SignalCell::set called on an already-initialized cell");
339            }
340            *ptr = Some(signal);
341        }
342    }
343
344    /// Returns the signal stored in the cell.
345    ///
346    /// # Returns
347    ///
348    /// - `Signal<T>` - The stored signal.
349    ///
350    /// # Panics
351    ///
352    /// Panics if no signal has been stored via `set`.
353    pub fn get(&self) -> Signal<T> {
354        unsafe {
355            let ptr: &Option<Signal<T>> = &*self.get_inner().get();
356            match ptr {
357                Some(signal) => *signal,
358                None => panic!("SignalCell::get called on an uninitialized cell"),
359            }
360        }
361    }
362}
363
364/// Provides a default empty `SignalCell`.
365///
366/// Creates a `SignalCell` with `None` stored in the inner `UnsafeCell`.
367///
368/// # Returns
369///
370/// - `Self`: An empty `SignalCell` with no signal stored.
371impl<T> Default for SignalCell<T>
372where
373    T: Clone + PartialEq + 'static,
374{
375    fn default() -> Self {
376        Self {
377            inner: UnsafeCell::new(None),
378        }
379    }
380}
381
382/// Marks `SignalInnerRegistryCell` as `Sync` for single-threaded WASM contexts.
383///
384/// SAFETY: `SignalInnerRegistryCell` is only used in single-threaded WASM contexts.
385/// Concurrent access from multiple threads would be undefined behavior.
386unsafe impl Sync for SignalInnerRegistryCell {}