dioxus_query/
mutation.rs

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