Skip to main content

euv_core/reactive/schedule/
fn.rs

1use crate::*;
2
3thread_local! {
4    /// The persistent dispatch `Closure`, kept alive for the lifetime of the
5    /// program so it can be handed to `setTimeout` repeatedly.
6    ///
7    /// The closure resets the `SCHEDULED` flag and then runs the queued signal
8    /// update callbacks. Resetting `SCHEDULED` here is what allows the next
9    /// `schedule_signal_update_targeted` call to schedule a fresh dispatch; if
10    /// this never ran, the flag would stay `true` forever and every reactive
11    /// update would be silently dropped.
12    static DISPATCH_CLOSURE: closure::Closure<dyn FnMut()> =
13        closure::Closure::wrap(Box::new(|| {
14            SCHEDULED.store(false, Ordering::Relaxed);
15            dispatch_signal_update_callbacks();
16        }) as Box<dyn FnMut()>);
17}
18
19/// Invokes `callback` with a reference to the persistent dispatch `Function`.
20///
21/// The dispatch closure is created once per thread and stored in
22/// `DISPATCH_CLOSURE`. This helper exposes it as a `&Function` so it can be
23/// passed to the various browser scheduling APIs (`setTimeout`,
24/// `queueMicrotask`, `requestAnimationFrame`) without recreating the closure
25/// on every schedule.
26///
27/// # Arguments
28///
29/// - `FnOnce(&Function) -> R` - Receives the dispatch function reference.
30///
31/// # Returns
32///
33/// - `R` - The value returned by `callback`.
34fn with_dispatch_function<F, R>(callback: F) -> R
35where
36    F: FnOnce(&Function) -> R,
37{
38    DISPATCH_CLOSURE.with(|dispatch_closure| {
39        let dispatch_function: &Function = dispatch_closure.as_ref().unchecked_ref::<Function>();
40        callback(dispatch_function)
41    })
42}
43
44/// Schedules a deferred signal update event.
45///
46/// If a schedule is already pending (`SCHEDULED` is true) or updates
47/// are suppressed (`SUPPRESS_SCHEDULE` is true), this is a no-op.
48/// Otherwise, sets `SCHEDULED` to true and queues the dispatch callback
49/// (preferring `queueMicrotask`) on WASM targets. This ensures that no
50/// matter how many signal updates occur within a single task, only one
51/// dispatch cycle runs, preventing CPU spikes during rapid input events
52/// (e.g., slider dragging).
53///
54/// On non-WASM targets, resets `SCHEDULED` immediately since there is
55/// no event loop to schedule on.
56pub fn schedule_signal_update() {
57    schedule_signal_update_targeted(&[]);
58}
59
60/// Schedules a deferred signal update with precise dirty marking.
61///
62/// If `dependents` is non-empty, only those dynamic node IDs are marked
63/// dirty. If `dependents` is empty, falls back to marking all slots dirty
64/// (for backwards compatibility with `batch_updates` and other callers
65/// that don't track dependencies).
66///
67/// # Arguments
68///
69/// - `&[usize]` - Dynamic node IDs to mark dirty. Empty slice means mark all.
70pub fn schedule_signal_update_targeted(dependents: &[usize]) {
71    if SUPPRESS_SCHEDULE.load(Ordering::Relaxed) {
72        if !dependents.is_empty() {
73            mark_slots_dirty_targeted(dependents);
74        } else {
75            mark_all_slots_dirty();
76        }
77        return;
78    }
79    if !dependents.is_empty() {
80        mark_slots_dirty_targeted(dependents);
81    } else {
82        mark_all_slots_dirty();
83    }
84    if SCHEDULED.load(Ordering::Relaxed) {
85        return;
86    }
87    SCHEDULED.store(true, Ordering::Relaxed);
88    let window_value: Window = match window() {
89        Some(window_instance) => window_instance,
90        None => {
91            SCHEDULED.store(false, Ordering::Relaxed);
92            return;
93        }
94    };
95    let queued_microtask: bool = with_dispatch_function(|dispatch_function| {
96        let queue_microtask_value: JsValue =
97            Reflect::get(&window_value, &JsValue::from_str(QUEUE_MICROTASK))
98                .unwrap_or(JsValue::UNDEFINED);
99        matches!(
100            queue_microtask_value.dyn_into::<Function>(),
101            Ok(queue_microtask) if queue_microtask.call1(&window_value, dispatch_function).is_ok()
102        )
103    });
104    if queued_microtask {
105        return;
106    }
107    let scheduled: bool = with_dispatch_function(|dispatch_function| {
108        window_value
109            .set_timeout_with_callback_and_timeout_and_arguments_0(dispatch_function, 0)
110            .is_ok()
111    });
112    if scheduled {
113        return;
114    }
115    let requested_frame: bool = with_dispatch_function(|dispatch_function| {
116        window_value
117            .request_animation_frame(dispatch_function)
118            .is_ok()
119    });
120    if requested_frame {
121        return;
122    }
123    SCHEDULED.store(false, Ordering::Relaxed);
124}
125
126/// Batches signal updates within a closure, deferring DOM synchronization until completion.
127///
128/// Saves the current `SUPPRESS_SCHEDULE` flag, sets it to `true`,
129/// executes the closure, and restores the previous flag value.
130/// This prevents `schedule_signal_update` from queuing microtasks
131/// during the closure execution, allowing multiple signal mutations
132/// to be applied before triggering a single DOM update cycle.
133///
134/// When the outermost `batch_updates` call completes (i.e., the previous
135/// suppress flag was `false`), a single `schedule_signal_update()` is
136/// invoked to ensure that any signal mutations performed inside the
137/// closure are reflected in the DOM. This is critical for `watch!`
138/// initialisation, where `Console::log` calls mutate the console signal
139/// inside the batched block and must still trigger DynamicNode re-renders.
140///
141/// # Arguments
142///
143/// - `FnOnce() -> R` - The closure to execute with batched updates.
144///
145/// # Returns
146///
147/// - `R` - The result of the closure execution.
148pub fn batch_updates<F, R>(callback: F) -> R
149where
150    F: FnOnce() -> R,
151{
152    let was_outermost: bool = !SUPPRESS_SCHEDULE.load(Ordering::Relaxed);
153    SUPPRESS_SCHEDULE.store(true, Ordering::Relaxed);
154    let result: R = callback();
155    if was_outermost {
156        SUPPRESS_SCHEDULE.store(false, Ordering::Relaxed);
157        schedule_signal_update();
158    } else {
159        SUPPRESS_SCHEDULE.store(false, Ordering::Relaxed);
160    }
161    result
162}
163
164/// Subscribes an attribute signal to the global signal update dispatch cycle.
165///
166/// Creates a callback that re-computes the attribute value and sets
167/// it on the signal whenever a signal update cycle runs. The callback
168/// is registered in the signal update registry using the signal's
169/// inner address as the key.
170///
171/// # Arguments
172///
173/// - `Signal<String>` - The attribute signal to subscribe.
174/// - `Fn() -> String + 'static` - A closure that computes the current attribute value string.
175pub(crate) fn subscribe_attr_signal<F>(attr_signal: Signal<String>, compute: F)
176where
177    F: Fn() -> String + 'static,
178{
179    register_attr_signal_listener(
180        attr_signal.get_inner(),
181        Box::new(move || {
182            attr_signal.set_silent(compute());
183        }),
184    );
185}
186
187/// Converts a bool signal into a reactive `Signal<String>` attribute value.
188///
189/// Creates a `Signal<String>` initialized with the bool's string
190/// representation, then subscribes to the source signal so that
191/// whenever the bool changes, the string signal is updated accordingly.
192///
193/// # Arguments
194///
195/// - `Signal<bool>` - The source boolean signal.
196///
197/// # Returns
198///
199/// - `AttributeValue` - An `AttributeValue::Signal` wrapping the derived string signal.
200pub(crate) fn bool_signal_to_string_attribute_value(source: Signal<bool>) -> AttributeValue {
201    let string_signal: Signal<String> = Signal::create(source.get().to_string());
202    let string_signal_clone: Signal<String> = string_signal;
203    source.subscribe({
204        let source_inner: Signal<bool> = source;
205        move || {
206            string_signal_clone.set_silent(source_inner.get().to_string());
207        }
208    });
209    AttributeValue::Signal(string_signal)
210}
211
212/// Returns a mutable reference to the current hook context.
213///
214/// SAFETY: Must only be called from the main thread (WASM single-threaded context).
215///
216/// # Returns
217///
218/// - `&'static mut Option<HookContextRc>`: A mutable reference to the global hook context.
219#[allow(static_mut_refs)]
220pub(crate) fn current_hook_context_mut() -> &'static mut Option<HookContextRc> {
221    unsafe { &mut *CURRENT_HOOK_CONTEXT.get_0().get() }
222}
223
224/// Returns a shared reference to the current hook context.
225///
226/// SAFETY: Must only be called from the main thread (WASM single-threaded context).
227///
228/// # Returns
229///
230/// - `&'static Option<HookContextRc>`: A shared reference to the global hook context.
231#[allow(static_mut_refs)]
232pub(crate) fn current_hook_context() -> &'static Option<HookContextRc> {
233    unsafe { &*CURRENT_HOOK_CONTEXT.get_0().get() }
234}