vertigo/fetch/
lazy_cache.rs1use 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
74pub 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 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 pub fn forget(&self) {
165 self.value.set(ApiResponse::Uninitialized);
166 }
167
168 pub fn force_update(&self, with_loading: bool) {
170 self.update(with_loading, true)
171 }
172
173 pub fn update(&self, with_loading: bool, force: bool) {
175 if self.queued.get() {
176 return;
177 }
178
179 self.queued.set(true); 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 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}