Skip to main content

freya_query/
mutation.rs

1use core::fmt;
2use std::{
3    cell::{
4        Ref,
5        RefCell,
6    },
7    collections::HashMap,
8    future::Future,
9    hash::Hash,
10    mem,
11    rc::Rc,
12    time::{
13        Duration,
14        Instant,
15    },
16};
17
18use async_io::Timer;
19use freya_core::{
20    integration::FxHashSet,
21    lifecycle::context::{
22        consume_context,
23        provide_context_for_scope_id,
24        try_consume_context,
25    },
26    prelude::*,
27    scope_id::ScopeId,
28};
29
30pub trait MutationCapability
31where
32    Self: 'static + Clone + PartialEq + Hash + Eq,
33{
34    type Ok;
35    type Err;
36    type Keys: Hash + PartialEq + Clone;
37
38    /// Mutation logic.
39    fn run(&self, keys: &Self::Keys) -> impl Future<Output = Result<Self::Ok, Self::Err>>;
40
41    /// Implement a custom logic to check if this mutation should be invalidated or not given a [MutationCapability::Keys].
42    fn matches(&self, _keys: &Self::Keys) -> bool {
43        true
44    }
45
46    /// Runs after [MutationCapability::run].
47    /// You may use this method to invalidate [crate::query::Query]s.
48    fn on_settled(
49        &self,
50        _keys: &Self::Keys,
51        _result: &Result<Self::Ok, Self::Err>,
52    ) -> impl Future<Output = ()> {
53        async {}
54    }
55}
56
57pub enum MutationStateData<Q: MutationCapability> {
58    /// Has not loaded yet.
59    Pending,
60    /// Is loading and may not have a previous settled value.
61    Loading { res: Option<Result<Q::Ok, Q::Err>> },
62    /// Is not loading and has a settled value.
63    Settled {
64        res: Result<Q::Ok, Q::Err>,
65        settlement_instant: Instant,
66    },
67}
68
69impl<Q> fmt::Debug for MutationStateData<Q>
70where
71    Q: MutationCapability,
72    Q::Ok: fmt::Debug,
73    Q::Err: fmt::Debug,
74{
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        match self {
77            Self::Pending => f.write_str("Pending"),
78            Self::Loading { res } => write!(f, "Loading {{ {res:?} }}"),
79            Self::Settled { res, .. } => write!(f, "Settled {{ {res:?} }}"),
80        }
81    }
82}
83
84impl<Q: MutationCapability> MutationStateData<Q> {
85    /// Check if the state is [MutationStateData::Settled] and [Result::Ok].
86    pub fn is_ok(&self) -> bool {
87        matches!(self, MutationStateData::Settled { res: Ok(_), .. })
88    }
89
90    /// Check if the state is [MutationStateData::Settled] and [Result::Err].
91    pub fn is_err(&self) -> bool {
92        matches!(self, MutationStateData::Settled { res: Err(_), .. })
93    }
94
95    /// Check if the state is [MutationStateData::Loading].
96    pub fn is_loading(&self) -> bool {
97        matches!(self, MutationStateData::Loading { .. })
98    }
99
100    /// Check if the state is [MutationStateData::Pending].
101    pub fn is_pending(&self) -> bool {
102        matches!(self, MutationStateData::Pending)
103    }
104
105    /// Get the value as an [Option].
106    pub fn ok(&self) -> Option<&Q::Ok> {
107        match self {
108            Self::Settled { res: Ok(res), .. } => Some(res),
109            Self::Loading { res: Some(Ok(res)) } => Some(res),
110            _ => None,
111        }
112    }
113
114    /// Get the value as an [Result] if possible, otherwise it will panic.
115    pub fn unwrap(&self) -> &Result<Q::Ok, Q::Err> {
116        match self {
117            Self::Loading { res: Some(v) } => v,
118            Self::Settled { res, .. } => res,
119            _ => unreachable!(),
120        }
121    }
122
123    fn into_loading(self) -> MutationStateData<Q> {
124        match self {
125            MutationStateData::Pending => MutationStateData::Loading { res: None },
126            MutationStateData::Loading { res } => MutationStateData::Loading { res },
127            MutationStateData::Settled { res, .. } => MutationStateData::Loading { res: Some(res) },
128        }
129    }
130}
131pub struct MutationsStorage<Q: MutationCapability> {
132    storage: State<HashMap<Mutation<Q>, MutationData<Q>>>,
133}
134
135impl<Q: MutationCapability> Copy for MutationsStorage<Q> {}
136
137impl<Q: MutationCapability> Clone for MutationsStorage<Q> {
138    fn clone(&self) -> Self {
139        *self
140    }
141}
142
143pub struct MutationData<Q: MutationCapability> {
144    state: Rc<RefCell<MutationStateData<Q>>>,
145    reactive_contexts: Rc<RefCell<FxHashSet<ReactiveContext>>>,
146
147    clean_task: Rc<RefCell<Option<TaskHandle>>>,
148}
149
150impl<Q: MutationCapability> Clone for MutationData<Q> {
151    fn clone(&self) -> Self {
152        Self {
153            state: self.state.clone(),
154            reactive_contexts: self.reactive_contexts.clone(),
155            clean_task: self.clean_task.clone(),
156        }
157    }
158}
159
160impl<Q: MutationCapability> MutationsStorage<Q> {
161    fn new_in_root() -> Self {
162        Self {
163            storage: State::create_global(HashMap::default()),
164        }
165    }
166
167    fn insert_or_get_mutation(&mut self, mutation: Mutation<Q>) -> MutationData<Q> {
168        let mut storage = self.storage.write_unchecked();
169
170        let mutation_data = storage.entry(mutation).or_insert_with(|| MutationData {
171            state: Rc::new(RefCell::new(MutationStateData::Pending)),
172            reactive_contexts: Rc::new(RefCell::new(FxHashSet::default())),
173            clean_task: Rc::default(),
174        });
175
176        // Cancel clean task
177        if let Some(clean_task) = mutation_data.clean_task.take() {
178            clean_task.cancel();
179        }
180
181        mutation_data.clone()
182    }
183
184    fn update_tasks(&mut self, mutation: Mutation<Q>) {
185        let storage_clone = self.storage;
186        let mut storage = self.storage.write_unchecked();
187
188        let mutation_data = storage.get_mut(&mutation).unwrap();
189
190        // Spawn clean up task if there no more reactive contexts
191        if mutation_data.reactive_contexts.borrow().is_empty() {
192            *mutation_data.clean_task.borrow_mut() = Some(spawn_forever(async move {
193                // Wait as long as the stale time is configured
194                Timer::after(mutation.clean_time).await;
195
196                // Finally clear the mutation
197                let mut storage = storage_clone.write_unchecked();
198                storage.remove(&mutation);
199            }));
200        }
201    }
202
203    async fn run(mutation: &Mutation<Q>, data: &MutationData<Q>, keys: Q::Keys) {
204        // Set to Loading
205        let res =
206            mem::replace(&mut *data.state.borrow_mut(), MutationStateData::Pending).into_loading();
207        *data.state.borrow_mut() = res;
208        for reactive_context in data.reactive_contexts.borrow().iter() {
209            reactive_context.notify();
210        }
211
212        // Run
213        let res = mutation.mutation.run(&keys).await;
214
215        // Set to Settled
216        mutation.mutation.on_settled(&keys, &res).await;
217        *data.state.borrow_mut() = MutationStateData::Settled {
218            res,
219            settlement_instant: Instant::now(),
220        };
221        for reactive_context in data.reactive_contexts.borrow().iter() {
222            reactive_context.notify();
223        }
224    }
225}
226
227#[derive(PartialEq, Clone)]
228pub struct Mutation<Q: MutationCapability> {
229    mutation: Q,
230
231    clean_time: Duration,
232}
233
234impl<Q: MutationCapability> Eq for Mutation<Q> {}
235impl<Q: MutationCapability> Hash for Mutation<Q> {
236    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
237        self.mutation.hash(state);
238    }
239}
240
241impl<Q: MutationCapability> Mutation<Q> {
242    pub fn new(mutation: Q) -> Self {
243        Self {
244            mutation,
245            clean_time: Duration::ZERO,
246        }
247    }
248
249    /// For how long the data is kept cached after there are no more mutation subscribers.
250    ///
251    /// Defaults to [Duration::ZERO], meaning it clears automatically.
252    pub fn clean_time(self, clean_time: Duration) -> Self {
253        Self { clean_time, ..self }
254    }
255}
256
257pub struct MutationReader<Q: MutationCapability> {
258    state: Rc<RefCell<MutationStateData<Q>>>,
259}
260
261impl<Q: MutationCapability> MutationReader<Q> {
262    pub fn state(&'_ self) -> Ref<'_, MutationStateData<Q>> {
263        self.state.borrow()
264    }
265}
266
267pub struct UseMutation<Q: MutationCapability> {
268    mutation: State<Mutation<Q>>,
269}
270
271impl<Q: MutationCapability> Clone for UseMutation<Q> {
272    fn clone(&self) -> Self {
273        *self
274    }
275}
276
277impl<Q: MutationCapability> Copy for UseMutation<Q> {}
278
279impl<Q: MutationCapability> UseMutation<Q> {
280    /// Read the [Mutation].
281    ///
282    /// This **will** automatically subscribe.
283    /// If you want a **subscribing** method have a look at [UseMutation::peek].
284    pub fn read(&self) -> MutationReader<Q> {
285        let storage = consume_context::<MutationsStorage<Q>>();
286        let map = storage.storage.peek();
287        let mutation_data = map.get(&self.mutation.peek()).cloned().unwrap();
288
289        // Subscribe if possible
290        if let Some(mut reactive_context) = ReactiveContext::try_current() {
291            reactive_context.subscribe(&mutation_data.reactive_contexts);
292        }
293
294        MutationReader {
295            state: mutation_data.state,
296        }
297    }
298
299    /// Read the [Mutation].
300    ///
301    /// This **will not** automatically subscribe.
302    /// If you want a **subscribing** method have a look at [UseMutation::read].
303    pub fn peek(&self) -> MutationReader<Q> {
304        let storage = consume_context::<MutationsStorage<Q>>();
305        let map = storage.storage.peek();
306        let mutation_data = map.get(&self.mutation.peek()).cloned().unwrap();
307
308        MutationReader {
309            state: mutation_data.state,
310        }
311    }
312
313    /// Run this mutation await its result.
314    ///
315    /// For a `sync` version use [UseMutation::mutate].
316    pub async fn mutate_async(&self, keys: Q::Keys) -> MutationReader<Q> {
317        let storage = consume_context::<MutationsStorage<Q>>();
318
319        let mutation = self.mutation.peek().clone();
320        let map = storage.storage.peek();
321        let mutation_data = map.get(&mutation).cloned().unwrap();
322
323        // Run the mutation
324        MutationsStorage::run(&mutation, &mutation_data, keys).await;
325
326        MutationReader {
327            state: mutation_data.state,
328        }
329    }
330
331    // Run this mutation and await its result.
332    ///
333    /// For an `async` version use [UseMutation::mutate_async].
334    pub fn mutate(&self, keys: Q::Keys) {
335        let storage = consume_context::<MutationsStorage<Q>>();
336
337        let mutation = self.mutation.peek().clone();
338        let map = storage.storage.peek();
339        let mutation_data = map.get(&mutation).cloned().unwrap();
340
341        // Run the mutation
342        spawn(async move { MutationsStorage::run(&mutation, &mutation_data, keys).await });
343    }
344}
345
346/// Mutations are used to update data asynchronously of an e.g external resources such as HTTP APIs.
347///
348/// ### Clean time
349/// This is how long will the mutation result be kept cached after there are no more subscribers of that mutation.
350///
351/// See [Mutation::clean_time].
352pub fn use_mutation<Q: MutationCapability>(mutation: Mutation<Q>) -> UseMutation<Q> {
353    let mut storage = match try_consume_context::<MutationsStorage<Q>>() {
354        Some(storage) => storage,
355        None => {
356            provide_context_for_scope_id(MutationsStorage::<Q>::new_in_root(), Some(ScopeId::ROOT));
357            try_consume_context::<MutationsStorage<Q>>().unwrap()
358        }
359    };
360
361    let mut make_mutation = |mutation: &Mutation<Q>, mut prev_mutation: Option<Mutation<Q>>| {
362        let _data = storage.insert_or_get_mutation(mutation.clone());
363
364        // Update the mutation tasks if there has been a change in the mutation
365        if let Some(prev_mutation) = prev_mutation.take() {
366            storage.update_tasks(prev_mutation);
367        }
368    };
369
370    let mut current_mutation = use_hook(|| {
371        make_mutation(&mutation, None);
372        State::create(mutation.clone())
373    });
374
375    if *current_mutation.read() != mutation {
376        let prev = mem::replace(&mut *current_mutation.write(), mutation.clone());
377        make_mutation(&mutation, Some(prev));
378    }
379
380    // Update the mutation tasks when the scope is dropped
381    use_drop({
382        move || {
383            storage.update_tasks(current_mutation.peek().clone());
384        }
385    });
386
387    UseMutation {
388        mutation: current_mutation,
389    }
390}