dioxus_query/
mutation.rs

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