euv_core/reactive/signal/impl.rs
1use super::*;
2
3/// Implementation of reactive signal operations.
4impl<T> Signal<T>
5where
6 T: Clone + PartialEq + 'static,
7{
8 /// Returns a shared reference to the signal inner registry.
9 ///
10 /// # Returns
11 ///
12 /// - `&'static HashSet<usize>` - A shared reference to the global signal address registry.
13 #[allow(static_mut_refs)]
14 fn registry() -> &'static HashSet<usize> {
15 unsafe { &*SIGNAL_INNER_REGISTRY.deref().get_0().get() }
16 }
17
18 /// Returns a mutable reference to the signal inner registry.
19 ///
20 /// # Returns
21 ///
22 /// - `&'static mut HashSet<usize>` - A mutable reference to the global signal address registry.
23 #[allow(static_mut_refs)]
24 fn registry_mut() -> &'static mut HashSet<usize> {
25 unsafe { &mut *SIGNAL_INNER_REGISTRY.deref().get_0().get() }
26 }
27
28 /// Creates a new `Signal` with the given initial value.
29 ///
30 /// Allocates `SignalInner<T>` on the heap via `Box`, stores the raw pointer
31 /// address, and registers it in the global registry for lifecycle tracking.
32 ///
33 /// # Arguments
34 ///
35 /// - `T: Clone + PartialEq + 'static` - The initial value of the signal.
36 ///
37 /// # Returns
38 ///
39 /// - `Self` - A handle to the newly created reactive signal.
40 pub fn create(value: T) -> Self {
41 let mut inner: SignalInner<T> = SignalInner::new(value, Vec::new(), true);
42 inner.set_listeners_replaced(false);
43 let boxed: Box<SignalInner<T>> = Box::new(inner);
44 let ptr: *mut SignalInner<T> = Box::into_raw(boxed);
45 let addr: usize = ptr as usize;
46 Self::registry_mut().insert(addr);
47 let mut signal: Self = Self::new(0, std::marker::PhantomData);
48 signal.set_inner(addr);
49 signal
50 }
51
52 /// Returns the current value of the signal.
53 ///
54 /// Directly reads the value from the heap-allocated inner state via raw
55 /// pointer dereference. No runtime borrow checking overhead.
56 ///
57 /// If the signal has been marked inactive (`alive == false`), returns the
58 /// last stored value without registering tracking dependencies. This
59 /// ensures that stale async callbacks (e.g., orphaned `setInterval`)
60 /// holding a `Signal` copy can still call `.get()` safely without
61 /// triggering side effects or panics.
62 ///
63 /// If a tracking context is active (i.e., a DynamicNode is being rendered),
64 /// automatically registers the current dynamic node as a dependent of
65 /// this signal for precise reactive updates.
66 ///
67 /// # Returns
68 ///
69 /// - `T: Clone + PartialEq + 'static` - The current value of the signal.
70 pub fn get(&self) -> T {
71 let inner: &mut SignalInner<T> = Self::inner_mut(self.get_inner());
72 if !inner.get_alive() {
73 return inner.get_value().clone();
74 }
75 let tracking_id: usize = CURRENT_TRACKING_DYNAMIC_ID.load(Ordering::Relaxed);
76 if tracking_id != usize::MAX {
77 self.add_dependent(tracking_id);
78 }
79 inner.get_value().clone()
80 }
81
82 /// Subscribes a callback to be invoked when the signal changes.
83 ///
84 /// # Arguments
85 ///
86 /// - `FnMut() + 'static` - The callback to invoke when the signal changes.
87 pub fn subscribe<F>(&self, callback: F)
88 where
89 F: FnMut() + 'static,
90 {
91 Self::inner_mut(self.get_inner())
92 .get_mut_listeners()
93 .push(Box::new(callback));
94 }
95
96 /// Replaces all listeners with a single new callback.
97 ///
98 /// Unlike `subscribe`, which appends a listener, this method clears any
99 /// existing listeners first and then adds the new one.
100 ///
101 /// # Arguments
102 ///
103 /// - `FnMut() + 'static` - The callback to invoke when the signal changes.
104 pub(crate) fn replace_listener<F>(&self, callback: F)
105 where
106 F: FnMut() + 'static,
107 {
108 let inner: &mut SignalInner<T> = Self::inner_mut(self.get_inner());
109 inner.get_mut_listeners().clear();
110 inner.get_mut_listeners().push(Box::new(callback));
111 inner.set_listeners_replaced(true);
112 }
113
114 /// Detaches this signal from the reactive system without freeing memory.
115 ///
116 /// Marks the signal inactive and clears its listeners and dependents, but
117 /// intentionally keeps the heap allocation alive.
118 ///
119 /// This is the only supported teardown path for a signal, and is used by
120 /// both DOM-bound subscribe closures (when their node is removed) and the
121 /// `use_signal` hook cleanup (when a component unmounts or a `match` arm
122 /// switches). Freeing the allocation is deliberately never done at these
123 /// points because `Signal<T>` is `Copy` (just a `usize` address): async
124 /// callbacks (`spawn_local` futures, `setTimeout` / `setInterval`
125 /// closures, Promise continuations) may still hold copies of the signal,
126 /// and freeing would turn their later `.get()` / `.set()` calls into a
127 /// use-after-free. Deactivating instead makes those stale calls safe
128 /// no-ops.
129 ///
130 /// The allocation remains valid until the page unloads. For SPAs this is
131 /// acceptable; a long-lived app could add a periodic sweep that frees
132 /// `alive == false` entries once no async references remain. This mirrors
133 /// the contract documented on `clear_signal_listeners`.
134 pub(crate) fn deactivate(&self) {
135 let inner: &mut SignalInner<T> = Self::inner_mut(self.get_inner());
136 inner.set_alive(false);
137 inner.get_mut_listeners().clear();
138 inner.get_mut_dependents().clear();
139 }
140
141 /// Core implementation of value update and listener notification.
142 ///
143 /// Returns `true` if the value was updated and listeners were notified.
144 /// Returns `false` if the signal is inactive or the value is unchanged.
145 ///
146 /// Uses a swap-out pattern for listeners: moves all listeners into a local
147 /// `Vec`, drops the mutable reference to inner state, then invokes each
148 /// listener. After invocation, listeners are moved back. This prevents
149 /// issues with re-entrant access during listener callbacks.
150 fn update(&self, value: T) -> bool {
151 let inner: &mut SignalInner<T> = Self::inner_mut(self.get_inner());
152 if !inner.get_alive() {
153 return false;
154 }
155 if *inner.get_value() == value {
156 return false;
157 }
158 inner.set_value(value);
159 inner.set_listeners_replaced(false);
160 let mut listeners: Vec<Box<dyn FnMut()>> = Vec::new();
161 swap(inner.get_mut_listeners(), &mut listeners);
162 for listener in listeners.iter_mut() {
163 listener();
164 }
165 if !Self::is_alive(self.get_inner()) {
166 return true;
167 }
168 let inner: &mut SignalInner<T> = Self::inner_mut(self.get_inner());
169 if inner.get_alive() {
170 if inner.get_listeners_replaced() {
171 inner.set_listeners_replaced(false);
172 } else {
173 let new_listeners: &mut Vec<Box<dyn FnMut()>> = inner.get_mut_listeners();
174 if new_listeners.is_empty() {
175 swap(new_listeners, &mut listeners);
176 } else {
177 listeners.append(new_listeners);
178 swap(new_listeners, &mut listeners);
179 }
180 }
181 }
182 true
183 }
184
185 /// Registers a dynamic node ID as a dependent of this signal.
186 ///
187 /// When this signal changes, only its registered dependents will be
188 /// marked dirty for re-rendering, enabling precise updates instead
189 /// of broadcasting to all dynamic nodes.
190 ///
191 /// # Arguments
192 ///
193 /// - `usize` - The dynamic node ID to register as a dependent.
194 pub(crate) fn add_dependent(&self, dynamic_id: usize) {
195 let deps: &mut Vec<usize> = Self::inner_mut(self.get_inner()).get_mut_dependents();
196 if !deps.contains(&dynamic_id) {
197 deps.push(dynamic_id);
198 }
199 }
200
201 /// Returns the list of dependent dynamic node IDs for this signal.
202 ///
203 /// # Returns
204 ///
205 /// - `Vec<usize>` - Clone of the dependents list.
206 pub(crate) fn get_dependents(&self) -> Vec<usize> {
207 Self::inner_mut(self.get_inner()).get_dependents().clone()
208 }
209
210 /// Sets the value of the signal and notifies listeners.
211 ///
212 /// Uses precise dirty marking: only dynamic nodes that depend on
213 /// this signal are marked dirty, avoiding full broadcast.
214 ///
215 /// When called inside `batch`, the dispatch is
216 /// deferred (dirty slots are still marked precisely), and the
217 /// outermost `set()` call outside the suppressed scope will
218 /// trigger the actual dispatch cycle.
219 ///
220 /// # Arguments
221 ///
222 /// - `T: Clone + PartialEq + 'static` - The new value to assign to the signal.
223 pub fn set(&self, value: T) {
224 if self.update(value) {
225 let dependents: Vec<usize> = self.get_dependents();
226 App::schedule_update(&dependents);
227 }
228 }
229
230 /// Retrieves a mutable pointer to `SignalInner<T>` directly from the
231 /// signal's stored address.
232 ///
233 /// SAFETY: The address stored in `Signal::inner` is always a valid pointer
234 /// to a `SignalInner<T>` that is kept alive by the global registry. Since
235 /// WASM is single-threaded, the pointer is always valid as long as the
236 /// signal has not been explicitly freed.
237 fn inner_mut(addr: usize) -> &'static mut SignalInner<T> {
238 unsafe { &mut *(addr as *mut SignalInner<T>) }
239 }
240
241 /// Returns whether the signal allocation at `addr` is still present in
242 /// the global registry (i.e. has not been freed).
243 fn is_alive(addr: usize) -> bool {
244 Self::registry().contains(&addr)
245 }
246}
247
248/// Provides a safe default for `Signal<T>` by creating a valid signal
249/// initialized with `T::default()`.
250///
251/// This prevents the creation of invalid signals with `inner = 0` (null
252/// pointer), which would cause a panic when `.get()` is called.
253///
254/// # Returns
255///
256/// - `Self` - A valid signal initialized with `T::default()`.
257impl<T> Default for Signal<T>
258where
259 T: Clone + Default + PartialEq + 'static,
260{
261 fn default() -> Self {
262 Self::create(T::default())
263 }
264}
265
266/// Clones the signal, sharing the same inner state.
267///
268/// Since `Signal` is `Copy`, this simply returns `*self`.
269///
270/// # Returns
271///
272/// - `Self` - A copy of the signal handle sharing the same inner state.
273impl<T> Clone for Signal<T>
274where
275 T: Clone + PartialEq + 'static,
276{
277 fn clone(&self) -> Self {
278 *self
279 }
280}
281
282/// Copies the signal, sharing the same inner state.
283///
284/// Safe because only the inner address (a `usize`) is copied;
285/// the actual heap allocation is owned by the global signal registry.
286impl<T> Copy for Signal<T> where T: Clone + PartialEq + 'static {}
287
288/// Marks `SignalCell` as `Sync` for single-threaded WASM contexts.
289///
290/// SAFETY: `SignalCell` is only used in single-threaded WASM contexts.
291/// Concurrent access from multiple threads would be undefined behavior.
292unsafe impl<T> Sync for SignalCell<T> where T: Clone + PartialEq + 'static {}
293
294/// Implementation of SignalCell construction and access.
295impl<T> SignalCell<T>
296where
297 T: Clone + PartialEq + 'static,
298{
299 /// Creates a new `SignalCell` with no signal stored.
300 ///
301 /// # Returns
302 ///
303 /// - `Self` - An empty `SignalCell` with `None` stored in the inner `UnsafeCell`.
304 pub const fn none() -> Self {
305 Self {
306 inner: UnsafeCell::new(None),
307 }
308 }
309
310 /// Stores a signal into the cell.
311 ///
312 /// # Arguments
313 ///
314 /// - `Signal<T>` - The signal to store.
315 ///
316 /// # Panics
317 ///
318 /// Panics if a signal has already been stored.
319 pub fn set(&self, signal: Signal<T>) {
320 unsafe {
321 let ptr: &mut Option<Signal<T>> = &mut *self.get_inner().get();
322 if ptr.is_some() {
323 panic!("SignalCell::set called on an already-initialized cell");
324 }
325 *ptr = Some(signal);
326 }
327 }
328
329 /// Returns the signal stored in the cell.
330 ///
331 /// # Returns
332 ///
333 /// - `Signal<T>` - The stored signal.
334 ///
335 /// # Panics
336 ///
337 /// Panics if no signal has been stored via `set`.
338 pub fn get(&self) -> Signal<T> {
339 unsafe {
340 let ptr: &Option<Signal<T>> = &*self.get_inner().get();
341 match ptr {
342 Some(signal) => *signal,
343 None => panic!("SignalCell::get called on an uninitialized cell"),
344 }
345 }
346 }
347}
348
349/// Provides a default empty `SignalCell`.
350///
351/// Creates a `SignalCell` with `None` stored in the inner `UnsafeCell`.
352///
353/// # Returns
354///
355/// - `Self` - An empty `SignalCell` with no signal stored.
356impl<T> Default for SignalCell<T>
357where
358 T: Clone + PartialEq + 'static,
359{
360 fn default() -> Self {
361 Self::new(UnsafeCell::new(None))
362 }
363}
364
365/// Marks `SignalInnerRegistryCell` as `Sync` for single-threaded WASM contexts.
366///
367/// SAFETY: `SignalInnerRegistryCell` is only used in single-threaded WASM contexts.
368/// Concurrent access from multiple threads would be undefined behavior.
369unsafe impl Sync for SignalInnerRegistryCell {}
370
371/// String-specific signal operations.
372impl Signal<String> {
373 /// Clears DOM-binding listeners on a bridge signal identified by its inner
374 /// pointer address, deactivates the bridge signal, and releases its value
375 /// memory.
376 ///
377 /// This function is used during DOM cleanup (`cleanup_dom_subtree`) to
378 /// release bridge `Signal<String>` instances that are no longer needed.
379 ///
380 /// Bridge signals are internal `Signal<String>` instances created by
381 /// `as_reactive_text` and `AttributeValue::Signal` for DOM binding.
382 /// They have exactly one consumer (the DOM element), so deactivating them
383 /// is safe when the element is removed. User-created source signals are
384 /// never passed to this function — they are tracked by `SignalInner.dependents`
385 /// and cleaned up by `use_signal`'s `deactivate()` on hook context teardown.
386 ///
387 /// The bridge signal's value is replaced with `String::new()` to release
388 /// the original string data, and `alive` is set to `false` so that any
389 /// stale async references become safe no-ops.
390 ///
391 /// # Arguments
392 ///
393 /// - `usize` - The inner pointer address of the bridge signal.
394 pub(crate) fn clear_listeners(addr: usize) {
395 let inner: &mut SignalInner<String> = Self::inner_mut(addr);
396 inner.get_mut_listeners().clear();
397 inner.set_alive(false);
398 inner.set_value(String::new());
399 Registry::cleanup_attr_slot(addr);
400 }
401}
402
403/// Implementation of `FireHandle` construction, invocation, and conversions.
404impl FireHandle {
405 /// Leaks the given closure and returns a handle pointing to its heap address.
406 ///
407 /// The closure is double-boxed (`Box<Box<dyn FnMut()>>`) and leaked so the
408 /// inner box's address remains stable for the lifetime of the program.
409 /// The address is captured as a `usize` and wrapped in a `FireHandle`.
410 ///
411 /// # Arguments
412 ///
413 /// - `F: FnMut() + 'static` - The fire closure to leak.
414 ///
415 /// # Returns
416 ///
417 /// - `FireHandle` - A handle holding the leaked closure's address.
418 pub fn new<F>(fire: F) -> Self
419 where
420 F: FnMut() + 'static,
421 {
422 let leaked: &'static mut Box<dyn FnMut()> =
423 Box::leak(Box::new(Box::new(fire) as Box<dyn FnMut()>));
424 let addr: usize = leaked as *mut Box<dyn FnMut()> as usize;
425 let mut handle: Self = Self { inner: 0 };
426 handle.set_inner(addr);
427 handle
428 }
429
430 /// Invokes the closure pointed to by this handle.
431 ///
432 /// Takes `self` by value because `FireHandle: Copy` — repeated invocations
433 /// on a single captured handle each copy the address and operate on the
434 /// same underlying closure.
435 ///
436 /// # Safety
437 ///
438 /// The handle must come from `FireHandle::new` (or `From`) and the
439 /// underlying boxed closure must still be live.
440 pub unsafe fn fire(self) {
441 unsafe { Self::fire_at(self.get_inner()) };
442 }
443
444 /// Invokes the closure stored at the given address.
445 ///
446 /// This is the static counterpart of `fire` for call sites that have
447 /// only the raw `usize` address (e.g., macro-generated code that
448 /// captures the address by `move` into a subscribe closure).
449 ///
450 /// # Arguments
451 ///
452 /// - `usize` - The address of a leaked `Box<dyn FnMut()>`.
453 ///
454 /// # Safety
455 ///
456 /// `addr` must come from a valid `FireHandle` produced by `new` (or
457 /// `From`) and the underlying boxed closure must still be live.
458 pub unsafe fn fire_at(addr: usize) {
459 let ptr: *mut Box<dyn FnMut()> = addr as *mut Box<dyn FnMut()>;
460 unsafe { (&mut *ptr)() };
461 }
462}
463
464/// Leaks a fire closure into a `FireHandle`.
465///
466/// This is the canonical `Into` path used by `watch!`/`computed!` macros
467/// and the virtual list component to obtain a `FireHandle` from a closure.
468impl<F> From<F> for FireHandle
469where
470 F: FnMut() + 'static,
471{
472 /// Leaks this closure and stores its address in the returned handle.
473 ///
474 /// # Returns
475 ///
476 /// - `FireHandle` - A handle holding the leaked closure's address.
477 fn from(fire: F) -> Self {
478 Self::new(fire)
479 }
480}
481
482/// Extracts the raw address from a `FireHandle`.
483///
484/// This is used by macro-generated code that needs to capture the address
485/// (a `Copy` type) into `FnMut() + 'static` subscribe closures.
486impl From<FireHandle> for usize {
487 /// Returns the leaked closure's heap address.
488 ///
489 /// # Returns
490 ///
491 /// - `usize` - The address held by this handle.
492 fn from(handle: FireHandle) -> Self {
493 handle.get_inner()
494 }
495}