rustolio_web/hooks/global.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
11use super::{Effect, Signal, SignalBase, SignalGetter, SignalSetter, SignalUpdater};
12
13/// Can be used to create a [`GlobalSignal`], a global [`Calculated`] or a global [`Effect`].
14///
15/// [`GlobalSignal`]: crate::prelude::GlobalSignal
16/// [`Calculated`]: crate::prelude::Calculated
17/// [`Effect`]: crate::prelude::Effect
18#[macro_export]
19macro_rules! __global {
20 // Empty case for recursion
21 () => {};
22
23 // // Global Signal - Result
24 // ($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty = ($init:expr)?; $($rest:tt)*) => {
25 // $(#[$attr])*
26 // $vis static $name: GlobalWrapper<$t> = {
27 // thread_local! {
28 // static SIGNAL: $t = unsafe {
29 // let s = $init;
30 // let s = $crate::prelude::__web_global_macro::match_result("global!", s);
31 // $crate::hooks::SignalBase::globalize(&s);
32 // s
33 // };
34 // }
35 // GlobalWrapper::__new(&SIGNAL)
36 // };
37 // $crate::__global!($($rest)*);
38 // };
39
40 // Global Signal
41 ($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty = $init:expr; $($rest:tt)*) => {
42 $(#[$attr])*
43 $vis static $name: GlobalWrapper<$t> = {
44 thread_local! {
45 static __SIGNAL: $t = unsafe {
46 let s = $init;
47 $crate::hooks::SignalBase::globalize(&s);
48 s
49 };
50 }
51 GlobalWrapper::__new(&__SIGNAL)
52 };
53 $crate::__global!($($rest)*);
54 };
55
56 // // Global Signal
57 // ($(#[$attr:meta])* $vis:vis static $name:ident: GlobalSignal<$t:ty> = $init:expr; $($rest:tt)*) => {
58 // $(#[$attr])*
59 // $vis static $name: GlobalWrapper<GlobalSignal<$t>> = {
60 // thread_local! {
61 // static SIGNAL: GlobalSignal<$t> = $init;
62 // }
63 // GlobalWrapper::__new(&SIGNAL)
64 // };
65 // $crate::__global!($($rest)*);
66 // };
67
68 // // Global Calculate
69 // ($(#[$attr:meta])* $vis:vis static $name:ident: Calculated<$t:ty> = Calculated::new($init:expr); $($rest:tt)*) => {
70 // $(#[$attr])*
71 // $vis static $name: GlobalWrapper<Calculated<$t>> = {
72 // thread_local! {
73 // static CALCULATED: Calculated<$t> = unsafe {
74 // // SAFETY: Cannot use scoped signals here
75 // Calculated::__global($init)
76 // };
77 // }
78 // GlobalWrapper::__new(&CALCULATED)
79 // };
80 // $crate::__global!($($rest)*);
81 // };
82 // ($(#[$attr:meta])* $vis:vis static $name:ident: Calculated<$t:ty> = Calculated::new_result($init:expr); $($rest:tt)*) => {
83 // $(#[$attr])*
84 // $vis static $name: GlobalWrapper<Calculated<$t>> = {
85 // thread_local! {
86 // static CALCULATED: Calculated<$t> = unsafe {
87 // // SAFETY: Cannot use scoped signals here
88 // Calculated::__global_result($init)
89 // };
90 // }
91 // GlobalWrapper::__new(&CALCULATED)
92 // };
93 // $crate::__global!($($rest)*);
94 // };
95
96 // Global Effect
97 (Effect::$method:ident($init:expr); $($rest:tt)*) => {
98 thread_local! {
99 static __EFFECT: Effect = unsafe {
100 // SAFETY: Cannot use scoped signals here
101 let mut e = Effect::$method($init);
102 e.deregister();
103 e
104 };
105 }
106 __web_global_macro::inventory::submit! { GlobalWrapper::__new(&__EFFECT) }
107 $crate::__global!($($rest)*);
108 };
109
110 // // thread_local! copy for convenience
111 // ($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty = const $init:block; $($rest:tt)*) => {
112 // thread_local! {
113 // $(#[$attr])* $vis static $name: $t = const $init;
114 // }
115 // $crate::__global!($($rest)*);
116 // };
117
118 // // thread_local! copy for convenience
119 // ($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty = $init:expr; $($rest:tt)*) => {
120 // thread_local! {
121 // $vis static $name: $t = $init;
122 // }
123 // $crate::__global!($($rest)*);
124 // };
125}
126pub use __global as global;
127
128#[derive(Debug, Clone, Copy)]
129pub struct GlobalWrapper<S: 'static> {
130 inner: &'static std::thread::LocalKey<S>,
131}
132
133impl<S> GlobalWrapper<S>
134where
135 S: Clone + 'static,
136{
137 /// Should only called using the [`global!`] macro.
138 #[doc(hidden)]
139 pub const fn __new(inner: &'static std::thread::LocalKey<S>) -> Self {
140 GlobalWrapper { inner }
141 }
142}
143
144impl<S, T> SignalBase<T> for GlobalWrapper<S>
145where
146 S: SignalBase<T>,
147{
148 fn base(&self) -> Signal<T> {
149 self.inner.with(|s| s.base())
150 }
151}
152impl<S, T> SignalGetter<T> for GlobalWrapper<S>
153where
154 S: SignalGetter<T>,
155 T: Clone + 'static,
156{
157}
158impl<S, T> SignalSetter<T> for GlobalWrapper<S>
159where
160 S: Copy + SignalSetter<T>,
161 T: PartialEq + 'static,
162{
163}
164impl<S, T> SignalUpdater<T> for GlobalWrapper<S>
165where
166 S: Copy + SignalUpdater<T>,
167 T: 'static,
168{
169}
170
171impl<S, T> From<GlobalWrapper<S>> for Signal<T>
172where
173 S: Copy + SignalGetter<T>,
174 T: Clone + 'static,
175{
176 fn from(val: GlobalWrapper<S>) -> Self {
177 val.base()
178 }
179}
180
181inventory::collect!(GlobalWrapper<Effect>);
182
183impl GlobalWrapper<Effect> {
184 pub(crate) fn register_globals() {
185 #[cfg(target_arch = "wasm32")]
186 {
187 // FIX: wasm builds run out of memory
188 // https://github.com/dtolnay/inventory/issues/77
189 unsafe extern "C" {
190 fn __wasm_call_ctors();
191 }
192 unsafe { __wasm_call_ctors() };
193 }
194
195 for effect in inventory::iter::<GlobalWrapper<Effect>> {
196 effect.inner.with(|_f| {})
197 }
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use std::sync::atomic::{AtomicUsize, Ordering};
204
205 use crate::prelude::*;
206
207 static EFFECT_CALLED: AtomicUsize = AtomicUsize::new(0);
208
209 global! {
210 // static SOME_VALUE: String = String::from("test");
211 // static SOME_MUT_VALUE: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
212
213 static GLOBAL_SIGNAL1: GlobalSignal<i32> = GlobalSignal::new(0);
214 static GLOBAL_SIGNAL2: GlobalSignal<i32> = GlobalSignal::new(0);
215 static GLOBAL_CALCULATED1: Calculated<i32> = Calculated::new(|| GLOBAL_SIGNAL1.value() * 2);
216 // static GLOBAL_SIGNAL3: Signal<i32> = 0;
217 Effect::new(|| {
218 EFFECT_CALLED.fetch_add(1, Ordering::Relaxed);
219 let _ = GLOBAL_SIGNAL1.value();
220 });
221 }
222
223 // #[test]
224 // fn test_thread_local() {
225 // assert_eq!(SOME_VALUE.with(|v| v.clone()), String::from("test"));
226 // SOME_MUT_VALUE.with(|v| v.borrow_mut().push(1));
227 // SOME_MUT_VALUE.with(|v| assert_eq!(v.borrow().as_ref(), vec![1]));
228 // }
229
230 #[test]
231 fn test_global_signal() {
232 assert_eq!(EFFECT_CALLED.load(Ordering::Relaxed), 0);
233
234 GlobalWrapper::register_globals();
235 assert_eq!(EFFECT_CALLED.load(Ordering::Relaxed), 1);
236
237 assert_eq!(GLOBAL_SIGNAL1.value(), 0);
238
239 GLOBAL_SIGNAL1.set(42);
240 assert_eq!(GLOBAL_SIGNAL1.value(), 42);
241 assert_eq!(EFFECT_CALLED.load(Ordering::Relaxed), 2);
242
243 GLOBAL_SIGNAL1.update(|v| *v += 1);
244 assert_eq!(GLOBAL_SIGNAL1.value(), 43);
245 assert_eq!(EFFECT_CALLED.load(Ordering::Relaxed), 3);
246
247 assert_eq!(GLOBAL_CALCULATED1.value(), 43 * 2);
248 }
249
250 #[test]
251 fn test_deref() {
252 let local_signal: Signal<i32> = GLOBAL_SIGNAL2.into();
253 assert_eq!(local_signal.value(), 0);
254 local_signal.set(50);
255 assert_eq!(local_signal.value(), 50);
256 assert_eq!(GLOBAL_SIGNAL2.value(), 50);
257 }
258}