Skip to main content

leptos_use/core/
option_local_signal.rs

1use leptos::prelude::*;
2use std::ops::{Deref, DerefMut};
3
4/// A signal of an optional send-wrapped type `T` that is always `None` on the server but behaves
5/// on the client like a `Signal<Option<T>>`.
6///
7/// This is useful for handling work-stealing executors like tokio/axum which may move the SSR
8/// render between threads at await points, disallowing the use of `LocalStorage` signals on the
9/// server.
10///
11/// This situation comes up when trying to store browser-api data which is not `Send` (like
12/// Geolocation, Document, etc.) in a signal on the client, while the signal is being set to `None`
13/// on the server.
14pub struct OptionLocalRwSignal<T> {
15    #[cfg(feature = "ssr")]
16    _data: std::marker::PhantomData<fn() -> T>,
17
18    #[cfg(not(feature = "ssr"))]
19    inner: RwSignal<Option<T>, LocalStorage>,
20
21    #[cfg(debug_assertions)]
22    defined_at: &'static std::panic::Location<'static>,
23}
24
25impl<T> OptionLocalRwSignal<T>
26where
27    T: 'static,
28{
29    #[track_caller]
30    pub fn new() -> Self {
31        Self {
32            #[cfg(feature = "ssr")]
33            _data: std::marker::PhantomData,
34
35            #[cfg(not(feature = "ssr"))]
36            inner: RwSignal::new_local(None),
37
38            #[cfg(debug_assertions)]
39            defined_at: std::panic::Location::caller(),
40        }
41    }
42
43    #[track_caller]
44    #[allow(unused_mut)]
45    pub fn read_only(mut self) -> OptionLocalSignal<T> {
46        #[cfg(not(feature = "ssr"))]
47        {
48            // Prolong the lifetime of the original RwSignal. This is basically equivalent to what
49            // RwSignal::read_only() does under the hood.
50            let new_inner = ArcRwSignal::from(self.inner);
51            self.inner = RwSignal::from_local(new_inner);
52        }
53
54        OptionLocalSignal {
55            inner: self,
56            #[cfg(debug_assertions)]
57            defined_at: std::panic::Location::caller(),
58        }
59    }
60}
61
62impl<T> Default for OptionLocalRwSignal<T>
63where
64    T: 'static,
65{
66    #[track_caller]
67    fn default() -> Self {
68        Self::new()
69    }
70}
71
72impl<T> Clone for OptionLocalRwSignal<T> {
73    fn clone(&self) -> Self {
74        *self
75    }
76}
77
78impl<T> Copy for OptionLocalRwSignal<T> {}
79
80impl<T> DefinedAt for OptionLocalRwSignal<T> {
81    #[inline(always)]
82    fn defined_at(&self) -> Option<&'static std::panic::Location<'static>> {
83        #[cfg(debug_assertions)]
84        {
85            Some(self.defined_at)
86        }
87
88        #[cfg(not(debug_assertions))]
89        {
90            None
91        }
92    }
93}
94
95// In order to allow `None::<T>` to be used like a Signal on the server, we have a special
96// `NoneGuard` that always derefs to `None`.
97
98#[cfg(feature = "ssr")]
99pub type OptionLocalGuard<T> = NoneGuard<T>;
100
101#[cfg(not(feature = "ssr"))]
102pub type OptionLocalGuard<T> = guards::ReadGuard<Option<T>, guards::Plain<Option<T>>>;
103
104impl<T> ReadUntracked for OptionLocalRwSignal<T>
105where
106    T: 'static,
107{
108    type Value = OptionLocalGuard<T>;
109
110    #[inline(always)]
111    fn try_read_untracked(&self) -> Option<Self::Value> {
112        #[cfg(feature = "ssr")]
113        {
114            Some(NoneGuard::default())
115        }
116        #[cfg(not(feature = "ssr"))]
117        {
118            self.inner.try_read_untracked()
119        }
120    }
121}
122
123impl<T> Track for OptionLocalRwSignal<T>
124where
125    T: 'static,
126{
127    #[inline(always)]
128    fn track(&self) {
129        #[cfg(feature = "ssr")]
130        {
131            // No-op on server
132        }
133        #[cfg(not(feature = "ssr"))]
134        {
135            self.inner.track();
136        }
137    }
138}
139
140impl<T> Notify for OptionLocalRwSignal<T>
141where
142    T: 'static,
143{
144    #[inline(always)]
145    fn notify(&self) {
146        #[cfg(feature = "ssr")]
147        {
148            leptos::logging::warn!(
149                "Attempted to notify an OptionLocalRwSignal on the server; this has no effect."
150            );
151        }
152        #[cfg(not(feature = "ssr"))]
153        {
154            self.inner.notify();
155        }
156    }
157}
158
159impl<T> Write for OptionLocalRwSignal<T>
160where
161    T: 'static,
162{
163    type Value = Option<T>;
164
165    #[inline(always)]
166    fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
167        #[cfg(feature = "ssr")]
168        {
169            leptos::logging::warn!(
170                "Attempted to write to an OptionLocalRwSignal on the server; this has no effect."
171            );
172            None::<NoneGuard<T>>
173        }
174        #[cfg(not(feature = "ssr"))]
175        {
176            self.inner.try_write()
177        }
178    }
179
180    #[inline(always)]
181    fn try_write_untracked(&self) -> Option<impl DerefMut<Target = Self::Value>> {
182        #[cfg(feature = "ssr")]
183        {
184            leptos::logging::warn!(
185                "Attempted to write to an OptionLocalRwSignal on the server; this has no effect."
186            );
187            None::<NoneGuard<T>>
188        }
189        #[cfg(not(feature = "ssr"))]
190        {
191            self.inner.try_write_untracked()
192        }
193    }
194}
195
196impl<T> IsDisposed for OptionLocalRwSignal<T>
197where
198    T: 'static,
199{
200    #[inline(always)]
201    fn is_disposed(&self) -> bool {
202        #[cfg(feature = "ssr")]
203        {
204            false
205        }
206        #[cfg(not(feature = "ssr"))]
207        {
208            self.inner.is_disposed()
209        }
210    }
211}
212
213/// A guard that only ever returns `None`. While this is implemented as both a read and a write guard,
214/// actually using it as a write guard or dereferencing this mutably will panic.
215/// The WriteGuard implementation only exists to allow returning `None` from `try_write` and `try_write_untracked`.
216pub struct NoneGuard<T> {
217    _data: std::marker::PhantomData<T>,
218}
219
220impl<T> Default for NoneGuard<T> {
221    fn default() -> Self {
222        Self {
223            _data: std::marker::PhantomData,
224        }
225    }
226}
227
228impl<T> Deref for NoneGuard<T> {
229    type Target = Option<T>;
230
231    fn deref(&self) -> &Self::Target {
232        &None
233    }
234}
235
236/// This implementation only exists so that we can create a `None::<impl DerefMut>`.
237impl<T> DerefMut for NoneGuard<T> {
238    fn deref_mut(&mut self) -> &mut Self::Target {
239        panic!("Attempted to mutably dereference a NoneGuard");
240    }
241}
242
243/// This implementation only exists so that we can create a `None::<impl UntrackableGuard>`.
244impl<T> UntrackableGuard for NoneGuard<T> {
245    fn untrack(&mut self) {
246        panic!("Attempted to untrack a NoneGuard");
247    }
248}
249
250/// Read-Only signal
251pub struct OptionLocalSignal<T> {
252    #[cfg(debug_assertions)]
253    defined_at: &'static std::panic::Location<'static>,
254
255    inner: OptionLocalRwSignal<T>,
256}
257
258impl<T> Clone for OptionLocalSignal<T> {
259    fn clone(&self) -> Self {
260        *self
261    }
262}
263
264impl<T> Copy for OptionLocalSignal<T> {}
265
266impl<T> DefinedAt for OptionLocalSignal<T> {
267    #[inline(always)]
268    fn defined_at(&self) -> Option<&'static std::panic::Location<'static>> {
269        #[cfg(debug_assertions)]
270        {
271            Some(self.defined_at)
272        }
273        #[cfg(not(debug_assertions))]
274        {
275            None
276        }
277    }
278}
279
280impl<T> ReadUntracked for OptionLocalSignal<T>
281where
282    T: 'static,
283{
284    type Value = OptionLocalGuard<T>;
285
286    #[inline(always)]
287    fn try_read_untracked(&self) -> Option<Self::Value> {
288        self.inner.try_read_untracked()
289    }
290}
291
292impl<T> Track for OptionLocalSignal<T>
293where
294    T: 'static,
295{
296    #[inline(always)]
297    fn track(&self) {
298        self.inner.track();
299    }
300}