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