dioxus-provider 0.2.1

Data fetching and caching library for Dioxus applications with intelligent caching strategies and global providers.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
//! # Provider Hooks
//!
//! This module provides hooks for working with providers in Dioxus applications.
//! It requires `dioxus_provider::global::init_global_providers()` to be called at application startup.
//!
//! ## Example
//!
//! ```rust
//! use dioxus::prelude::*;
//! use dioxus_provider::{prelude::*, global::init_global_providers};
//!
//! #[provider]
//! async fn fetch_user(id: u32) -> Result<String, String> {
//!     Ok(format!("User {}", id))
//! }
//!
//! #[component]
//! fn App() -> Element {
//!     let user = use_provider(fetch_user(), (1,));
//!     rsx! { div { "User: {user:?}" } }
//! }
//!
//! fn main() {
//!     init_global_providers();
//!     launch(App);
//! }
//! ```

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};

// Import helper functions from internal modules
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;

/// A unified trait for defining providers - async operations that return data
///
/// This trait supports both simple providers (no parameters) and parameterized providers.
/// Use `Provider<()>` for simple providers and `Provider<ParamType>` for parameterized providers.
///
/// ## Features
///
/// - **Async Execution**: All providers are async by default
/// - **Configurable Caching**: Optional cache expiration times
/// - **Stale-While-Revalidate**: Serve stale data while revalidating in background
/// - **Auto-Refresh**: Optional automatic refresh at intervals
/// - **Auto-Dispose**: Automatic cleanup when providers are no longer used
///
/// ## Cross-Platform Compatibility
///
/// The Provider trait is designed to work across platforms using Dioxus's spawn system:
/// - Uses `dioxus::spawn` for async execution (no Send + Sync required for most types)
/// - Parameters may need Send + Sync if shared across contexts
/// - Output and Error types only need Clone since they stay within Dioxus context
///
/// ## Example
///
/// ```rust,no_run
/// use dioxus_provider::prelude::*;
/// use std::time::Duration;
///
/// #[provider(stale_time = "1m", cache_expiration = "5m")]
/// async fn data_provider() -> Result<String, String> {
///     // Fetch data from API
///     Ok("Hello, World!".to_string())
/// }
///
/// #[component]
/// fn Consumer() -> Element {
///     let data = use_provider(data_provider(), ());
///     // ...
/// }
/// ```
pub trait Provider<Param = ()>: Clone + PartialEq + 'static
where
    Param: ProviderParamBounds,
{
    /// The type of data returned on success
    type Output: ProviderOutputBounds;
    /// The type of error returned on failure
    type Error: ProviderErrorBounds;

    /// Execute the async operation
    ///
    /// This method performs the actual work of the provider, such as fetching data
    /// from an API, reading from a database, or computing a value.
    fn run(&self, param: Param) -> impl Future<Output = Result<Self::Output, Self::Error>>;

    /// Get a unique identifier for this provider instance with the given parameters
    ///
    /// This ID is used for caching and invalidation. The default implementation
    /// hashes the provider's type and parameters to generate a unique ID.
    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())
    }

    /// Get the interval duration for automatic refresh (None means no interval)
    ///
    /// When set, the provider will automatically refresh its data at the specified
    /// interval, even if no component is actively watching it.
    fn interval(&self) -> Option<Duration> {
        None
    }

    /// Get the cache expiration duration (None means no expiration)
    ///
    /// When set, cached data will be considered expired after this duration and
    /// will be removed from the cache, forcing a fresh fetch on the next access.
    fn cache_expiration(&self) -> Option<Duration> {
        None
    }

    /// Get the stale time duration for stale-while-revalidate behavior (None means no SWR)
    ///
    /// When set, data older than this duration will be considered stale and will
    /// trigger a background revalidation while still serving the stale data to the UI.
    fn stale_time(&self) -> Option<Duration> {
        None
    }
}

/// Extension trait to enable suspense support for provider signals
///
/// Allows you to call `.suspend()` on a `Signal<ProviderState<T, E>>`
/// inside a component. If the state is `Loading`, this will suspend
/// rendering and trigger Dioxus's SuspenseBoundary fallback.
///
/// Usage:
/// ```rust
/// let user = use_provider(fetch_user(), (1,)).suspend()?;
/// ```
pub trait SuspenseSignalExt<T, E> {
    /// Returns Ok(data) if ready, Err(RenderError::Suspended) if loading, or Ok(Err(error)) if error.
    fn suspend(&self) -> Result<Result<T, E>, RenderError>;
}

/// Error type for suspending rendering (compatible with Dioxus SuspenseBoundary)
#[derive(Debug, Clone, PartialEq)]
pub enum RenderError {
    Suspended(SuspendedFuture),
}

// Implement conversion so `?` works in components using Dioxus's RenderError
impl From<RenderError> for dioxus_core::RenderError {
    fn from(err: RenderError) -> Self {
        match err {
            RenderError::Suspended(fut) => dioxus_core::RenderError::Suspended(fut),
        }
    }
}

// Update SuspenseSignalExt to use ProviderState
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())),
        }
    }
}

/// Get the provider cache - requires global providers to be initialized
fn get_provider_cache() -> ProviderCache {
    get_global_cache()
        .unwrap_or_else(|_| {
            panic!("Global providers not initialized. Call dioxus_provider::init() before using providers.")
        })
        .clone()
}

/// Get the refresh registry - requires global providers to be initialized
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()
}

/// Hook to access the provider cache for manual cache management
///
/// This hook provides direct access to the global provider cache for manual
/// invalidation, clearing, and other cache operations.
///
/// ## Global Providers Required
///
/// You must call `init_global_providers()` at application startup before using any provider hooks.
///
/// ## Setup
///
/// ```rust,no_run
/// use dioxus_provider::{prelude::*, global::init_global_providers};
///
/// fn main() {
///     init_global_providers();
///     dioxus::launch(App);
/// }
///
/// #[component]
/// fn App() -> Element {
///     rsx! {
///         MyComponent {}
///     }
/// }
/// ```
///
/// ## Example
///
/// ```rust,no_run
/// use dioxus::prelude::*;
/// use dioxus_provider::prelude::*;
///
/// #[component]
/// fn MyComponent() -> Element {
///     let cache = use_provider_cache();
///
///     // Manually invalidate a specific cache entry
///     cache.invalidate("my_provider_key");
///
///     rsx! {
///         div { "Cache operations example" }
///     }
/// }
/// ```
pub fn use_provider_cache() -> ProviderCache {
    get_provider_cache()
}

/// Hook to invalidate a specific provider cache entry
///
/// Returns a function that, when called, will invalidate the cache entry for the
/// specified provider and parameters, and trigger a refresh of all components
/// using that provider.
///
/// Requires global providers to be initialized with `init_global_providers()`.
///
/// ## Example
///
/// ```rust,no_run
/// use dioxus::prelude::*;
/// use dioxus_provider::prelude::*;
///
/// #[provider]
/// async fn user_provider(id: u32) -> Result<String, String> {
///     Ok(format!("User {}", id))
/// }
///
/// #[component]
/// fn MyComponent() -> Element {
///     let invalidate_user = use_invalidate_provider(user_provider(), 1);
///
///     rsx! {
///         button {
///             onclick: move |_| invalidate_user(),
///             "Refresh User Data"
///         }
///     }
/// }
/// ```
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(&param);

    move || {
        cache.invalidate(&cache_key);
        refresh_registry.trigger_refresh(&cache_key);
    }
}

/// Hook to clear the entire provider cache
///
/// Returns a function that, when called, will clear all cached provider data
/// and trigger a refresh of all providers currently in use.
///
/// Requires global providers to be initialized with `init_global_providers()`.
///
/// ## Example
///
/// ```rust,no_run
/// use dioxus::prelude::*;
/// use dioxus_provider::prelude::*;
///
/// #[component]
/// fn MyComponent() -> Element {
///     let clear_cache = use_clear_provider_cache();
///
///     rsx! {
///         button {
///             onclick: move |_| clear_cache(),
///             "Clear All Cache"
///         }
///     }
/// }
/// ```
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();
    }
}

/// Unified trait for using providers with any parameter format
///
/// This trait provides a single, unified interface for using providers
/// regardless of their parameter format. It automatically handles:
/// - No parameters `()`
/// - Tuple parameters `(param,)`
/// - Direct parameters `param`
pub trait UseProvider<Args> {
    /// The type of data returned on success
    type Output: ProviderOutputBounds;
    /// The type of error returned on failure
    type Error: ProviderErrorBounds;

    /// Use the provider with the given arguments
    fn use_provider(self, args: Args) -> Signal<State<Self::Output, Self::Error>>;
}

/// Unified implementation for all providers using parameter normalization
///
/// This single implementation replaces all the previous repetitive implementations
/// by using the `IntoProviderParam` trait to normalize different parameter formats.
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)
    }
}

/// Core provider implementation that handles all the common logic
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(&param);
    let cache_expiration = provider.cache_expiration();

    // Setup intelligent cache management (replaces old auto-dispose system)
    setup_intelligent_cache_management(&provider, &cache_key, &cache, &refresh_registry);

    // Check cache expiration before the memo - this happens on every render
    check_and_handle_cache_expiration(cache_expiration, &cache_key, &cache, &refresh_registry);

    // SWR staleness checking - runs on every render to check for stale data
    check_and_handle_swr_core(&provider, &param, &cache_key, &cache, &refresh_registry);

    // Use memo with reactive dependencies to track changes automatically
    let _execution_memo = use_memo(use_reactive!(|(provider, param)| {
        let cache_key = provider.id(&param);

        #[cfg(feature = "tracing")]
        crate::debug_log!(
            "🔄 [USE_PROVIDER] Memo executing for key: {} with param: {:?}",
            cache_key,
            param
        );

        // Subscribe to refresh events for this cache key if we have a reactive context
        if let Some(reactive_context) = ReactiveContext::current() {
            refresh_registry.subscribe_to_refresh(&cache_key, reactive_context);
        }

        // Read the current refresh count (this makes the memo reactive to changes)
        let _current_refresh_count = refresh_registry.get_refresh_count(&cache_key);

        // Set up cache expiration monitoring task
        setup_cache_expiration_task_core(&provider, &param, &cache_key, &cache, &refresh_registry);

        // Set up interval task if provider has interval configured
        setup_interval_task_core(&provider, &param, &cache_key, &cache, &refresh_registry);

        // Set up stale check task if provider has stale time configured
        setup_stale_check_task_core(&provider, &param, &cache_key, &cache, &refresh_registry);

        // Check cache for valid data
        if let Some(cached_result) = cache.get::<Result<P::Output, P::Error>>(&cache_key) {
            // Access tracking is automatically handled by cache.get() updating last_accessed time
            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;
        }

        // Cache miss - check if this is due to invalidation and we should use SWR behavior
        let is_invalidation_refresh = refresh_registry.get_refresh_count(&cache_key) > 0;

        if is_invalidation_refresh {
            // This is an invalidation refresh - use SWR behavior to prevent jitters
            // Don't show loading state immediately, let SWR handle background revalidation
            crate::debug_log!(
                "🔄 [INVALIDATION] Cache miss due to invalidation for: {}, using SWR behavior",
                cache_key
            );

            // Set up background revalidation without showing loading state
            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
                    );
                }
            });

            // Don't set loading state - let the component handle the absence of data gracefully
            return;
        }

        // Regular cache miss - set loading and spawn async task
        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;

        // Spawn the real async task and store the handle in Loading
        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 {
                // Only update state and trigger rerender if value changed
                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
}

/// Performs SWR staleness checking and triggers background revalidation if needed
/// Unified hook for using any provider - automatically detects parameterized vs non-parameterized providers
///
/// This is the main hook for consuming providers in Dioxus components. It automatically
/// handles both simple providers (no parameters) and parameterized providers, providing
/// a consistent interface for all provider types through the `IntoProviderParam` trait.
///
/// ## Supported Parameter Formats
///
/// - **No parameters**: `use_provider(provider, ())`
/// - **Tuple parameters**: `use_provider(provider, (param,))`
/// - **Direct parameters**: `use_provider(provider, param)`
///
/// ## Features
///
/// - **Automatic Caching**: Results are cached based on provider configuration
/// - **Reactive Updates**: Components automatically re-render when data changes
/// - **Loading States**: Provides loading, success, and error states
/// - **Background Refresh**: Supports interval refresh and stale-while-revalidate
/// - **Auto-Dispose**: Automatically cleans up unused providers
/// - **Unified API**: Single function handles all parameter formats
///
/// ## Usage Examples
///
/// ```rust,no_run
/// use dioxus::prelude::*;
/// use dioxus_provider::prelude::*;
///
/// #[provider]
/// async fn fetch_user() -> Result<String, String> {
///     Ok("User data".to_string())
/// }
///
/// #[provider]
/// async fn fetch_user_by_id(user_id: u32) -> Result<String, String> {
///     Ok(format!("User {}", user_id))
/// }
///
/// #[component]
/// fn MyComponent() -> Element {
///     // All of these work seamlessly:
///     let user = use_provider(fetch_user(), ());           // No parameters
///     let user_by_id = use_provider(fetch_user_by_id(), 123);     // Direct parameter
///     let user_by_id_tuple = use_provider(fetch_user_by_id(), (123,)); // Tuple parameter
///
///     rsx! {
///         div { "Users loaded!" }
///     }
/// }
/// ```
pub fn use_provider<P, Args>(provider: P, args: Args) -> Signal<State<P::Output, P::Error>>
where
    P: UseProvider<Args>,
{
    provider.use_provider(args)
}