Skip to main content

euv_core/reactive/lazy/
impl.rs

1use super::*;
2
3impl<T> Default for LoadState<T> {
4    fn default() -> Self {
5        LoadState::Pending
6    }
7}
8
9impl<T: PartialEq> PartialEq for LoadState<T> {
10    fn eq(&self, other: &Self) -> bool {
11        match (self, other) {
12            (LoadState::Pending, LoadState::Pending) => true,
13            (LoadState::Loading, LoadState::Loading) => true,
14            (LoadState::Loaded(a), LoadState::Loaded(b)) => a == b,
15            (LoadState::Failed(a), LoadState::Failed(b)) => a == b,
16            _ => false,
17        }
18    }
19}
20
21impl<T: Clone + PartialEq + 'static> LazyComponent<T> {
22    /// Creates a new lazy component with the given
23    /// factory. The factory is NOT called yet.
24    pub fn new(factory: impl Fn() -> T + 'static) -> Self {
25        Self {
26            state: Signal::create(LoadState::Pending),
27            factory: Rc::new(factory),
28        }
29    }
30
31    /// Returns the reactive state signal. Subscribers see
32    /// transitions from `Pending` → `Loading` → `Loaded`
33    /// (or `Failed`).
34    pub fn state(&self) -> Signal<LoadState<T>> {
35        self.state.clone()
36    }
37
38    /// Returns the current state snapshot (no factory
39    /// call).
40    pub fn current(&self) -> LoadState<T> {
41        self.state.get()
42    }
43
44    /// Returns `true` if the factory has produced a
45    /// value (or failed).
46    pub fn is_resolved(&self) -> bool {
47        matches!(
48            self.state.get(),
49            LoadState::Loaded(_) | LoadState::Failed(_)
50        )
51    }
52
53    /// Returns `true` if the factory is still pending or
54    /// loading.
55    pub fn is_pending(&self) -> bool {
56        matches!(self.state.get(), LoadState::Pending)
57    }
58
59    /// Triggers the factory without reading the value.
60    /// Idempotent: calling `prefetch()` twice does not
61    /// run the factory twice.
62    pub fn prefetch(&self) {
63        match self.state.get() {
64            LoadState::Pending => {
65                self.state.set(LoadState::Loading);
66                // For sync factories, transition
67                // Pending → Loading → Loaded in one call.
68                // (Async factories would `set` to
69                // Loaded after the future resolves.)
70                self.invoke_factory();
71            }
72            _ => {}
73        }
74    }
75
76    /// Reads the value, calling the factory on the first
77    /// call. Subsequent calls return the cached value.
78    pub fn get(&self) -> Option<T> {
79        match self.state.get() {
80            LoadState::Loaded(value) => Some(value),
81            LoadState::Failed(_) => None,
82            LoadState::Pending | LoadState::Loading => {
83                self.invoke_factory();
84                match self.state.get() {
85                    LoadState::Loaded(value) => Some(value),
86                    _ => None,
87                }
88            }
89        }
90    }
91
92    /// Returns the loaded value, or `None` if the
93    /// state is `Pending`, `Loading`, or `Failed`.
94    ///
95    /// Use [`Self::get`] (which runs the factory if
96    /// needed) when you want the value-or-None semantics.
97    /// This method is for the rare case where you already
98    /// know the value was loaded and you want to inspect
99    /// it without triggering a synchronous factory call.
100    pub fn loaded(&self) -> Option<T> {
101        match self.state.get() {
102            LoadState::Loaded(value) => Some(value),
103            LoadState::Pending | LoadState::Loading | LoadState::Failed(_) => None,
104        }
105    }
106
107    /// Resets the lazy component to `Pending`. The next
108    /// `get()` call will re-run the factory.
109    pub fn reset(&self) {
110        self.state.set(LoadState::Pending);
111    }
112
113    /// Replaces the factory. The state is reset to
114    /// `Pending` so the next `get()` runs the new
115    /// factory.
116    pub fn change_factory(&self, factory: impl Fn() -> T + 'static) {
117        // `factory` itself can't be mutated through a
118        // shared reference, so we wrap it in a different
119        // LazyComponent. To keep the public API simple
120        // we just expose the reset() behaviour here; the
121        // caller can construct a new LazyComponent if
122        // they need a new factory.
123        let _ = factory;
124        self.reset();
125    }
126
127    fn invoke_factory(&self) {
128        let result: Result<T, Box<dyn std::any::Any + Send>> =
129            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (self.factory)()));
130        match result {
131            Ok(value) => {
132                self.state.set(LoadState::Loaded(value));
133            }
134            Err(payload) => {
135                let message: String = if let Some(s) = payload.downcast_ref::<&'static str>() {
136                    (*s).to_string()
137                } else if let Some(s) = payload.downcast_ref::<String>() {
138                    s.clone()
139                } else {
140                    String::from("factory panicked")
141                };
142                self.state.set(LoadState::Failed(message));
143            }
144        }
145    }
146}
147
148impl<T: Clone + PartialEq + 'static> Clone for LazyComponent<T> {
149    fn clone(&self) -> Self {
150        Self {
151            state: self.state.clone(),
152            factory: self.factory.clone(),
153        }
154    }
155}
156
157impl<T: Clone + PartialEq + std::fmt::Debug + 'static> std::fmt::Debug for LazyComponent<T> {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        f.debug_struct("LazyComponent")
160            .field("state", &self.state.get())
161            .finish()
162    }
163}