Skip to main content

reactive_stores/
option.rs

1use crate::{
2    KeyMap, StoreField, StoreFieldTrigger,
3    path::{StorePath, StorePathSegment},
4};
5use reactive_graph::{
6    signal::{
7        ArcTrigger,
8        guards::{Mapped, MappedMut, WriteGuard},
9    },
10    traits::{
11        DefinedAt, FlattenOptionRefOption, IsDisposed, Notify, Read,
12        ReadUntracked, Track, UntrackableGuard, Write,
13    },
14};
15use std::{
16    iter,
17    marker::PhantomData,
18    ops::{Deref, DerefMut},
19    panic::Location,
20};
21
22/// Accesses the inner value of an `Option`-typed store field.
23///
24/// Unlike a plain [`Subfield`](crate::Subfield), this projection is fallible:
25/// its [`reader`](StoreField::reader)/[`writer`](StoreField::writer) return
26/// `None` when the underlying field is currently `None`, instead of handing
27/// back a guard that panics when dereferenced.
28pub struct OptionSubfield<Inner, T> {
29    #[cfg(any(debug_assertions, leptos_debuginfo))]
30    defined_at: &'static Location<'static>,
31    path_segment: StorePathSegment,
32    inner: Inner,
33    ty: PhantomData<T>,
34}
35
36impl<Inner, T> Clone for OptionSubfield<Inner, T>
37where
38    Inner: Clone,
39{
40    fn clone(&self) -> Self {
41        Self {
42            #[cfg(any(debug_assertions, leptos_debuginfo))]
43            defined_at: self.defined_at,
44            path_segment: self.path_segment,
45            inner: self.inner.clone(),
46            ty: self.ty,
47        }
48    }
49}
50
51impl<Inner, T> Copy for OptionSubfield<Inner, T> where Inner: Copy {}
52
53impl<Inner, T> OptionSubfield<Inner, T> {
54    /// Creates an accessor for the inner value of an `Option`-typed field.
55    #[track_caller]
56    pub fn new(inner: Inner) -> Self {
57        Self {
58            #[cfg(any(debug_assertions, leptos_debuginfo))]
59            defined_at: Location::caller(),
60            path_segment: 0.into(),
61            inner,
62            ty: PhantomData,
63        }
64    }
65}
66
67impl<Inner, T> StoreField for OptionSubfield<Inner, T>
68where
69    Inner: StoreField<Value = Option<T>>,
70    T: 'static,
71{
72    type Value = T;
73    type Reader = Mapped<Inner::Reader, T>;
74    type Writer = MappedMut<WriteGuard<Vec<ArcTrigger>, Inner::Writer>, T>;
75
76    fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
77        self.inner
78            .path()
79            .into_iter()
80            .chain(iter::once(self.path_segment))
81    }
82
83    fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
84        self.inner
85            .path_unkeyed()
86            .into_iter()
87            .chain(iter::once(self.path_segment))
88    }
89
90    fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
91        self.inner.get_trigger(path)
92    }
93
94    fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger {
95        self.inner.get_trigger_unkeyed(path)
96    }
97
98    fn reader(&self) -> Option<Self::Reader> {
99        let inner = self.inner.reader()?;
100        // The reader holds the inner lock for its whole lifetime, so the
101        // value cannot toggle to `None` before the (lazy) projection runs on
102        // deref. Bail out here instead of handing back a guard that would
103        // panic in `as_ref().unwrap()`.
104        if inner.is_none() {
105            return None;
106        }
107        Some(Mapped::new_with_guard(inner, |t| t.as_ref().unwrap()))
108    }
109
110    fn writer(&self) -> Option<Self::Writer> {
111        let mut parent = self.inner.writer()?;
112        // See `reader`: the write guard holds the inner lock, so a single
113        // check here keeps the `as_mut().unwrap()` projection panic-free.
114        if parent.is_none() {
115            return None;
116        }
117        // untrack the parent so it doesn't notify its `this` trigger (which
118        // would notify siblings); the path triggers are included below.
119        parent.untrack();
120        let triggers = self.triggers_for_current_path();
121        let guard = WriteGuard::new(triggers, parent);
122        Some(MappedMut::new(
123            guard,
124            |t| t.as_ref().unwrap(),
125            |t| t.as_mut().unwrap(),
126        ))
127    }
128
129    #[inline(always)]
130    fn keys(&self) -> Option<KeyMap> {
131        self.inner.keys()
132    }
133
134    #[track_caller]
135    fn track_field(&self) {
136        let mut full_path = self.path().into_iter().collect::<StorePath>();
137        let trigger = self.get_trigger(self.path().into_iter().collect());
138        trigger.this.track();
139        trigger.children.track();
140
141        while !full_path.is_empty() {
142            full_path.pop();
143            let inner = self.get_trigger(full_path.clone());
144            inner.this.track();
145        }
146    }
147}
148
149impl<Inner, T> DefinedAt for OptionSubfield<Inner, T> {
150    fn defined_at(&self) -> Option<&'static Location<'static>> {
151        #[cfg(any(debug_assertions, leptos_debuginfo))]
152        {
153            Some(self.defined_at)
154        }
155        #[cfg(not(any(debug_assertions, leptos_debuginfo)))]
156        {
157            None
158        }
159    }
160}
161
162impl<Inner, T> IsDisposed for OptionSubfield<Inner, T>
163where
164    Inner: IsDisposed,
165{
166    fn is_disposed(&self) -> bool {
167        self.inner.is_disposed()
168    }
169}
170
171impl<Inner, T> Notify for OptionSubfield<Inner, T>
172where
173    Inner: StoreField<Value = Option<T>>,
174    T: 'static,
175{
176    #[track_caller]
177    fn notify(&self) {
178        let trigger = self.get_trigger(self.path().into_iter().collect());
179        trigger.this.notify();
180        trigger.children.notify();
181    }
182}
183
184impl<Inner, T> Track for OptionSubfield<Inner, T>
185where
186    Inner: StoreField<Value = Option<T>> + Track + 'static,
187    T: 'static,
188{
189    #[track_caller]
190    fn track(&self) {
191        self.track_field();
192    }
193}
194
195impl<Inner, T> ReadUntracked for OptionSubfield<Inner, T>
196where
197    Inner: StoreField<Value = Option<T>>,
198    T: 'static,
199{
200    type Value = <Self as StoreField>::Reader;
201
202    fn try_read_untracked(&self) -> Option<Self::Value> {
203        self.reader()
204    }
205}
206
207impl<Inner, T> Write for OptionSubfield<Inner, T>
208where
209    Inner: StoreField<Value = Option<T>>,
210    T: 'static,
211{
212    type Value = T;
213
214    fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
215        self.writer()
216    }
217
218    fn try_write_untracked(
219        &self,
220    ) -> Option<impl DerefMut<Target = Self::Value>> {
221        self.writer().map(|mut writer| {
222            writer.untrack();
223            writer
224        })
225    }
226}
227
228/// Extends optional store fields, with the ability to unwrap or map over them.
229pub trait OptionStoreExt
230where
231    Self: StoreField<Value = Option<Self::Output>>,
232{
233    /// The inner type of the `Option<_>` this field holds.
234    type Output;
235
236    /// Provides access to the inner value, as a subfield, unwrapping the outer value.
237    ///
238    /// The returned field reads and writes fallibly: if the outer value becomes
239    /// `None` before the projection runs, its reader/writer yield `None` rather
240    /// than panicking.
241    fn unwrap(self) -> OptionSubfield<Self, Self::Output>;
242
243    /// Inverts a subfield of an `Option` to an `Option` of a subfield.
244    fn invert(self) -> Option<OptionSubfield<Self, Self::Output>> {
245        self.map(|f| f)
246    }
247
248    /// Reactively maps over the field.
249    ///
250    /// This returns `None` if the subfield is currently `None`,
251    /// and a new store subfield with the inner value if it is `Some`. This can be used in some
252    /// other reactive context, which will cause it to re-run if the field toggles between `None`
253    /// and `Some(_)`.
254    fn map<U>(
255        self,
256        map_fn: impl FnOnce(OptionSubfield<Self, Self::Output>) -> U,
257    ) -> Option<U>;
258
259    /// Unreactively maps over the field.
260    ///
261    /// This returns `None` if the subfield is currently `None`,
262    /// and a new store subfield with the inner value if it is `Some`. This is an unreactive variant of
263    /// `[OptionStoreExt::map]`, and will not cause the reactive context to re-run if the field changes.
264    fn map_untracked<U>(
265        self,
266        map_fn: impl FnOnce(OptionSubfield<Self, Self::Output>) -> U,
267    ) -> Option<U>;
268}
269
270impl<T, S> OptionStoreExt for S
271where
272    S: StoreField<Value = Option<T>> + Read + ReadUntracked,
273    <S as Read>::Value: Deref<Target = Option<T>>,
274    <S as ReadUntracked>::Value: Deref<Target = Option<T>>,
275{
276    type Output = T;
277
278    fn unwrap(self) -> OptionSubfield<Self, Self::Output> {
279        OptionSubfield::new(self)
280    }
281
282    fn map<U>(
283        self,
284        map_fn: impl FnOnce(OptionSubfield<S, T>) -> U,
285    ) -> Option<U> {
286        if self.try_read().as_deref().flatten().is_some() {
287            Some(map_fn(self.unwrap()))
288        } else {
289            None
290        }
291    }
292
293    fn map_untracked<U>(
294        self,
295        map_fn: impl FnOnce(OptionSubfield<S, T>) -> U,
296    ) -> Option<U> {
297        if self.try_read_untracked().as_deref().flatten().is_some() {
298            Some(map_fn(self.unwrap()))
299        } else {
300            None
301        }
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use crate::{self as reactive_stores, Patch as _, Store};
308    use any_spawner::Executor;
309    use reactive_graph::{
310        effect::Effect,
311        traits::{Get, Read, ReadUntracked, Set, Write},
312    };
313    use reactive_stores_macro::Patch;
314    use std::sync::{
315        Arc,
316        atomic::{AtomicUsize, Ordering},
317    };
318
319    pub async fn tick() {
320        Executor::tick().await;
321    }
322
323    #[derive(Debug, Clone, Store)]
324    pub struct User {
325        pub name: Option<Name>,
326    }
327
328    #[derive(Debug, Clone, Store)]
329    pub struct Name {
330        pub first_name: Option<String>,
331    }
332
333    #[tokio::test]
334    async fn substores_reachable_through_option() {
335        use crate::OptionStoreExt;
336
337        _ = any_spawner::Executor::init_tokio();
338
339        let combined_count = Arc::new(AtomicUsize::new(0));
340
341        let store = Store::new(User { name: None });
342
343        Effect::new_sync({
344            let combined_count = Arc::clone(&combined_count);
345            move |prev: Option<()>| {
346                if prev.is_none() {
347                    println!("first run");
348                } else {
349                    println!("next run");
350                }
351
352                if store.name().read().is_some() {
353                    println!(
354                        "inner value = {:?}",
355                        *store.name().unwrap().first_name().read()
356                    );
357                } else {
358                    println!("no inner value");
359                }
360
361                combined_count.fetch_add(1, Ordering::Relaxed);
362            }
363        });
364
365        tick().await;
366        store.name().set(Some(Name {
367            first_name: Some("Greg".into()),
368        }));
369        tick().await;
370        store.name().set(None);
371        tick().await;
372        store.name().set(Some(Name {
373            first_name: Some("Bob".into()),
374        }));
375        tick().await;
376        store
377            .name()
378            .unwrap()
379            .first_name()
380            .write()
381            .as_mut()
382            .unwrap()
383            .push_str("!!!");
384        tick().await;
385        assert_eq!(combined_count.load(Ordering::Relaxed), 5);
386        assert_eq!(
387            store
388                .name()
389                .read_untracked()
390                .as_ref()
391                .unwrap()
392                .first_name
393                .as_ref()
394                .unwrap(),
395            "Bob!!!"
396        );
397    }
398
399    #[tokio::test]
400    async fn mapping_over_optional_store_field() {
401        use crate::OptionStoreExt;
402
403        _ = any_spawner::Executor::init_tokio();
404
405        let parent_count = Arc::new(AtomicUsize::new(0));
406        let inner_count = Arc::new(AtomicUsize::new(0));
407
408        let store = Store::new(User { name: None });
409
410        Effect::new_sync({
411            let parent_count = Arc::clone(&parent_count);
412            move |prev: Option<()>| {
413                if prev.is_none() {
414                    println!("parent: first run");
415                } else {
416                    println!("parent: next run");
417                }
418
419                println!("  is_some = {}", store.name().read().is_some());
420                parent_count.fetch_add(1, Ordering::Relaxed);
421            }
422        });
423        Effect::new_sync({
424            let inner_count = Arc::clone(&inner_count);
425            move |prev: Option<()>| {
426                if prev.is_none() {
427                    println!("inner: first run");
428                } else {
429                    println!("inner: next run");
430                }
431
432                println!(
433                    "store inner value length = {:?}",
434                    store.name().map(|inner| inner
435                        .first_name()
436                        .get()
437                        .unwrap_or_default()
438                        .len())
439                );
440                inner_count.fetch_add(1, Ordering::Relaxed);
441            }
442        });
443
444        tick().await;
445        assert_eq!(parent_count.load(Ordering::Relaxed), 1);
446        assert_eq!(inner_count.load(Ordering::Relaxed), 1);
447
448        store.name().set(Some(Name {
449            first_name: Some("Greg".into()),
450        }));
451        tick().await;
452        assert_eq!(parent_count.load(Ordering::Relaxed), 2);
453        assert_eq!(inner_count.load(Ordering::Relaxed), 2);
454
455        println!("\nUpdating first name only");
456        store
457            .name()
458            .unwrap()
459            .first_name()
460            .write()
461            .as_mut()
462            .unwrap()
463            .push_str("!!!");
464
465        tick().await;
466        assert_eq!(parent_count.load(Ordering::Relaxed), 3);
467        assert_eq!(inner_count.load(Ordering::Relaxed), 3);
468    }
469
470    #[tokio::test]
471    async fn patch() {
472        use crate::OptionStoreExt;
473
474        _ = any_spawner::Executor::init_tokio();
475
476        #[derive(Debug, Clone, Store, Patch)]
477        struct Outer {
478            inner: Option<Inner>,
479        }
480
481        #[derive(Debug, Clone, Store, Patch)]
482        struct Inner {
483            first: String,
484            second: String,
485        }
486
487        let store = Store::new(Outer {
488            inner: Some(Inner {
489                first: "A".to_owned(),
490                second: "B".to_owned(),
491            }),
492        });
493
494        let parent_count = Arc::new(AtomicUsize::new(0));
495        let inner_first_count = Arc::new(AtomicUsize::new(0));
496        let inner_second_count = Arc::new(AtomicUsize::new(0));
497
498        Effect::new_sync({
499            let parent_count = Arc::clone(&parent_count);
500            move |prev: Option<()>| {
501                if prev.is_none() {
502                    println!("parent: first run");
503                } else {
504                    println!("parent: next run");
505                }
506
507                println!("  value = {:?}", store.inner().get());
508                parent_count.fetch_add(1, Ordering::Relaxed);
509            }
510        });
511        Effect::new_sync({
512            let inner_first_count = Arc::clone(&inner_first_count);
513            move |prev: Option<()>| {
514                if prev.is_none() {
515                    println!("inner_first: first run");
516                } else {
517                    println!("inner_first: next run");
518                }
519
520                // note: we specifically want to test whether using `.patch()`
521                // correctly limits notifications on the first field when only the second
522                // field has changed
523                //
524                // `.map()` would also track the parent field (to track when it changed from Some
525                // to None), which would mean the notification numbers were always the same
526                //
527                // so here, we'll do `.map_untracked()`, but in general in a real case you'd want
528                // to use `.map()` so that if the parent switches to None you do track that
529                println!(
530                    "  value = {:?}",
531                    store.inner().map_untracked(|inner| inner.first().get())
532                );
533                inner_first_count.fetch_add(1, Ordering::Relaxed);
534            }
535        });
536        Effect::new_sync({
537            let inner_second_count = Arc::clone(&inner_second_count);
538            move |prev: Option<()>| {
539                if prev.is_none() {
540                    println!("inner_second: first run");
541                } else {
542                    println!("inner_second: next run");
543                }
544
545                println!(
546                    "  value = {:?}",
547                    store.inner().map(|inner| inner.second().get())
548                );
549                inner_second_count.fetch_add(1, Ordering::Relaxed);
550            }
551        });
552
553        tick().await;
554        assert_eq!(parent_count.load(Ordering::Relaxed), 1);
555        assert_eq!(inner_first_count.load(Ordering::Relaxed), 1);
556        assert_eq!(inner_second_count.load(Ordering::Relaxed), 1);
557
558        println!("\npatching with A/C");
559        store.patch(Outer {
560            inner: Some(Inner {
561                first: "A".to_string(),
562                second: "C".to_string(),
563            }),
564        });
565
566        tick().await;
567        assert_eq!(parent_count.load(Ordering::Relaxed), 2);
568        assert_eq!(inner_first_count.load(Ordering::Relaxed), 1);
569        assert_eq!(inner_second_count.load(Ordering::Relaxed), 2);
570
571        store.patch(Outer { inner: None });
572
573        tick().await;
574        assert_eq!(parent_count.load(Ordering::Relaxed), 3);
575        assert_eq!(inner_first_count.load(Ordering::Relaxed), 2);
576        assert_eq!(inner_second_count.load(Ordering::Relaxed), 3);
577
578        println!("\npatching with A/B");
579        store.patch(Outer {
580            inner: Some(Inner {
581                first: "A".to_string(),
582                second: "B".to_string(),
583            }),
584        });
585
586        tick().await;
587        assert_eq!(parent_count.load(Ordering::Relaxed), 4);
588        assert_eq!(inner_first_count.load(Ordering::Relaxed), 2);
589        assert_eq!(inner_second_count.load(Ordering::Relaxed), 4);
590    }
591
592    #[test]
593    fn unwrap_reads_as_none_after_option_is_cleared() {
594        use crate::OptionStoreExt;
595        use reactive_graph::owner::Owner;
596
597        #[derive(Debug, Clone, Store)]
598        struct State {
599            value: Option<i32>,
600        }
601
602        let owner = Owner::new();
603        owner.set();
604
605        let store = Store::new(State { value: Some(1) });
606
607        // Capture the unwrapped inner field while the option is `Some`.
608        let inner = OptionStoreExt::unwrap(store.value());
609        assert_eq!(inner.try_read_untracked().map(|g| *g), Some(1));
610
611        // Another writer clears the option after the inner field was captured.
612        *store.value().write() = None;
613
614        // The previously valid projection must now read as `None` instead of
615        // panicking with "called `Option::unwrap()` on a `None` value".
616        let guard = inner.try_read_untracked();
617        assert!(guard.is_none());
618        // Forcing the (lazy) projection must not panic.
619        assert!(guard.map(|g| *g).is_none());
620
621        // The writer side is fallible in the same way.
622        assert!(inner.try_write().is_none());
623    }
624}