dioxus_provider/
mutation.rs

1//! # Mutation System for Dioxus Provider
2//!
3//! This module provides mutation capabilities for modifying data and keeping caches in sync.
4//! It integrates seamlessly with the provider system for automatic cache invalidation and
5//! optimistic updates.
6//!
7//! ## Features
8//!
9//! - **Mutation Providers**: Define mutations with the `#[mutation]` macro
10//! - **Optimistic Updates**: Immediate UI updates with rollback on failure
11//! - **Automatic Cache Invalidation**: Invalidate related providers automatically
12//! - **Mutation State**: Track loading, success, and error states
13//! - **Rollback Support**: Automatic rollback of optimistic updates on failure
14
15use dioxus::prelude::*;
16use futures::channel::oneshot;
17use std::{collections::HashSet, future::Future};
18
19use crate::{
20    global::{get_global_cache, get_global_refresh_registry},
21    hooks::Provider,
22    types::ProviderParamBounds,
23};
24
25/// Represents the state of a mutation operation
26#[derive(Clone, PartialEq)]
27pub enum MutationState<T, E> {
28    /// The mutation is idle (not running)
29    Idle,
30    /// The mutation is currently loading
31    Loading,
32    /// The mutation completed successfully
33    Success(T),
34    /// The mutation failed with an error
35    Error(E),
36}
37
38impl<T, E> crate::state::AsyncState for MutationState<T, E> {
39    type Data = T;
40    type Error = E;
41
42    fn is_loading(&self) -> bool {
43        matches!(self, MutationState::Loading)
44    }
45
46    fn is_success(&self) -> bool {
47        matches!(self, MutationState::Success(_))
48    }
49
50    fn is_error(&self) -> bool {
51        matches!(self, MutationState::Error(_))
52    }
53
54    fn data(&self) -> Option<&T> {
55        match self {
56            MutationState::Success(data) => Some(data),
57            _ => None,
58        }
59    }
60
61    fn error(&self) -> Option<&E> {
62        match self {
63            MutationState::Error(error) => Some(error),
64            _ => None,
65        }
66    }
67}
68
69impl<T, E> MutationState<T, E> {
70    /// Returns true if the mutation is idle
71    pub fn is_idle(&self) -> bool {
72        matches!(self, MutationState::Idle)
73    }
74
75    /// Returns true if the mutation is currently loading
76    pub fn is_loading(&self) -> bool {
77        <Self as crate::state::AsyncState>::is_loading(self)
78    }
79
80    /// Returns true if the mutation completed successfully
81    pub fn is_success(&self) -> bool {
82        <Self as crate::state::AsyncState>::is_success(self)
83    }
84
85    /// Returns true if the mutation failed
86    pub fn is_error(&self) -> bool {
87        <Self as crate::state::AsyncState>::is_error(self)
88    }
89
90    /// Returns the success data if available
91    pub fn data(&self) -> Option<&T> {
92        <Self as crate::state::AsyncState>::data(self)
93    }
94
95    /// Returns the error if available
96    pub fn error(&self) -> Option<&E> {
97        <Self as crate::state::AsyncState>::error(self)
98    }
99}
100
101/// Provides access to cached mutation data for implementations generated by the macro.
102pub struct MutationContext<'a, Data, Error> {
103    current: Option<&'a Result<Data, Error>>,
104}
105
106impl<'a, Data, Error> MutationContext<'a, Data, Error> {
107    /// Create a new mutation context from the current cached result.
108    pub fn new(current: Option<&'a Result<Data, Error>>) -> Self {
109        Self { current }
110    }
111
112    /// Returns the current cached result, including the error if the cache failed previously.
113    pub fn current(&self) -> Option<&Result<Data, Error>> {
114        self.current
115    }
116
117    /// Returns a reference to the current successful cached data, if available.
118    pub fn current_success(&self) -> Option<&Data> {
119        match self.current? {
120            Ok(data) => Some(data),
121            Err(_) => None,
122        }
123    }
124
125    /// Clones the current successful cached data, if available.
126    pub fn cloned_success(&self) -> Option<Data>
127    where
128        Data: Clone,
129    {
130        self.current()?.as_ref().ok().cloned()
131    }
132
133    /// Applies a transformation to the cloned cached data and returns the updated value.
134    pub fn map_current<F>(&self, f: F) -> Option<Data>
135    where
136        Data: Clone,
137        F: FnOnce(&mut Data),
138    {
139        let mut cloned = self.cloned_success()?;
140        f(&mut cloned);
141        Some(cloned)
142    }
143
144    /// Applies a transformation to the cloned cached data, or returns a default value if no data is available.
145    ///
146    /// This is useful when you need to ensure a value is always returned, even if the cache is empty.
147    ///
148    /// ## Example
149    ///
150    /// ```rust,no_run
151    /// use dioxus_provider::prelude::*;
152    ///
153    /// #[mutation(invalidates = [fetch_items])]
154    /// async fn add_item(
155    ///     item: String,
156    ///     ctx: MutationContext<Vec<String>, String>,
157    /// ) -> Result<Vec<String>, String> {
158    ///     Ok(ctx.map_or_else(
159    ///         || vec![item.clone()],  // default if no cached data
160    ///         |items| {
161    ///             items.push(item.clone());
162    ///         }
163    ///     ))
164    /// }
165    /// ```
166    pub fn map_or_else<F, D>(&self, default: D, f: F) -> Data
167    where
168        Data: Clone,
169        F: FnOnce(&mut Data),
170        D: FnOnce() -> Data,
171    {
172        self.map_current(f).unwrap_or_else(default)
173    }
174
175    /// Updates the cached data in place, returning the modified data or None if no data exists.
176    ///
177    /// This is similar to `map_current` but provides a more explicit name for mutations
178    /// that modify existing data.
179    ///
180    /// ## Example
181    ///
182    /// ```rust,no_run
183    /// use dioxus_provider::prelude::*;
184    ///
185    /// #[mutation(invalidates = [fetch_counter])]
186    /// async fn increment_counter(
187    ///     ctx: MutationContext<i32, String>,
188    /// ) -> Result<i32, String> {
189    ///     ctx.update_in_place(|count| *count += 1)
190    ///         .ok_or_else(|| "No counter data available".to_string())
191    /// }
192    /// ```
193    pub fn update_in_place<F>(&self, f: F) -> Option<Data>
194    where
195        Data: Clone,
196        F: FnOnce(&mut Data),
197    {
198        self.map_current(f)
199    }
200
201    /// Returns true if there is currently cached data available.
202    pub fn has_data(&self) -> bool {
203        self.current_success().is_some()
204    }
205
206    /// Returns true if the current cache contains an error.
207    pub fn has_error(&self) -> bool {
208        matches!(self.current, Some(Err(_)))
209    }
210}
211
212/// Trait for defining mutations - operations that modify data
213///
214/// Mutations are similar to providers but are designed for data modification operations.
215/// They typically involve server requests to create, update, or delete data.
216///
217/// ## Usage
218/// Prefer using the `#[mutation]` macro to define mutations. Manual trait implementations are for advanced use only.
219///
220/// ## Example
221///
222/// ```rust,no_run
223/// use dioxus_provider::prelude::*;
224///
225/// #[mutation(invalidates = [fetch_user, fetch_users])]
226/// async fn update_user(user_id: u32, data: UserData) -> Result<User, String> {
227///     // Make API call to update user
228///     api_client.update_user(user_id, data).await
229/// }
230/// ```
231pub trait Mutation<Input = ()>: Clone + PartialEq + 'static
232where
233    Input: Clone + PartialEq + 'static,
234{
235    /// The type of data returned on successful mutation
236    type Output: Clone + PartialEq + Send + Sync + 'static;
237    /// The type of error returned on mutation failure
238    type Error: Clone + PartialEq + Send + Sync + 'static;
239
240    /// Execute the mutation with the given input
241    fn mutate(&self, input: Input) -> impl Future<Output = Result<Self::Output, Self::Error>>;
242
243    /// Execute the mutation with access to current cached data
244    /// This allows mutations to work with existing state instead of redefining data
245    /// If not implemented, falls back to the simple mutate method
246    fn mutate_with_current(
247        &self,
248        input: Input,
249        _current_data: Option<&Result<Self::Output, Self::Error>>,
250    ) -> impl Future<Output = Result<Self::Output, Self::Error>> {
251        // Default implementation falls back to simple mutate
252        self.mutate(input)
253    }
254
255    /// Get a unique identifier for this mutation type
256    fn id(&self) -> String {
257        std::any::type_name::<Self>().to_string()
258    }
259
260    /// Get list of provider cache keys that should be invalidated after successful mutation
261    /// Override this to specify which providers should be refreshed after mutation
262    fn invalidates(&self) -> Vec<String> {
263        Vec::new()
264    }
265
266    /// Returns true if this mutation has optimistic updates configured
267    /// Used by `use_mutation` to automatically detect and enable optimistic behavior
268    fn has_optimistic(&self) -> bool {
269        false
270    }
271
272    /// Provide optimistic cache updates for immediate UI feedback
273    /// Returns a list of (cache_key, optimistic_result) pairs to update the cache with
274    /// This allows the UI to update immediately with the expected result
275    /// The Result should contain the optimistic success value
276    fn optimistic_updates(
277        &self,
278        _input: &Input,
279    ) -> Vec<(String, Result<Self::Output, Self::Error>)> {
280        Vec::new()
281    }
282
283    /// Compute optimistic updates with access to current cached data
284    /// This is more efficient as it allows mutations to work with existing data
285    /// instead of duplicating data structures
286    ///
287    /// ## Return Value Usage
288    /// The mutation's return value is used to update the cache directly on success,
289    /// avoiding unnecessary provider refetches when using optimistic updates.
290    /// When optimistic updates are defined, the actual server response replaces
291    /// the optimistic value in the cache, keeping the UI in sync with the backend.
292    fn optimistic_updates_with_current(
293        &self,
294        _input: &Input,
295        _current_data: Option<&Result<Self::Output, Self::Error>>,
296    ) -> Vec<(String, Result<Self::Output, Self::Error>)> {
297        // Fallback to the simple method if not overridden
298        self.optimistic_updates(_input)
299    }
300}
301
302/// Type alias for the return type of mutation hooks
303pub type MutationHookResult<M, Input, F> = (
304    Signal<MutationState<<M as Mutation<Input>>::Output, <M as Mutation<Input>>::Error>>,
305    F,
306);
307
308/// Configuration for mutation behavior
309#[derive(Clone, Debug)]
310struct MutationConfig {
311    /// Whether to apply optimistic updates
312    optimistic: bool,
313}
314
315impl MutationConfig {
316    /// Create a default mutation configuration (no optimistic updates)
317    fn default() -> Self {
318        Self { optimistic: false }
319    }
320
321    /// Create a mutation configuration with optimistic updates enabled
322    fn optimistic() -> Self {
323        Self { optimistic: true }
324    }
325}
326
327/// Core mutation logic shared between use_mutation and use_optimistic_mutation
328fn mutation_core<M, Input>(
329    mutation: M,
330    config: MutationConfig,
331) -> MutationHookResult<M, Input, impl Fn(Input) + Clone>
332where
333    M: Mutation<Input> + Send + Sync + 'static,
334    Input: Clone + PartialEq + Send + Sync + 'static,
335{
336    let state = use_signal(|| MutationState::Idle);
337    let cache = get_global_cache();
338    let refresh_registry = get_global_refresh_registry();
339
340    let mutate_fn = {
341        let mutation = mutation.clone();
342        let cache = cache
343            .unwrap_or_else(|_| {
344                panic!("Global providers not initialized. Call dioxus_provider::init() before using mutations.")
345            })
346            .clone();
347        let refresh_registry = refresh_registry
348            .unwrap_or_else(|_| {
349                panic!("Global providers not initialized. Call dioxus_provider::init() before using mutations.")
350            })
351            .clone();
352        let is_optimistic = config.optimistic;
353
354        move |input: Input| {
355            // Prevent concurrent optimistic mutations
356            if is_optimistic && matches!(*state.read(), MutationState::Loading) {
357                crate::debug_log!(
358                    "⏸️ [MUTATION] Skipping mutation - already in progress for: {}",
359                    mutation.id()
360                );
361                return;
362            }
363
364            let mutation = mutation.clone();
365            let cache = cache.clone();
366            let refresh_registry = refresh_registry.clone();
367            let input = input.clone();
368            let mut ui_state = state;
369
370            // Set loading state
371            ui_state.set(MutationState::Loading);
372
373            // Collect optimistic updates if enabled
374            let cache_keys_to_check: Vec<String> = mutation.invalidates();
375            let mut optimistic_updates = Vec::new();
376
377            if is_optimistic {
378                // First, try to get optimistic updates from providers that have cached data
379                for cache_key in &cache_keys_to_check {
380                    let current_data = cache.get::<Result<M::Output, M::Error>>(cache_key);
381                    let updates =
382                        mutation.optimistic_updates_with_current(&input, current_data.as_ref());
383                    optimistic_updates.extend(updates);
384                }
385
386                // If we don't have any optimistic updates yet, try the fallback method
387                if optimistic_updates.is_empty() {
388                    optimistic_updates = mutation.optimistic_updates(&input);
389                }
390
391                // If we still don't have optimistic updates, but we have cache keys to invalidate,
392                // we need to handle the case where some providers don't have cached data
393                if optimistic_updates.is_empty() && !cache_keys_to_check.is_empty() {
394                    // For providers without cached data, we'll use SWR behavior:
395                    // - Don't show loading state immediately
396                    // - Let them fetch in the background while showing stale data (if any)
397                    // - This prevents jitters for providers that don't have optimistic updates
398                    crate::debug_log!(
399                        "⚑ [OPTIMISTIC] No optimistic updates available, using SWR for {} cache keys",
400                        cache_keys_to_check.len()
401                    );
402                }
403
404                if !optimistic_updates.is_empty() {
405                    crate::debug_log!(
406                        "⚑ [OPTIMISTIC] Optimistically updating {} cache entries",
407                        optimistic_updates.len()
408                    );
409                    for (cache_key, optimistic_result) in &optimistic_updates {
410                        cache.set(cache_key.clone(), optimistic_result.clone());
411                        refresh_registry.trigger_refresh(cache_key);
412                    }
413                }
414            }
415
416            let optimistic_updates_for_rollback = optimistic_updates.clone();
417            let (result_tx, result_rx) = oneshot::channel::<Result<M::Output, M::Error>>();
418
419            spawn({
420                let mut state = ui_state;
421                async move {
422                    if let Ok(outcome) = result_rx.await {
423                        match outcome {
424                            Ok(result) => state.set(MutationState::Success(result)),
425                            Err(error) => state.set(MutationState::Error(error)),
426                        }
427                    }
428                }
429            });
430
431            dioxus_core::spawn_forever(async move {
432                #[cfg(feature = "tracing")]
433                let mutation_type = if is_optimistic {
434                    "optimistic mutation"
435                } else {
436                    "mutation"
437                };
438                crate::debug_log!(
439                    "πŸ”„ [MUTATION] Starting {}: {}",
440                    mutation_type,
441                    mutation.id()
442                );
443
444                // Get current data for the mutation
445                let mutation_current_data = cache_keys_to_check
446                    .first()
447                    .and_then(|first_key| cache.get::<Result<M::Output, M::Error>>(first_key));
448
449                let mutation_result = mutation
450                    .mutate_with_current(input, mutation_current_data.as_ref())
451                    .await;
452
453                crate::debug_log!(
454                    "πŸ“‘ [MUTATION] Mutation completed for: {}, result: {}",
455                    mutation.id(),
456                    match &mutation_result {
457                        Ok(_) => "Success",
458                        Err(_) => "Error",
459                    }
460                );
461
462                match &mutation_result {
463                    Ok(result) => {
464                        crate::debug_log!("βœ… [MUTATION] Mutation succeeded: {}", mutation.id());
465
466                        if is_optimistic && !optimistic_updates_for_rollback.is_empty() {
467                            // Update optimistic caches with real result
468                            let optimistic_keys: HashSet<String> = optimistic_updates_for_rollback
469                                .iter()
470                                .map(|(key, _)| key.clone())
471                                .collect();
472
473                            crate::debug_log!(
474                                "πŸ“¦ [MUTATION] Updating {} optimistic cache entries with mutation result",
475                                optimistic_keys.len()
476                            );
477
478                            for cache_key in &optimistic_keys {
479                                cache.set(cache_key.clone(), Ok::<_, M::Error>(result.clone()));
480                                refresh_registry.trigger_refresh(cache_key);
481                            }
482
483                            let invalidation_keys: Vec<_> = cache_keys_to_check
484                                .iter()
485                                .filter(|key| !optimistic_keys.contains(*key))
486                                .cloned()
487                                .collect();
488
489                            if !invalidation_keys.is_empty() {
490                                crate::debug_log!(
491                                    "πŸ”„ [MUTATION] Invalidating {} cache keys: {:?}",
492                                    invalidation_keys.len(),
493                                    invalidation_keys
494                                );
495
496                                for cache_key in invalidation_keys {
497                                    cache.invalidate(&cache_key);
498                                    refresh_registry.trigger_refresh(&cache_key);
499                                }
500                            }
501                        } else {
502                            // Standard cache invalidation
503                            crate::debug_log!(
504                                "πŸ”„ [MUTATION] Invalidating {} cache keys: {:?}",
505                                cache_keys_to_check.len(),
506                                cache_keys_to_check
507                            );
508
509                            for cache_key in &cache_keys_to_check {
510                                crate::debug_log!(
511                                    "πŸ—‘οΈ [MUTATION] Invalidating cache key: {}",
512                                    cache_key
513                                );
514                                cache.invalidate(cache_key);
515                                refresh_registry.trigger_refresh(cache_key);
516                            }
517                        }
518                    }
519                    Err(_) => {
520                        crate::debug_log!("❌ [MUTATION] Mutation failed: {}", mutation.id());
521
522                        if is_optimistic && !optimistic_updates_for_rollback.is_empty() {
523                            crate::debug_log!(
524                                "πŸ”„ [ROLLBACK] Rolling back {} optimistic updates",
525                                optimistic_updates_for_rollback.len()
526                            );
527
528                            for (cache_key, _) in &optimistic_updates_for_rollback {
529                                crate::debug_log!(
530                                    "πŸ”„ [ROLLBACK] Rolling back optimistic update for cache key: {}",
531                                    cache_key
532                                );
533                                cache.invalidate(cache_key);
534                                refresh_registry.trigger_refresh(cache_key);
535                            }
536                        }
537                    }
538                }
539
540                if result_tx.send(mutation_result).is_err() {
541                    crate::debug_log!(
542                        "⚠️ [MUTATION] Result receiver dropped before completion for: {}",
543                        mutation.id()
544                    );
545                }
546            });
547        }
548    };
549
550    (state, mutate_fn)
551}
552
553/// Hook to create a mutation that can be triggered manually
554///
555/// This hook automatically detects whether optimistic updates are configured
556/// in the `#[mutation]` macro and enables them accordingly. You no longer need
557/// to choose between `use_mutation` and `use_optimistic_mutation` - this hook
558/// handles both cases automatically.
559///
560/// Returns a tuple containing:
561/// 1. A signal with the current mutation state
562/// 2. A function to trigger the mutation
563///
564/// ## Example
565///
566/// ```rust,no_run
567/// use dioxus::prelude::*;
568/// use dioxus_provider::prelude::*;
569///
570/// #[component]
571/// fn UpdateUserForm(user_id: u32) -> Element {
572///     let (mutation_state, mutate) = use_mutation(update_user());
573///     
574///     let handle_submit = move |data: UserData| {
575///         mutate(user_id, data);
576///     };
577///     
578///     rsx! {
579///         form {
580///             button {
581///                 disabled: mutation_state.read().is_loading(),
582///                 onclick: move |_| handle_submit(get_form_data()),
583///                 "Update User"
584///             }
585///             match &*mutation_state.read() {
586///                 MutationState::Loading => rsx! { div { "Updating..." } },
587///                 MutationState::Success(_) => rsx! { div { "Updated successfully!" } },
588///                 MutationState::Error(err) => rsx! { div { "Error: {err}" } },
589///                 MutationState::Idle => rsx! { div {} },
590///             }
591///         }
592///     }
593/// }
594/// ```
595pub fn use_mutation<M, Input>(mutation: M) -> MutationHookResult<M, Input, impl Fn(Input) + Clone>
596where
597    M: Mutation<Input> + Send + Sync + 'static,
598    Input: Clone + PartialEq + Send + Sync + 'static,
599{
600    let config = if mutation.has_optimistic() {
601        MutationConfig::optimistic()
602    } else {
603        MutationConfig::default()
604    };
605    mutation_core(mutation, config)
606}
607
608/// Hook to create a mutation with optimistic updates
609///
610/// **Note:** This hook is now an alias for `use_mutation`, which automatically detects
611/// whether optimistic updates are configured. You can use `use_mutation` directly instead.
612/// This function is maintained for backward compatibility.
613///
614/// The optimistic behavior is automatically detected from the `#[mutation]` macro's
615/// `optimistic` parameter. If an optimistic closure is provided in the macro,
616/// the mutation will automatically use optimistic updates.
617///
618/// ## Example
619///
620/// ```rust,no_run
621/// use dioxus::prelude::*;
622/// use dioxus_provider::prelude::*;
623///
624/// #[component]
625/// fn TodoItem(todo_id: u32) -> Element {
626///     // Both of these are equivalent:
627///     let (mutation_state, mutate) = use_mutation(toggle_todo());
628///     // let (mutation_state, mutate) = use_optimistic_mutation(toggle_todo());
629///     
630///     rsx! {
631///         div {
632///             button {
633///                 onclick: move |_| mutate(todo_id),
634///                 "Toggle Todo"
635///             }
636///             match &*mutation_state.read() {
637///                 MutationState::Loading => rsx! { span { "Saving..." } },
638///                 MutationState::Error(err) => rsx! { span { "Error: {err}" } },
639///                 _ => rsx! { span {} },
640///             }
641///         }
642///     }
643/// }
644/// ```
645pub fn use_optimistic_mutation<M, Input>(
646    mutation: M,
647) -> MutationHookResult<M, Input, impl Fn(Input) + Clone>
648where
649    M: Mutation<Input> + Send + Sync + 'static,
650    Input: Clone + PartialEq + Send + Sync + 'static,
651{
652    // Simply delegate to use_mutation, which auto-detects optimistic updates
653    use_mutation(mutation)
654}
655
656/// Helper function to create cache keys for providers with parameters
657pub fn provider_cache_key<P, Param>(provider: P, param: Param) -> String
658where
659    P: Provider<Param>,
660    Param: ProviderParamBounds,
661{
662    provider.id(&param)
663}
664
665/// Helper function to create cache keys for providers without parameters
666pub fn provider_cache_key_simple<P>(provider: P) -> String
667where
668    P: Provider<()>,
669{
670    provider.id(&())
671}