use dioxus::prelude::*;
use futures::channel::oneshot;
use std::{collections::HashSet, future::Future};
use crate::{
global::{get_global_cache, get_global_refresh_registry},
hooks::Provider,
types::ProviderParamBounds,
};
#[derive(Clone, PartialEq)]
pub enum MutationState<T, E> {
Idle,
Loading,
Success(T),
Error(E),
}
impl<T, E> crate::state::AsyncState for MutationState<T, E> {
type Data = T;
type Error = E;
fn is_loading(&self) -> bool {
matches!(self, MutationState::Loading)
}
fn is_success(&self) -> bool {
matches!(self, MutationState::Success(_))
}
fn is_error(&self) -> bool {
matches!(self, MutationState::Error(_))
}
fn data(&self) -> Option<&T> {
match self {
MutationState::Success(data) => Some(data),
_ => None,
}
}
fn error(&self) -> Option<&E> {
match self {
MutationState::Error(error) => Some(error),
_ => None,
}
}
}
impl<T, E> MutationState<T, E> {
pub fn is_idle(&self) -> bool {
matches!(self, MutationState::Idle)
}
pub fn is_loading(&self) -> bool {
<Self as crate::state::AsyncState>::is_loading(self)
}
pub fn is_success(&self) -> bool {
<Self as crate::state::AsyncState>::is_success(self)
}
pub fn is_error(&self) -> bool {
<Self as crate::state::AsyncState>::is_error(self)
}
pub fn data(&self) -> Option<&T> {
<Self as crate::state::AsyncState>::data(self)
}
pub fn error(&self) -> Option<&E> {
<Self as crate::state::AsyncState>::error(self)
}
}
pub struct MutationContext<'a, Data, Error> {
current: Option<&'a Result<Data, Error>>,
}
impl<'a, Data, Error> MutationContext<'a, Data, Error> {
pub fn new(current: Option<&'a Result<Data, Error>>) -> Self {
Self { current }
}
pub fn current(&self) -> Option<&Result<Data, Error>> {
self.current
}
pub fn current_success(&self) -> Option<&Data> {
match self.current? {
Ok(data) => Some(data),
Err(_) => None,
}
}
pub fn cloned_success(&self) -> Option<Data>
where
Data: Clone,
{
self.current()?.as_ref().ok().cloned()
}
pub fn map_current<F>(&self, f: F) -> Option<Data>
where
Data: Clone,
F: FnOnce(&mut Data),
{
let mut cloned = self.cloned_success()?;
f(&mut cloned);
Some(cloned)
}
pub fn map_or_else<F, D>(&self, default: D, f: F) -> Data
where
Data: Clone,
F: FnOnce(&mut Data),
D: FnOnce() -> Data,
{
self.map_current(f).unwrap_or_else(default)
}
pub fn update_in_place<F>(&self, f: F) -> Option<Data>
where
Data: Clone,
F: FnOnce(&mut Data),
{
self.map_current(f)
}
pub fn has_data(&self) -> bool {
self.current_success().is_some()
}
pub fn has_error(&self) -> bool {
matches!(self.current, Some(Err(_)))
}
}
pub trait Mutation<Input = ()>: Clone + PartialEq + 'static
where
Input: Clone + PartialEq + 'static,
{
type Output: Clone + PartialEq + Send + Sync + 'static;
type Error: Clone + PartialEq + Send + Sync + 'static;
fn mutate(&self, input: Input) -> impl Future<Output = Result<Self::Output, Self::Error>>;
fn mutate_with_current(
&self,
input: Input,
_current_data: Option<&Result<Self::Output, Self::Error>>,
) -> impl Future<Output = Result<Self::Output, Self::Error>> {
self.mutate(input)
}
fn id(&self) -> String {
std::any::type_name::<Self>().to_string()
}
fn invalidates(&self) -> Vec<String> {
Vec::new()
}
fn has_optimistic(&self) -> bool {
false
}
fn optimistic_updates(
&self,
_input: &Input,
) -> Vec<(String, Result<Self::Output, Self::Error>)> {
Vec::new()
}
fn optimistic_updates_with_current(
&self,
_input: &Input,
_current_data: Option<&Result<Self::Output, Self::Error>>,
) -> Vec<(String, Result<Self::Output, Self::Error>)> {
self.optimistic_updates(_input)
}
}
pub type MutationHookResult<M, Input, F> = (
Signal<MutationState<<M as Mutation<Input>>::Output, <M as Mutation<Input>>::Error>>,
F,
);
#[derive(Clone, Debug)]
struct MutationConfig {
optimistic: bool,
}
impl MutationConfig {
fn default() -> Self {
Self { optimistic: false }
}
fn optimistic() -> Self {
Self { optimistic: true }
}
}
fn mutation_core<M, Input>(
mutation: M,
config: MutationConfig,
) -> MutationHookResult<M, Input, impl Fn(Input) + Clone>
where
M: Mutation<Input> + Send + Sync + 'static,
Input: Clone + PartialEq + Send + Sync + 'static,
{
let state = use_signal(|| MutationState::Idle);
let cache = get_global_cache();
let refresh_registry = get_global_refresh_registry();
let mutate_fn = {
let mutation = mutation.clone();
let cache = cache
.unwrap_or_else(|_| {
panic!("Global providers not initialized. Call dioxus_provider::init() before using mutations.")
})
.clone();
let refresh_registry = refresh_registry
.unwrap_or_else(|_| {
panic!("Global providers not initialized. Call dioxus_provider::init() before using mutations.")
})
.clone();
let is_optimistic = config.optimistic;
move |input: Input| {
if is_optimistic && matches!(*state.read(), MutationState::Loading) {
crate::debug_log!(
"⏸️ [MUTATION] Skipping mutation - already in progress for: {}",
mutation.id()
);
return;
}
let mutation = mutation.clone();
let cache = cache.clone();
let refresh_registry = refresh_registry.clone();
let input = input.clone();
let mut ui_state = state;
ui_state.set(MutationState::Loading);
let cache_keys_to_check: Vec<String> = mutation.invalidates();
let mut optimistic_updates = Vec::new();
if is_optimistic {
for cache_key in &cache_keys_to_check {
let current_data = cache.get::<Result<M::Output, M::Error>>(cache_key);
let updates =
mutation.optimistic_updates_with_current(&input, current_data.as_ref());
optimistic_updates.extend(updates);
}
if optimistic_updates.is_empty() {
optimistic_updates = mutation.optimistic_updates(&input);
}
if optimistic_updates.is_empty() && !cache_keys_to_check.is_empty() {
crate::debug_log!(
"⚡ [OPTIMISTIC] No optimistic updates available, using SWR for {} cache keys",
cache_keys_to_check.len()
);
}
if !optimistic_updates.is_empty() {
crate::debug_log!(
"⚡ [OPTIMISTIC] Optimistically updating {} cache entries",
optimistic_updates.len()
);
for (cache_key, optimistic_result) in &optimistic_updates {
cache.set(cache_key.clone(), optimistic_result.clone());
refresh_registry.trigger_refresh(cache_key);
}
}
}
let optimistic_updates_for_rollback = optimistic_updates.clone();
let (result_tx, result_rx) = oneshot::channel::<Result<M::Output, M::Error>>();
spawn({
let mut state = ui_state;
async move {
if let Ok(outcome) = result_rx.await {
match outcome {
Ok(result) => state.set(MutationState::Success(result)),
Err(error) => state.set(MutationState::Error(error)),
}
}
}
});
dioxus_core::spawn_forever(async move {
#[cfg(feature = "tracing")]
let mutation_type = if is_optimistic {
"optimistic mutation"
} else {
"mutation"
};
crate::debug_log!(
"🔄 [MUTATION] Starting {}: {}",
mutation_type,
mutation.id()
);
let mutation_current_data = cache_keys_to_check
.first()
.and_then(|first_key| cache.get::<Result<M::Output, M::Error>>(first_key));
let mutation_result = mutation
.mutate_with_current(input, mutation_current_data.as_ref())
.await;
crate::debug_log!(
"📡 [MUTATION] Mutation completed for: {}, result: {}",
mutation.id(),
match &mutation_result {
Ok(_) => "Success",
Err(_) => "Error",
}
);
match &mutation_result {
Ok(result) => {
crate::debug_log!("✅ [MUTATION] Mutation succeeded: {}", mutation.id());
if is_optimistic && !optimistic_updates_for_rollback.is_empty() {
let optimistic_keys: HashSet<String> = optimistic_updates_for_rollback
.iter()
.map(|(key, _)| key.clone())
.collect();
crate::debug_log!(
"📦 [MUTATION] Updating {} optimistic cache entries with mutation result",
optimistic_keys.len()
);
for cache_key in &optimistic_keys {
cache.set(cache_key.clone(), Ok::<_, M::Error>(result.clone()));
refresh_registry.trigger_refresh(cache_key);
}
let invalidation_keys: Vec<_> = cache_keys_to_check
.iter()
.filter(|key| !optimistic_keys.contains(*key))
.cloned()
.collect();
if !invalidation_keys.is_empty() {
crate::debug_log!(
"🔄 [MUTATION] Invalidating {} cache keys: {:?}",
invalidation_keys.len(),
invalidation_keys
);
for cache_key in invalidation_keys {
cache.invalidate(&cache_key);
refresh_registry.trigger_refresh(&cache_key);
}
}
} else {
crate::debug_log!(
"🔄 [MUTATION] Invalidating {} cache keys: {:?}",
cache_keys_to_check.len(),
cache_keys_to_check
);
for cache_key in &cache_keys_to_check {
crate::debug_log!(
"🗑️ [MUTATION] Invalidating cache key: {}",
cache_key
);
cache.invalidate(cache_key);
refresh_registry.trigger_refresh(cache_key);
}
}
}
Err(_) => {
crate::debug_log!("❌ [MUTATION] Mutation failed: {}", mutation.id());
if is_optimistic && !optimistic_updates_for_rollback.is_empty() {
crate::debug_log!(
"🔄 [ROLLBACK] Rolling back {} optimistic updates",
optimistic_updates_for_rollback.len()
);
for (cache_key, _) in &optimistic_updates_for_rollback {
crate::debug_log!(
"🔄 [ROLLBACK] Rolling back optimistic update for cache key: {}",
cache_key
);
cache.invalidate(cache_key);
refresh_registry.trigger_refresh(cache_key);
}
}
}
}
if result_tx.send(mutation_result).is_err() {
crate::debug_log!(
"⚠️ [MUTATION] Result receiver dropped before completion for: {}",
mutation.id()
);
}
});
}
};
(state, mutate_fn)
}
pub fn use_mutation<M, Input>(mutation: M) -> MutationHookResult<M, Input, impl Fn(Input) + Clone>
where
M: Mutation<Input> + Send + Sync + 'static,
Input: Clone + PartialEq + Send + Sync + 'static,
{
let config = if mutation.has_optimistic() {
MutationConfig::optimistic()
} else {
MutationConfig::default()
};
mutation_core(mutation, config)
}
pub fn use_optimistic_mutation<M, Input>(
mutation: M,
) -> MutationHookResult<M, Input, impl Fn(Input) + Clone>
where
M: Mutation<Input> + Send + Sync + 'static,
Input: Clone + PartialEq + Send + Sync + 'static,
{
use_mutation(mutation)
}
pub fn provider_cache_key<P, Param>(provider: P, param: Param) -> String
where
P: Provider<Param>,
Param: ProviderParamBounds,
{
provider.id(¶m)
}
pub fn provider_cache_key_simple<P>(provider: P) -> String
where
P: Provider<()>,
{
provider.id(&())
}