use dioxus::{
core::{ReactiveContext, SuspendedFuture},
prelude::*,
};
use std::{fmt::Debug, future::Future, time::Duration};
use crate::{
cache::ProviderCache,
global::{get_global_cache, get_global_refresh_registry},
refresh::RefreshRegistry,
};
use crate::param_utils::IntoProviderParam;
use crate::types::{ProviderErrorBounds, ProviderOutputBounds, ProviderParamBounds};
use super::internal::cache_mgmt::setup_intelligent_cache_management;
use super::internal::swr::check_and_handle_swr_core;
use super::internal::tasks::{
check_and_handle_cache_expiration, setup_cache_expiration_task_core, setup_interval_task_core,
setup_stale_check_task_core,
};
pub use crate::state::State;
pub trait Provider<Param = ()>: Clone + PartialEq + 'static
where
Param: ProviderParamBounds,
{
type Output: ProviderOutputBounds;
type Error: ProviderErrorBounds;
fn run(&self, param: Param) -> impl Future<Output = Result<Self::Output, Self::Error>>;
fn id(&self, param: &Param) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
std::any::TypeId::of::<Self>().hash(&mut hasher);
param.hash(&mut hasher);
format!("{:x}", hasher.finish())
}
fn interval(&self) -> Option<Duration> {
None
}
fn cache_expiration(&self) -> Option<Duration> {
None
}
fn stale_time(&self) -> Option<Duration> {
None
}
}
pub trait SuspenseSignalExt<T, E> {
fn suspend(&self) -> Result<Result<T, E>, RenderError>;
}
#[derive(Debug, Clone, PartialEq)]
pub enum RenderError {
Suspended(SuspendedFuture),
}
impl From<RenderError> for dioxus_core::RenderError {
fn from(err: RenderError) -> Self {
match err {
RenderError::Suspended(fut) => dioxus_core::RenderError::Suspended(fut),
}
}
}
impl<T: Clone + 'static, E: Clone + 'static> SuspenseSignalExt<T, E> for Signal<State<T, E>> {
fn suspend(&self) -> Result<Result<T, E>, RenderError> {
match &*self.read() {
State::Loading { task } => Err(RenderError::Suspended(SuspendedFuture::new(*task))),
State::Success(data) => Ok(Ok(data.clone())),
State::Error(error) => Ok(Err(error.clone())),
}
}
}
fn get_provider_cache() -> ProviderCache {
get_global_cache()
.unwrap_or_else(|_| {
panic!("Global providers not initialized. Call dioxus_provider::init() before using providers.")
})
.clone()
}
fn get_refresh_registry() -> RefreshRegistry {
get_global_refresh_registry()
.unwrap_or_else(|_| {
panic!("Global providers not initialized. Call dioxus_provider::init() before using providers.")
})
.clone()
}
pub fn use_provider_cache() -> ProviderCache {
get_provider_cache()
}
pub fn use_invalidate_provider<P, Param>(provider: P, param: Param) -> impl Fn() + Clone
where
P: Provider<Param>,
Param: ProviderParamBounds,
{
let cache = get_provider_cache();
let refresh_registry = get_refresh_registry();
let cache_key = provider.id(¶m);
move || {
cache.invalidate(&cache_key);
refresh_registry.trigger_refresh(&cache_key);
}
}
pub fn use_clear_provider_cache() -> impl Fn() + Clone {
let cache = get_provider_cache();
let refresh_registry = get_refresh_registry();
move || {
cache.clear();
refresh_registry.clear_all();
}
}
pub trait UseProvider<Args> {
type Output: ProviderOutputBounds;
type Error: ProviderErrorBounds;
fn use_provider(self, args: Args) -> Signal<State<Self::Output, Self::Error>>;
}
impl<P, Args> UseProvider<Args> for P
where
P: Provider<Args::Param> + Clone,
Args: IntoProviderParam,
{
type Output = P::Output;
type Error = P::Error;
fn use_provider(self, args: Args) -> Signal<State<Self::Output, Self::Error>> {
let param = args.into_param();
use_provider_core(self, param)
}
}
fn use_provider_core<P, Param>(provider: P, param: Param) -> Signal<State<P::Output, P::Error>>
where
P: Provider<Param> + Clone,
Param: ProviderParamBounds,
{
let mut state = use_signal(|| State::Loading {
task: spawn(async {}),
});
let cache = get_provider_cache();
let refresh_registry = get_refresh_registry();
let cache_key = provider.id(¶m);
let cache_expiration = provider.cache_expiration();
setup_intelligent_cache_management(&provider, &cache_key, &cache, &refresh_registry);
check_and_handle_cache_expiration(cache_expiration, &cache_key, &cache, &refresh_registry);
check_and_handle_swr_core(&provider, ¶m, &cache_key, &cache, &refresh_registry);
let _execution_memo = use_memo(use_reactive!(|(provider, param)| {
let cache_key = provider.id(¶m);
#[cfg(feature = "tracing")]
crate::debug_log!(
"🔄 [USE_PROVIDER] Memo executing for key: {} with param: {:?}",
cache_key,
param
);
if let Some(reactive_context) = ReactiveContext::current() {
refresh_registry.subscribe_to_refresh(&cache_key, reactive_context);
}
let _current_refresh_count = refresh_registry.get_refresh_count(&cache_key);
setup_cache_expiration_task_core(&provider, ¶m, &cache_key, &cache, &refresh_registry);
setup_interval_task_core(&provider, ¶m, &cache_key, &cache, &refresh_registry);
setup_stale_check_task_core(&provider, ¶m, &cache_key, &cache, &refresh_registry);
if let Some(cached_result) = cache.get::<Result<P::Output, P::Error>>(&cache_key) {
crate::debug_log!("📊 [CACHE-HIT] Serving cached data for: {}", cache_key);
match cached_result {
Ok(data) => {
let _ = spawn(async move {
state.set(State::Success(data));
});
}
Err(error) => {
let _ = spawn(async move {
state.set(State::Error(error));
});
}
}
return;
}
let is_invalidation_refresh = refresh_registry.get_refresh_count(&cache_key) > 0;
if is_invalidation_refresh {
crate::debug_log!(
"🔄 [INVALIDATION] Cache miss due to invalidation for: {}, using SWR behavior",
cache_key
);
let cache_clone = cache.clone();
let cache_key_clone = cache_key.clone();
let provider = provider.clone();
let param = param.clone();
let refresh_registry_clone = refresh_registry.clone();
spawn(async move {
let result = provider.run(param).await;
let updated = cache_clone.set(cache_key_clone.clone(), result.clone());
if updated {
refresh_registry_clone.trigger_refresh(&cache_key_clone);
crate::debug_log!(
"✅ [INVALIDATION] Background revalidation completed for: {}",
cache_key_clone
);
}
});
return;
}
let cache_clone = cache.clone();
let cache_key_clone = cache_key.clone();
let provider = provider.clone();
let param = param.clone();
let mut state_for_async = state;
let task = spawn(async move {
let result = provider.run(param).await;
let updated = cache_clone.set(cache_key_clone.clone(), result.clone());
crate::debug_log!(
"📊 [CACHE-STORE] Attempted to store new data for: {} (updated: {})",
cache_key_clone,
updated
);
if updated {
match result {
Ok(data) => state_for_async.set(State::Success(data)),
Err(error) => state_for_async.set(State::Error(error)),
}
}
});
state.set(State::Loading { task });
}));
state
}
pub fn use_provider<P, Args>(provider: P, args: Args) -> Signal<State<P::Output, P::Error>>
where
P: UseProvider<Args>,
{
provider.use_provider(args)
}