Skip to main content

rustolio_web/hooks/
mod.rs

1//
2// SPDX-License-Identifier: MPL-2.0
3//
4// Copyright (c) 2026 Tobias Binnewies. All rights reserved.
5//
6// This Source Code Form is subject to the terms of the Mozilla Public
7// License, v. 2.0. If a copy of the MPL was not distributed with this
8// file, You can obtain one at http://mozilla.org/MPL/2.0/.
9//
10
11mod calculated;
12mod callbacks;
13mod context;
14mod effect;
15mod global;
16mod query;
17mod scope;
18mod signal;
19mod storage;
20
21use scope::{Dependency, Scope};
22
23pub(crate) use callbacks::{signal_updater_callback, signal_updater_callback_full};
24
25pub use calculated::Calculated;
26pub use context::Context;
27pub use effect::Effect;
28pub use global::{GlobalWrapper, global};
29pub use query::QuerySignal;
30pub use signal::{
31    GlobalSignal, Signal, SignalBase, SignalGetter, SignalSetter, SignalToggle, SignalUpdater,
32};
33pub use storage::StorageSignal;
34
35/// Runs the provided future.
36///
37/// This can be used to fetch resources or perform any operation like reding the local storage.
38///
39/// The future runs after the DOM has loaded, so state changes will be reflected in the UI.
40///
41/// The given future is guaranteed to run after the current render cycle is complete.
42///
43/// **Important**: The provided future will run until the end even if the scope which started it was already replaced. So if scoped signals are read / updated after the scope and therefore signals are removed the whole application crashes.
44#[inline]
45pub fn spawn<F>(future: F)
46where
47    F: std::future::Future<Output = ()> + 'static,
48{
49    let scope_id = Scope::current();
50    spawn_raw(async move {
51        let Some(scope_id) = scope_id else {
52            future.await;
53            return;
54        };
55        Scope::push_existing(scope_id);
56        future.await;
57        Scope::pop();
58    });
59}
60
61pub fn spawn_result<F>(future: F)
62where
63    F: std::future::Future<Output = rustolio_utils::Result<()>> + 'static,
64{
65    spawn(async move { crate::error::match_result("Spawn", future.await) });
66}
67
68/// Creates a timeout for the specified number of milliseconds. This runs asynchronously and therefore does not block any thread.
69///
70/// A timeout is cancelled when the scope in which it was created goes out of scope. In this case, any calls after the timeout will not be made.
71// TODO: Is this a problem (promise will not be resolved nor rejected)? e.g. does this adds up in memory usage??
72pub async fn timeout(ms: u16) -> crate::Result<()> {
73    #[cfg(all(not(target_arch = "wasm32")))]
74    {
75        use std::time::Duration;
76        tokio::time::sleep(Duration::from_millis(ms as u64)).await;
77    }
78
79    #[cfg(target_arch = "wasm32")]
80    {
81        use crate::prelude::*;
82        let _ = (|resolve, _| {
83            let id = crate::prelude::window()
84                .unwrap() // Will be catched in the future unwrap
85                .set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms as i32)
86                .unwrap(); // Will be catched in the future unwrap
87            Scope::register_timeout(id);
88        })
89        .future::<JsValue>()
90        .await
91        .context("Failed to create run timeout")?;
92    }
93
94    Ok(())
95}
96
97/// Runs the provided future without adding / popping a scope
98fn spawn_raw<F>(future: F)
99where
100    F: std::future::Future<Output = ()> + 'static,
101{
102    // Block on non-wasm32 targets for testing purposes
103    #[cfg(not(target_arch = "wasm32"))]
104    futures::executor::block_on(future);
105
106    // Spawn on wasm32 targets
107    #[cfg(target_arch = "wasm32")]
108    {
109        wasm_bindgen_futures::spawn_local(future);
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use std::rc::Rc;
116
117    use super::*;
118
119    macro_rules! signal_getter {
120        ($($signal:ident), *) => {
121                $(
122                    $signal.value();
123                    $signal.peek();
124                )*
125        };
126    }
127
128    macro_rules! signal_setter {
129        ($val:expr, $($signal:ident), *) => {
130                $(
131                    $signal.set($val);
132                )*
133        };
134    }
135
136    macro_rules! signal_updater {
137        ($val:expr, $($signal:ident), *) => {
138                $(
139                    $signal.update(|s| *s = $val);
140                    $signal.set_unchecked($val);
141                )*
142        };
143    }
144
145    #[derive(Clone, PartialEq)]
146    struct _ClonePartialeq;
147    fn _signals_clone_partialeq() {
148        let s = Signal::new(_ClonePartialeq);
149        let g = GlobalSignal::new(_ClonePartialeq);
150        let c = Calculated::new(|| _ClonePartialeq);
151        signal_getter!(s, g, c);
152        signal_setter!(_ClonePartialeq, s, g);
153        signal_updater!(_ClonePartialeq, s, g);
154    }
155
156    #[derive(Clone)]
157    struct _Clone;
158    fn _signals_clone() {
159        let s = Signal::new(_Clone);
160        let g = GlobalSignal::new(_Clone);
161        let c = Calculated::new(|| _Clone);
162        signal_getter!(s, g, c);
163        signal_updater!(_Clone, s, g);
164    }
165
166    #[derive(PartialEq)]
167    struct _Partialeq;
168    fn _signals_partialeq() {
169        let s = Signal::new(_Partialeq);
170        let g = GlobalSignal::new(_Partialeq);
171        let c = Calculated::new(|| Rc::new(_Partialeq));
172        signal_getter!(c);
173        signal_setter!(_Partialeq, s, g);
174        signal_updater!(_Partialeq, s, g);
175    }
176
177    struct _Nothing;
178    fn _signals() {
179        let s = Signal::new(_Nothing);
180        let g = GlobalSignal::new(_Nothing);
181        let c = Calculated::new(|| Rc::new(_Nothing));
182        signal_getter!(c);
183        signal_updater!(_Nothing, s, g);
184    }
185}