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 global typed signal slab.
9 ///
10 /// # Returns
11 ///
12 /// - `&'static SignalSlab` - A shared reference to the global signal slab.
13 #[allow(static_mut_refs)]
14 fn slab() -> &'static SignalSlab {
15 unsafe { &*SIGNAL_SLAB.deref().get() }
16 }
17
18 /// Returns a mutable reference to the global typed signal slab.
19 ///
20 /// # Returns
21 ///
22 /// - `&'static mut SignalSlab` - A mutable reference to the global signal slab.
23 #[allow(static_mut_refs)]
24 fn slab_mut() -> &'static mut SignalSlab {
25 unsafe { &mut *SIGNAL_SLAB.deref().get() }
26 }
27
28 /// Creates a new `Signal` with the given initial value.
29 ///
30 /// Stores the `SignalInner<T>` in the global slab ([`SIGNAL_SLAB`]) and
31 /// returns a `Signal<T>` handle carrying the slot index. The slab is
32 /// append-only: a slot always belongs to the `Signal` that created it,
33 /// so stale handles can never observe a recycled slot of a different
34 /// type.
35 ///
36 /// # Arguments
37 ///
38 /// - `T: Clone + PartialEq + 'static` - The initial value of the signal.
39 ///
40 /// # Returns
41 ///
42 /// - `Self` - A handle to the newly created reactive signal.
43 pub fn create(value: T) -> Self {
44 let inner: SignalInner<T> = SignalInner::new(value, Vec::new(), true);
45 let idx: usize = Self::slab_mut().insert(inner);
46 let mut signal: Self = Self::new(0, PhantomData);
47 signal.set_inner(idx);
48 signal
49 }
50
51 /// Returns the current value of the signal.
52 ///
53 /// Directly reads the value from the slot stored in the global slab.
54 ///
55 /// If the signal has been marked inactive (`alive == false`), returns the
56 /// last stored value without registering tracking dependencies. This
57 /// ensures that stale async callbacks (e.g., orphaned `setInterval`)
58 /// holding a `Signal` copy can still call `.get()` safely without
59 /// triggering side effects or panics.
60 ///
61 /// If a tracking context is active (i.e., a DynamicNode is being rendered),
62 /// automatically registers the current dynamic node as a dependent of
63 /// this signal for precise reactive updates.
64 ///
65 /// # Returns
66 ///
67 /// - `T: Clone + PartialEq + 'static` - The current value of the signal.
68 pub fn get(&self) -> T {
69 let idx: usize = self.get_inner();
70 let Some(inner) = Self::slab_mut().get_mut::<T>(idx) else {
71 // Out-of-bounds handle: the slot index was never issued by this
72 // slab (a corrupted or foreign handle). Slots are never freed or
73 // recycled, so this branch is unreachable for any handle produced
74 // by `Signal::create`. Returning a zero-initialized `T` keeps the
75 // defensive contract deterministic instead of panicking.
76 return unsafe { std::mem::zeroed() };
77 };
78 if !inner.get_alive() {
79 return inner.get_value().clone();
80 }
81 let tracking_id: usize = CURRENT_TRACKING_DYNAMIC_ID.load(Ordering::Relaxed);
82 if tracking_id != usize::MAX {
83 self.add_dependent(tracking_id);
84 }
85 inner.get_value().clone()
86 }
87
88 /// Read-only access to the signal value without cloning.
89 ///
90 /// OPT 17: callers that only need to inspect the value (e.g. format!, eq
91 /// check, debug print, length) can borrow via `with(|v| ...)` and avoid
92 /// one `T::clone` per call. The closure runs under the same tracking
93 /// rules as `get` (still registers `CURRENT_TRACKING_DYNAMIC_ID` if a
94 /// DynamicNode is rendering). The `T: Clone` bound stays on the impl
95 /// because `get` is required by the existing public API; `with` is the
96 /// zero-copy alternative for new code.
97 ///
98 /// # Arguments
99 ///
100 /// - `F: FnOnce(&T) -> R` - Closure receiving `&T`.
101 ///
102 /// # Returns
103 ///
104 /// - `R` - Whatever the closure returns.
105 pub fn with<F, R>(&self, f: F) -> R
106 where
107 F: FnOnce(&T) -> R,
108 {
109 let idx: usize = self.get_inner();
110 let Some(inner) = Self::slab_mut().get_mut::<T>(idx) else {
111 // Out-of-bounds handle: unreachable for slab-issued handles (see
112 // `get`). We avoid adding an `R: Default` bound to preserve the
113 // public API (R is whatever the closure returns).
114 return unsafe { std::mem::zeroed() };
115 };
116 if !inner.get_alive() {
117 return f(inner.get_value());
118 }
119 let tracking_id: usize = CURRENT_TRACKING_DYNAMIC_ID.load(Ordering::Relaxed);
120 if tracking_id != usize::MAX {
121 self.add_dependent(tracking_id);
122 }
123 f(inner.get_value())
124 }
125
126 /// Subscribes a callback to be invoked when the signal changes.
127 ///
128 /// Returns the subscription id, which can later be passed to
129 /// [`Signal::unsubscribe`] to detach exactly this listener. Framework
130 /// DOM bindings use the id to tear down a binding when its element is
131 /// removed; macro-generated `watch!` / `computed!` subscriptions keep
132 /// the id unused because their lifetime is the enclosing hook context.
133 ///
134 /// # Arguments
135 ///
136 /// - `FnMut() + 'static` - The callback to invoke when the signal changes.
137 ///
138 /// # Returns
139 ///
140 /// - `u64` - The subscription id. `u64::MAX` when the handle is stale
141 /// (out-of-bounds slot); such an id is a safe no-op for `unsubscribe`.
142 pub fn subscribe<F>(&self, callback: F) -> u64
143 where
144 F: FnMut() + 'static,
145 {
146 let Some(inner) = Self::slab_mut().get_mut::<T>(self.get_inner()) else {
147 // Stale handle: no slot to register against — the subscription
148 // is silently dropped, matching the previous no-op semantics.
149 return u64::MAX;
150 };
151 let id: u64 = inner.get_next_listener_id();
152 inner.set_next_listener_id(id.wrapping_add(1));
153 inner.get_mut_listeners().push((id, Box::new(callback)));
154 id
155 }
156
157 /// Detaches a single listener previously registered by [`Signal::subscribe`].
158 ///
159 /// When called while the signal is mid-notification (a listener callback
160 /// triggered this call re-entrantly), the removal is deferred: the id is
161 /// recorded and filtered out during `update`'s merge-back pass, so the
162 /// detached listener cannot be resurrected into the live list.
163 ///
164 /// # Arguments
165 ///
166 /// - `u64` - The subscription id returned by `subscribe`.
167 pub fn unsubscribe(&self, id: u64) {
168 let Some(inner) = Self::slab_mut().get_mut::<T>(self.get_inner()) else {
169 return;
170 };
171 if inner.get_notifying() {
172 inner.get_mut_removed_listener_ids().push(id);
173 return;
174 }
175 inner
176 .get_mut_listeners()
177 .retain(|(listener_id, _): &(u64, Box<dyn FnMut()>)| *listener_id != id);
178 }
179
180 /// Detaches this signal from the reactive system without freeing memory.
181 ///
182 /// Marks the signal inactive and clears its listeners and dependents, but
183 /// intentionally keeps the slab slot alive.
184 ///
185 /// This is the only supported teardown path for a signal, and is used by
186 /// the `use_signal` hook cleanup (when a component unmounts or a `match`
187 /// arm switches). The slot is deliberately never freed or recycled because
188 /// `Signal<T>` is `Copy` (just a `usize` slot index): async callbacks
189 /// (`spawn_local` futures, `setTimeout` / `setInterval` closures, Promise
190 /// continuations) may still hold copies of the signal, and recycling would
191 /// turn their later `.get()` / `.set()` calls into reads of an unrelated
192 /// signal. Deactivating instead makes those stale calls safe no-ops.
193 pub(crate) fn deactivate(&self) {
194 let idx: usize = self.get_inner();
195 let Some(inner) = Self::slab_mut().get_mut::<T>(idx) else {
196 // Out-of-bounds handle — treat as no-op. Mirrors the
197 // "deactivate on already-deactivated signal is a safe no-op"
198 // semantic.
199 return;
200 };
201 inner.set_alive(false);
202 inner.get_mut_listeners().clear();
203 inner.get_mut_dependents().clear();
204 inner.get_mut_removed_listener_ids().clear();
205 }
206
207 /// Core implementation of value update and listener notification.
208 ///
209 /// Returns `true` if the value was updated and listeners were notified.
210 /// Returns `false` if the signal is inactive or the value is unchanged.
211 ///
212 /// Uses a swap-out pattern for listeners: moves all listeners into a local
213 /// `Vec`, drops the mutable reference to inner state, then invokes each
214 /// listener. After invocation, listeners are moved back. This prevents
215 /// issues with re-entrant access during listener callbacks. Listeners
216 /// detached via `unsubscribe` mid-notification are filtered out during
217 /// the merge-back pass via `removed_listener_ids`.
218 ///
219 /// # Arguments
220 ///
221 /// - `T: Clone + PartialEq + 'static` - A generic type parameter.
222 ///
223 /// # Returns
224 ///
225 /// - `bool` - A boolean.
226 fn update(&self, value: T) -> bool {
227 let idx: usize = self.get_inner();
228 let Some(inner) = Self::slab_mut().get_mut::<T>(idx) else {
229 // Stale handle — treat as no-op.
230 return false;
231 };
232 if !inner.get_alive() {
233 return false;
234 }
235 if *inner.get_value() == value {
236 return false;
237 }
238 inner.set_value(value);
239 inner.set_notifying(true);
240 let mut listeners: Vec<(u64, Box<dyn FnMut()>)> = Vec::new();
241 swap(inner.get_mut_listeners(), &mut listeners);
242 for (_id, listener) in listeners.iter_mut() {
243 listener();
244 }
245 if !Self::is_alive(self.get_inner()) {
246 // The signal was deactivated by a listener mid-notification.
247 // Nothing should be merged back into a dead slot; clear the
248 // notification state so a later `unsubscribe` cannot pile up
249 // deferred removals that will never be drained.
250 if let Some(inner) = Self::slab_mut().get_mut::<T>(idx) {
251 inner.set_notifying(false);
252 inner.get_mut_removed_listener_ids().clear();
253 }
254 return true;
255 }
256 if let Some(inner) = Self::slab_mut().get_mut::<T>(idx)
257 && inner.get_alive()
258 {
259 let removed: Vec<u64> = take(inner.get_mut_removed_listener_ids());
260 if !removed.is_empty() {
261 listeners.retain(|(listener_id, _): &(u64, Box<dyn FnMut()>)| {
262 !removed.contains(listener_id)
263 });
264 }
265 let new_listeners: &mut Vec<(u64, Box<dyn FnMut()>)> = inner.get_mut_listeners();
266 if new_listeners.is_empty() {
267 swap(new_listeners, &mut listeners);
268 } else {
269 listeners.append(new_listeners);
270 swap(new_listeners, &mut listeners);
271 }
272 inner.set_notifying(false);
273 }
274 true
275 }
276
277 /// Registers a dynamic node ID as a dependent of this signal.
278 ///
279 /// When this signal changes, only its registered dependents will be
280 /// marked dirty for re-rendering, enabling precise updates instead
281 /// of broadcasting to all dynamic nodes.
282 ///
283 /// # Arguments
284 ///
285 /// - `usize` - The dynamic node ID to register as a dependent.
286 ///
287 /// OPT 9: the common rendering case is "this dependent was just added
288 /// (last element of the list)". A `deps.last() == Some(&dynamic_id)`
289 /// check short-circuits the `Vec::contains` linear scan, turning the
290 /// typical append-into-existing-list call from O(N) to O(1). Only the
291 /// rare cases (first add, or `dynamic_id` re-added after a previous
292 /// unsubscription) fall back to the full scan + push.
293 pub(crate) fn add_dependent(&self, dynamic_id: usize) {
294 let Some(inner) = Self::slab_mut().get_mut::<T>(self.get_inner()) else {
295 return;
296 };
297 let deps: &mut Vec<usize> = inner.get_mut_dependents();
298 if let Some(last) = deps.last() {
299 if *last == dynamic_id {
300 return;
301 }
302 if !deps.contains(&dynamic_id) {
303 deps.push(dynamic_id);
304 }
305 } else {
306 deps.push(dynamic_id);
307 }
308 }
309
310 /// Returns the list of dependent dynamic node IDs for this signal.
311 ///
312 /// # Returns
313 ///
314 /// - `Vec<usize>` - Clone of the dependents list.
315 pub(crate) fn get_dependents(&self) -> Vec<usize> {
316 Self::slab_mut()
317 .get_mut::<T>(self.get_inner())
318 .map(|inner: &mut SignalInner<T>| inner.get_dependents().clone())
319 .unwrap_or_default()
320 }
321
322 /// Sets the value of the signal and notifies listeners.
323 ///
324 /// Uses precise dirty marking: only dynamic nodes that depend on
325 /// this signal are marked dirty, avoiding full broadcast.
326 ///
327 /// When called inside `batch`, the dispatch is
328 /// deferred (dirty slots are still marked precisely), and the
329 /// outermost `set()` call outside the suppressed scope will
330 /// trigger the actual dispatch cycle.
331 ///
332 /// # Arguments
333 ///
334 /// - `T: Clone + PartialEq + 'static` - The new value to assign to the signal.
335 pub fn set(&self, value: T) {
336 if self.update(value) {
337 let dependents: Vec<usize> = self.get_dependents();
338 App::schedule_update(&dependents);
339 }
340 }
341
342 /// Returns whether the signal slot at `idx` is still alive
343 /// (i.e. has not been deactivated).
344 ///
345 /// # Arguments
346 ///
347 /// - `usize` - Slab index to test.
348 ///
349 /// # Returns
350 ///
351 /// - `bool` - `true` when the slot refers to a live signal.
352 pub(crate) fn is_alive(idx: usize) -> bool {
353 Self::slab().is_alive(idx)
354 }
355}
356
357/// Provides a safe default for `Signal<T>` by creating a valid signal
358/// initialized with `T::default()`.
359///
360/// This prevents the creation of invalid signals with `inner = 0` (null
361/// pointer), which would cause a panic when `.get()` is called.
362///
363/// # Returns
364///
365/// - `Self` - A valid signal initialized with `T::default()`.
366impl<T> Default for Signal<T>
367where
368 T: Clone + Default + PartialEq + 'static,
369{
370 /// Constructs a default [`Signal`] value.
371 fn default() -> Self {
372 Self::create(T::default())
373 }
374}
375
376/// Clones the signal, sharing the same inner state.
377///
378/// Since `Signal` is `Copy`, this simply returns `*self`.
379///
380/// # Returns
381///
382/// - `Self` - A copy of the signal handle sharing the same inner state.
383impl<T> Clone for Signal<T>
384where
385 T: Clone + PartialEq + 'static,
386{
387 /// Clones the [`Signal`] by reusing shared, cheap-to-clone state where possible.
388 fn clone(&self) -> Self {
389 *self
390 }
391}
392
393/// Copies the signal, sharing the same inner state.
394///
395/// Safe because only the inner address (a `usize`) is copied;
396/// the actual heap allocation is owned by the global signal registry.
397impl<T> Copy for Signal<T> where T: Clone + PartialEq + 'static {}
398
399/// Marks `SignalCell` as `Sync` for single-threaded WASM contexts.
400///
401/// SAFETY: `SignalCell` is only used in single-threaded WASM contexts.
402/// Concurrent access from multiple threads would be undefined behavior.
403unsafe impl<T> Sync for SignalCell<T> where T: Clone + PartialEq + 'static {}
404
405/// Implementation of SignalCell construction and access.
406impl<T> SignalCell<T>
407where
408 T: Clone + PartialEq + 'static,
409{
410 /// Creates a new `SignalCell` with no signal stored.
411 ///
412 /// # Returns
413 ///
414 /// - `Self` - An empty `SignalCell` with `None` stored in the inner `UnsafeCell`.
415 pub const fn none() -> Self {
416 Self {
417 inner: UnsafeCell::new(None),
418 }
419 }
420
421 /// Stores a signal into the cell.
422 ///
423 /// First write wins: if a signal has already been stored, the new
424 /// signal is dropped and the existing one is kept.
425 ///
426 /// # Arguments
427 ///
428 /// - `Signal<T>` - The signal to store.
429 pub fn set(&self, signal: Signal<T>) {
430 unsafe {
431 let ptr: &mut Option<Signal<T>> = &mut *self.get_inner().get();
432 if ptr.is_none() {
433 *ptr = Some(signal);
434 }
435 }
436 }
437
438 /// Returns the signal stored in the cell, if any.
439 ///
440 /// # Returns
441 ///
442 /// - `Option<Signal<T>>` - The stored signal, or `None` when no signal
443 /// has been stored via `set` yet.
444 pub fn loaded(&self) -> Option<Signal<T>> {
445 unsafe {
446 let ptr: &Option<Signal<T>> = &*self.get_inner().get();
447 *ptr
448 }
449 }
450}
451
452/// Provides a default empty `SignalCell`.
453///
454/// Creates a `SignalCell` with `None` stored in the inner `UnsafeCell`.
455///
456/// # Returns
457///
458/// - `Self` - An empty `SignalCell` with no signal stored.
459impl<T> Default for SignalCell<T>
460where
461 T: Clone + PartialEq + 'static,
462{
463 /// Constructs a default [`SignalCell`] value.
464 fn default() -> Self {
465 Self::new(UnsafeCell::new(None))
466 }
467}
468
469/// Implementation of `FireHandle` construction, invocation, and conversions.
470impl FireHandle {
471 /// Leaks the given closure and returns a handle pointing to its heap address.
472 ///
473 /// The closure is double-boxed (`Box<Box<dyn FnMut()>>`) and leaked so the
474 /// inner box's address remains stable for the lifetime of the program.
475 /// The address is captured as a `usize` and wrapped in a `FireHandle`.
476 ///
477 /// # Arguments
478 ///
479 /// - `F: FnMut() + 'static` - The fire closure to leak.
480 ///
481 /// # Returns
482 ///
483 /// - `Self` - A handle holding the leaked closure's address.
484 pub fn new<F>(fire: F) -> Self
485 where
486 F: FnMut() + 'static,
487 {
488 let leaked: &'static mut Box<dyn FnMut()> =
489 Box::leak(Box::new(Box::new(fire) as Box<dyn FnMut()>));
490 let addr: usize = leaked as *mut Box<dyn FnMut()> as usize;
491 let mut handle: Self = Self { inner: 0 };
492 handle.set_inner(addr);
493 handle
494 }
495
496 /// Invokes the closure pointed to by this handle.
497 ///
498 /// Takes `self` by value because `FireHandle: Copy` — repeated invocations
499 /// on a single captured handle each copy the address and operate on the
500 /// same underlying closure.
501 ///
502 /// # Safety
503 ///
504 /// The handle must come from `FireHandle::new` (or `From`) and the
505 /// underlying boxed closure must still be live.
506 pub unsafe fn fire(self) {
507 unsafe { Self::fire_at(self.get_inner()) };
508 }
509
510 /// Invokes the closure stored at the given address.
511 ///
512 /// This is the static counterpart of `fire` for call sites that have
513 /// only the raw `usize` address (e.g., macro-generated code that
514 /// captures the address by `move` into a subscribe closure).
515 ///
516 /// # Arguments
517 ///
518 /// - `usize` - The address of a leaked `Box<dyn FnMut()>`.
519 ///
520 /// # Safety
521 ///
522 /// `addr` must come from a valid `FireHandle` produced by `new` (or
523 /// `From`) and the underlying boxed closure must still be live.
524 pub unsafe fn fire_at(addr: usize) {
525 let ptr: *mut Box<dyn FnMut()> = addr as *mut Box<dyn FnMut()>;
526 unsafe { (&mut *ptr)() };
527 }
528}
529
530/// Leaks a fire closure into a `FireHandle`.
531///
532/// This is the canonical `Into` path used by `watch!`/`computed!` macros
533/// and the virtual list component to obtain a `FireHandle` from a closure.
534impl<F> From<F> for FireHandle
535where
536 F: FnMut() + 'static,
537{
538 /// Leaks this closure and stores its address in the returned handle.
539 ///
540 /// # Returns
541 ///
542 /// - `FireHandle` - A handle holding the leaked closure's address.
543 ///
544 /// # Arguments
545 ///
546 /// - `F` - Input value to convert from.
547 fn from(fire: F) -> Self {
548 Self::new(fire)
549 }
550}
551
552/// Extracts the raw address from a `FireHandle`.
553///
554/// This is used by macro-generated code that needs to capture the address
555/// (a `Copy` type) into `FnMut() + 'static` subscribe closures.
556impl From<FireHandle> for usize {
557 /// Returns the leaked closure's heap address.
558 ///
559 /// # Returns
560 ///
561 /// - `usize` - The address held by this handle.
562 ///
563 /// # Arguments
564 ///
565 /// - `FireHandle` - Input value to convert from.
566 fn from(handle: FireHandle) -> Self {
567 handle.get_inner()
568 }
569}
570
571/// Implementation of the typed signal slab allocator.
572impl SignalSlab {
573 /// Creates an empty slab.
574 pub(crate) fn new() -> Self {
575 Self {
576 entries: Vec::new(),
577 }
578 }
579
580 /// Inserts a new typed `SignalInner<T>` and returns its slot index.
581 ///
582 /// Append-only: the slot index issued here is never reused for another
583 /// signal, which is what makes stale-handle reads sound (they always
584 /// resolve to this slot's original, possibly deactivated, inner state).
585 pub(crate) fn insert<T>(&mut self, inner: SignalInner<T>) -> usize
586 where
587 T: Clone + PartialEq + 'static,
588 {
589 let boxed: Box<dyn AnySignalInner> = Box::new(inner);
590 let idx: usize = self.entries.len();
591 self.entries.push(boxed);
592 idx
593 }
594
595 /// Returns a typed `&mut SignalInner<T>` view of the slot at `idx`.
596 ///
597 /// Returns `None` when the index is out of bounds or was issued for a
598 /// different concrete `T` (defensive TypeId check). Slots are never
599 /// freed, so `None` means the caller is holding a corrupted handle —
600 /// surfaced as `None` rather than panicking so that stale handles
601 /// degrade into safe no-ops (matching the `alive == false` semantics).
602 pub(crate) fn get_mut<T>(&mut self, idx: usize) -> Option<&mut SignalInner<T>>
603 where
604 T: Clone + PartialEq + 'static,
605 {
606 self.entries
607 .get_mut(idx)?
608 .as_any_mut()
609 .downcast_mut::<SignalInner<T>>()
610 }
611
612 /// Returns `true` when the slot at `idx` exists AND its inner signal is
613 /// still marked `alive`. Used by `Signal::is_alive`.
614 pub(crate) fn is_alive(&self, idx: usize) -> bool {
615 match self.entries.get(idx) {
616 Some(inner) => inner.alive(),
617 None => false,
618 }
619 }
620}