Skip to main content

euv_core/reactive/schedule/
fn.rs

1use crate::*;
2
3/// Invokes `callback` with a reference to the persistent dispatch `Function`.
4///
5/// The dispatch closure is created once per thread and stored in
6/// `DISPATCH_CLOSURE`. This helper exposes it as a `&Function` so it can be
7/// passed to the various browser scheduling APIs (`setTimeout`,
8/// `queueMicrotask`, `requestAnimationFrame`) without recreating the closure
9/// on every schedule.
10///
11/// # Arguments
12///
13/// - `FnOnce(&Function) -> R` - Receives the dispatch function reference.
14///
15/// # Returns
16///
17/// - `R` - The value returned by `callback`.
18fn with_dispatch_function<F, R>(callback: F) -> R
19where
20    F: FnOnce(&Function) -> R,
21{
22    DISPATCH_CLOSURE.with(|dispatch_closure: &Closure<dyn FnMut()>| {
23        let dispatch_function: &Function = dispatch_closure.as_ref().unchecked_ref::<Function>();
24        callback(dispatch_function)
25    })
26}
27
28/// Schedules a deferred signal update with precise dirty marking.
29///
30/// Marks only the specified dynamic node IDs as dirty, then queues a
31/// single microtask dispatch if one is not already pending. When
32/// `SUPPRESS_SCHEDULE` is `true`, slots are still marked dirty but no
33/// dispatch is scheduled, allowing `batch` to batch
34/// precise dirty marks without triggering premature DOM updates.
35///
36/// # Arguments
37///
38/// - `&[usize]` - Dynamic node IDs to mark dirty.
39pub fn schedule_update(dependents: &[usize]) {
40    mark_dirty(dependents);
41    if SUPPRESS_SCHEDULE.load(Ordering::Relaxed) {
42        return;
43    }
44    if SCHEDULED.load(Ordering::Relaxed) {
45        return;
46    }
47    SCHEDULED.store(true, Ordering::Relaxed);
48    let window_value: Window = match window() {
49        Some(window_instance) => window_instance,
50        None => {
51            SCHEDULED.store(false, Ordering::Relaxed);
52            return;
53        }
54    };
55    let queued_microtask: bool = with_dispatch_function(|dispatch_function: &Function| {
56        let queue_microtask_value: JsValue =
57            Reflect::get(&window_value, &JsValue::from_str(QUEUE_MICROTASK))
58                .unwrap_or(JsValue::UNDEFINED);
59        matches!(
60            queue_microtask_value.dyn_into::<Function>(),
61            Ok(queue_microtask) if queue_microtask.call1(&window_value, dispatch_function).is_ok()
62        )
63    });
64    if queued_microtask {
65        return;
66    }
67    let scheduled: bool = with_dispatch_function(|dispatch_function: &Function| {
68        window_value
69            .set_timeout_with_callback_and_timeout_and_arguments_0(dispatch_function, 0)
70            .is_ok()
71    });
72    if scheduled {
73        return;
74    }
75    let requested_frame: bool = with_dispatch_function(|dispatch_function: &Function| {
76        window_value
77            .request_animation_frame(dispatch_function)
78            .is_ok()
79    });
80    if requested_frame {
81        return;
82    }
83    SCHEDULED.store(false, Ordering::Relaxed);
84}
85
86/// Batches signal updates within a closure, deferring DOM dispatch until the
87/// outermost batch completes.
88///
89/// Sets `SUPPRESS_SCHEDULE` to `true` so that any `Signal::set()` calls
90/// inside the closure mark their dependents dirty precisely but do not
91/// queue a microtask dispatch. When the outermost batch completes,
92/// a single dispatch is scheduled if any dirty slots were accumulated
93/// during the batch, ensuring that all pending updates are processed.
94///
95/// Unlike the legacy full-broadcast approach, this uses precise dependency
96/// tracking: only the dynamic nodes that actually depend on the changed
97/// signals are marked dirty and re-rendered.
98///
99/// # Arguments
100///
101/// - `FnOnce() -> R` - The closure to execute with batched updates.
102///
103/// # Returns
104///
105/// - `R` - The result of the closure execution.
106pub fn batch<F, R>(callback: F) -> R
107where
108    F: FnOnce() -> R,
109{
110    let was_outermost: bool = !SUPPRESS_SCHEDULE.load(Ordering::Relaxed);
111    SUPPRESS_SCHEDULE.store(true, Ordering::Relaxed);
112    let result: R = callback();
113    SUPPRESS_SCHEDULE.store(!was_outermost, Ordering::Relaxed);
114    if was_outermost && has_dirty() {
115        schedule_update(&[]);
116    }
117    result
118}
119
120/// Subscribes an attribute signal to the global signal update dispatch cycle.
121///
122/// Creates a callback that re-computes the attribute value and sets
123/// it on the signal whenever a signal update cycle runs. The callback
124/// is registered in the signal update registry using the signal's
125/// inner address as the key.
126///
127/// # Arguments
128///
129/// - `Signal<String>` - The attribute signal to subscribe.
130/// - `Fn() -> String + 'static` - A closure that computes the current attribute value string.
131pub(crate) fn subscribe_attr<F>(attr_signal: Signal<String>, compute: F)
132where
133    F: Fn() -> String + 'static,
134{
135    register_attr_listener(
136        attr_signal.get_inner(),
137        Box::new(move || {
138            attr_signal.set(compute());
139        }),
140    );
141}
142
143/// Converts a bool signal into a reactive `Signal<String>` attribute value.
144///
145/// Creates a `Signal<String>` initialized with the bool's string
146/// representation, then subscribes to the source signal so that
147/// whenever the bool changes, the string signal is updated accordingly.
148///
149/// # Arguments
150///
151/// - `Signal<bool>` - The source boolean signal.
152///
153/// # Returns
154///
155/// - `AttributeValue` - An `AttributeValue::Signal` wrapping the derived string signal.
156pub(crate) fn bool_to_attr(source: Signal<bool>) -> AttributeValue {
157    let string_signal: Signal<String> = Signal::create(source.get().to_string());
158    let string_signal_clone: Signal<String> = string_signal;
159    let source_for_sub: Signal<bool> = source;
160    source_for_sub.subscribe(move || {
161        string_signal_clone.set(source_for_sub.get().to_string());
162    });
163    AttributeValue::Signal(string_signal)
164}
165
166/// Returns a mutable reference to the current hook context.
167///
168/// SAFETY: Must only be called from the main thread (WASM single-threaded context).
169///
170/// # Returns
171///
172/// - `&'static mut Option<HookContextRc>`: A mutable reference to the global hook context.
173#[allow(static_mut_refs)]
174pub(crate) fn try_get_current_hook_context_mut() -> &'static mut Option<HookContextRc> {
175    unsafe { &mut *CURRENT_HOOK_CONTEXT.get_0().get() }
176}
177
178/// Returns a shared reference to the current hook context.
179///
180/// SAFETY: Must only be called from the main thread (WASM single-threaded context).
181///
182/// # Returns
183///
184/// - `&'static Option<HookContextRc>`: A shared reference to the global hook context.
185#[allow(static_mut_refs)]
186pub(crate) fn try_get_current_hook_context() -> &'static Option<HookContextRc> {
187    unsafe { &*CURRENT_HOOK_CONTEXT.get_0().get() }
188}