dioxus_query/
mutation.rs

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