vertigo/fetch/
lazy_cache.rs

1use std::fmt::Debug;
2use std::rc::Rc;
3
4use crate::{
5    computed::{context::Context, Value},
6    get_driver,
7    struct_mut::ValueMut,
8    transaction, Computed, DomNode, Instant, JsJsonDeserialize, Resource, ToComputed,
9};
10
11use super::request_builder::{RequestBody, RequestBuilder};
12
13type MapResponse<T> = Option<Result<T, String>>;
14
15fn get_unique_id() -> u64 {
16    use std::sync::atomic::{AtomicU64, Ordering};
17    static COUNTER: AtomicU64 = AtomicU64::new(1);
18    COUNTER.fetch_add(1, Ordering::Relaxed)
19}
20
21enum ApiResponse<T> {
22    Uninitialized,
23    Data {
24        value: Resource<Rc<T>>,
25        expiry: Option<Instant>,
26    },
27}
28
29impl<T> ApiResponse<T> {
30    pub fn new(value: Resource<Rc<T>>, expiry: Option<Instant>) -> Self {
31        Self::Data { value, expiry }
32    }
33
34    pub fn new_loading() -> Self {
35        ApiResponse::Data {
36            value: Resource::Loading,
37            expiry: None,
38        }
39    }
40
41    pub fn get_value(&self) -> Resource<Rc<T>> {
42        match self {
43            Self::Uninitialized => Resource::Loading,
44            Self::Data { value, expiry: _ } => value.clone(),
45        }
46    }
47
48    pub fn needs_update(&self) -> bool {
49        match self {
50            ApiResponse::Uninitialized => true,
51            ApiResponse::Data { value: _, expiry } => {
52                let Some(expiry) = expiry else {
53                    return false;
54                };
55
56                expiry.is_expire()
57            }
58        }
59    }
60}
61
62impl<T> Clone for ApiResponse<T> {
63    fn clone(&self) -> Self {
64        match self {
65            ApiResponse::Uninitialized => ApiResponse::Uninitialized,
66            ApiResponse::Data { value, expiry } => ApiResponse::Data {
67                value: value.clone(),
68                expiry: expiry.clone(),
69            },
70        }
71    }
72}
73
74/// A structure similar to [Value] but supports Loading/Error states and automatic refresh
75/// after defined amount of time.
76///
77/// ```rust
78/// use vertigo::{LazyCache, RequestBuilder, AutoJsJson};
79///
80/// #[derive(AutoJsJson, PartialEq, Clone)]
81/// pub struct Model {
82///     id: i32,
83///     name: String,
84/// }
85///
86/// pub struct TodoState {
87///     posts: LazyCache<Vec<Model>>,
88/// }
89///
90/// impl TodoState {
91///     pub fn new() -> Self {
92///         let posts = RequestBuilder::get("https://some.api/posts")
93///             .ttl_seconds(300)
94///             .lazy_cache(|status, body| {
95///                 if status == 200 {
96///                     Some(body.into::<Vec<Model>>())
97///                 } else {
98///                     None
99///                 }
100///             });
101///
102///         TodoState {
103///             posts
104///         }
105///     }
106/// }
107/// ```
108///
109/// See ["todo" example](../src/vertigo_demo/app/todo/state.rs.html) in vertigo-demo package for more.
110pub struct LazyCache<T: 'static> {
111    id: u64,
112    value: Value<ApiResponse<T>>,
113    queued: Rc<ValueMut<bool>>,
114    request: Rc<RequestBuilder>,
115    map_response: Rc<dyn Fn(u32, RequestBody) -> MapResponse<T>>,
116}
117
118impl<T: 'static> Debug for LazyCache<T> {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        f.debug_struct("LazyCache")
121            .field("queued", &self.queued)
122            .finish()
123    }
124}
125
126impl<T> Clone for LazyCache<T> {
127    fn clone(&self) -> Self {
128        LazyCache {
129            id: self.id,
130            value: self.value.clone(),
131            queued: self.queued.clone(),
132            request: self.request.clone(),
133            map_response: self.map_response.clone(),
134        }
135    }
136}
137
138impl<T> LazyCache<T> {
139    pub fn new(
140        request: RequestBuilder,
141        map_response: impl Fn(u32, RequestBody) -> MapResponse<T> + 'static,
142    ) -> Self {
143        Self {
144            id: get_unique_id(),
145            value: Value::new(ApiResponse::Uninitialized),
146            queued: Rc::new(ValueMut::new(false)),
147            request: Rc::new(request),
148            map_response: Rc::new(map_response),
149        }
150    }
151
152    /// Get value (update if needed)
153    pub fn get(&self, context: &Context) -> Resource<Rc<T>> {
154        let api_response = self.value.get(context);
155
156        if !self.queued.get() && api_response.needs_update() {
157            self.update(false, false);
158        }
159
160        api_response.get_value()
161    }
162
163    /// Delete value so it will refresh on next access
164    pub fn forget(&self) {
165        self.value.set(ApiResponse::Uninitialized);
166    }
167
168    /// Force refresh the value now
169    pub fn force_update(&self, with_loading: bool) {
170        self.update(with_loading, true)
171    }
172
173    /// Update the value if expired
174    pub fn update(&self, with_loading: bool, force: bool) {
175        if self.queued.get() {
176            return;
177        }
178
179        self.queued.set(true); //set lock
180        get_driver().inner.api.on_fetch_start.trigger(());
181
182        let self_clone = self.clone();
183
184        get_driver().spawn(async move {
185            if !self_clone.queued.get() {
186                log::error!("force_update_spawn: queued.get() in spawn -> expected false");
187                return;
188            }
189
190            let api_response = transaction(|context| self_clone.value.get(context));
191
192            if force || api_response.needs_update() {
193                if with_loading {
194                    self_clone.value.set(ApiResponse::new_loading());
195                }
196
197                let new_value = self_clone
198                    .request
199                    .call()
200                    .await
201                    .into(self_clone.map_response.as_ref());
202
203                let new_value = match new_value {
204                    Ok(value) => Resource::Ready(Rc::new(value)),
205                    Err(message) => Resource::Error(message),
206                };
207
208                let expiry = self_clone
209                    .request
210                    .get_ttl()
211                    .map(|ttl| get_driver().now().add_duration(ttl));
212
213                self_clone.value.set(ApiResponse::new(new_value, expiry));
214            }
215
216            self_clone.queued.set(false);
217            get_driver().inner.api.on_fetch_stop.trigger(());
218        });
219    }
220
221    pub fn to_computed(&self) -> Computed<Resource<Rc<T>>> {
222        Computed::from({
223            let state = self.clone();
224            move |context| state.get(context)
225        })
226    }
227}
228
229impl<T: Clone> ToComputed<Resource<Rc<T>>> for LazyCache<T> {
230    fn to_computed(&self) -> Computed<Resource<Rc<T>>> {
231        self.to_computed()
232    }
233}
234
235impl<T> PartialEq for LazyCache<T> {
236    fn eq(&self, other: &Self) -> bool {
237        self.id == other.id
238    }
239}
240
241impl<T: PartialEq + Clone> LazyCache<T> {
242    pub fn render(&self, render: impl Fn(Rc<T>) -> DomNode + 'static) -> DomNode {
243        self.to_computed().render_value(move |value| match value {
244            Resource::Ready(value) => render(value),
245            Resource::Loading => {
246                use crate as vertigo;
247
248                vertigo::dom! {
249                    <vertigo-suspense />
250                }
251            }
252            Resource::Error(error) => {
253                use crate as vertigo;
254
255                vertigo::dom! {
256                    <div>
257                        "error = "
258                        {error}
259                    </div>
260                }
261            }
262        })
263    }
264}
265
266impl<T: JsJsonDeserialize> LazyCache<T> {
267    /// Helper to easily create a lazy cache of `Vec<T>` deserialized from provided URL base and route
268    ///
269    /// ```rust
270    /// use vertigo::{LazyCache, AutoJsJson};
271    ///
272    /// #[derive(AutoJsJson, PartialEq, Clone)]
273    /// pub struct Model {
274    ///     id: i32,
275    ///     name: String,
276    /// }
277    ///
278    /// let posts = LazyCache::<Vec<Model>>::new_resource("https://some.api", "/posts", 60);
279    /// ```
280    pub fn new_resource(api: &str, path: &str, ttl: u64) -> Self {
281        let url = [api, path].concat();
282
283        LazyCache::new(
284            get_driver().request_get(url).ttl_seconds(ttl),
285            |status, body| {
286                if status == 200 {
287                    Some(body.into::<T>())
288                } else {
289                    None
290                }
291            },
292        )
293    }
294}