dioxus_provider/
refresh.rs

1//! # Refresh Registry
2//!
3//! This module provides the refresh registry that manages reactive updates and interval tasks
4//! for providers. It handles triggering re-execution of providers when their dependencies change
5//! and manages automatic refresh intervals for live data providers.
6//!
7//! ## Key Features
8//!
9//! - **Refresh Tracking**: Maintains counters for provider refresh events
10//! - **Reactive Context Management**: Subscribes and notifies reactive contexts when data changes
11//! - **Interval Tasks**: Manages background tasks for auto-refreshing providers
12//! - **Revalidation Control**: Prevents duplicate revalidations and manages ongoing operations
13//!
14//! ## Cross-Platform Compatibility
15//!
16//! This module uses cross-platform abstractions:
17//! - `dioxus::spawn` for background tasks (works on both web and desktop)
18//! - `wasmtimer` for web timing and `tokio` for desktop timing
19//! - Automatic task cleanup when components unmount
20
21use dioxus::{core::ReactiveContext, prelude::*};
22use std::{
23    collections::{HashMap, HashSet},
24    sync::{Arc, Mutex, atomic::AtomicBool},
25    time::Duration,
26};
27
28#[cfg(not(target_family = "wasm"))]
29use tokio::time;
30#[cfg(target_family = "wasm")]
31use wasmtimer::tokio as time;
32
33/// Type alias for reactive context storage
34type ReactiveContextSet = Arc<Mutex<HashSet<ReactiveContext>>>;
35type ReactiveContextRegistry = Arc<Mutex<HashMap<String, ReactiveContextSet>>>;
36
37/// Task type for different periodic operations
38#[derive(Debug, Clone, PartialEq)]
39pub enum TaskType {
40    /// Interval refresh task that re-executes providers at regular intervals
41    IntervalRefresh,
42    /// Stale checking task that monitors for stale data and triggers revalidation
43    StaleCheck,
44    /// Cache cleanup task that removes unused entries and enforces size limits
45    CacheCleanup,
46    /// Cache expiration task that monitors and removes expired entries
47    CacheExpiration,
48}
49
50/// Registry for periodic tasks (intervals and stale checks)
51/// Stores task type, duration, and cancellation flag
52type PeriodicTaskRegistry = Arc<Mutex<HashMap<String, (TaskType, Duration, Arc<AtomicBool>)>>>;
53
54/// Global registry for refresh signals that can trigger provider re-execution
55///
56/// The `RefreshRegistry` manages the reactive update system for providers. It tracks
57/// which reactive contexts are subscribed to which providers, maintains refresh counters,
58/// and manages periodic tasks for both auto-refreshing and stale-checking.
59///
60/// ## Thread Safety
61///
62/// All internal state is protected by mutexes to ensure thread-safe access across
63/// different contexts and background tasks.
64#[derive(Clone, Default)]
65pub struct RefreshRegistry {
66    /// Counters for tracking how many times each provider has been refreshed
67    refresh_counters: Arc<Mutex<HashMap<String, u64>>>,
68    /// Registry of reactive contexts subscribed to each provider key
69    reactive_contexts: ReactiveContextRegistry,
70    /// Registry of periodic tasks (both interval refresh and stale checking)
71    periodic_tasks: PeriodicTaskRegistry,
72    /// Set of provider keys that are currently being revalidated
73    ongoing_revalidations: Arc<Mutex<HashSet<String>>>,
74}
75
76impl RefreshRegistry {
77    /// Create a new refresh registry
78    pub fn new() -> Self {
79        Self::default()
80    }
81
82    /// Get the current refresh count for a provider key
83    ///
84    /// Returns the number of times the provider has been refreshed, or 0 if not found.
85    pub fn get_refresh_count(&self, key: &str) -> u64 {
86        if let Ok(counters) = self.refresh_counters.lock() {
87            *counters.get(key).unwrap_or(&0)
88        } else {
89            0
90        }
91    }
92
93    /// Subscribe a reactive context to refresh events for a provider key
94    ///
95    /// When the provider is refreshed, the reactive context will be marked as dirty,
96    /// causing any components using it to re-render.
97    pub fn subscribe_to_refresh(&self, key: &str, reactive_context: ReactiveContext) {
98        if let Ok(mut contexts) = self.reactive_contexts.lock() {
99            let key_contexts = contexts
100                .entry(key.to_string())
101                .or_insert_with(|| Arc::new(Mutex::new(HashSet::new())));
102            if let Ok(mut context_set) = key_contexts.lock() {
103                context_set.insert(reactive_context);
104            }
105        }
106    }
107
108    /// Trigger a refresh for a provider key
109    ///
110    /// This increments the refresh counter and marks all subscribed reactive contexts
111    /// as dirty, causing components to re-render and providers to re-execute.
112    pub fn trigger_refresh(&self, key: &str) {
113        // Increment the counter
114        if let Ok(mut counters) = self.refresh_counters.lock() {
115            let counter = counters.entry(key.to_string()).or_insert(0);
116            *counter += 1;
117        }
118
119        // Mark all reactive contexts as dirty
120        if let Ok(contexts) = self.reactive_contexts.lock() {
121            if let Some(key_contexts) = contexts.get(key) {
122                if let Ok(context_set) = key_contexts.lock() {
123                    for reactive_context in context_set.iter() {
124                        reactive_context.mark_dirty();
125                    }
126                }
127            }
128        }
129    }
130
131    /// Clear all cached data and trigger refresh for all providers
132    ///
133    /// This is useful for global cache invalidation scenarios.
134    pub fn clear_all(&self) {
135        if let Ok(counters) = self.refresh_counters.lock() {
136            let keys: Vec<String> = counters.keys().cloned().collect();
137            drop(counters);
138
139            for key in keys {
140                self.trigger_refresh(&key);
141            }
142        }
143    }
144
145    /// Start a periodic task for automatic provider operations
146    ///
147    /// Creates a background task that will call the provided function at regular
148    /// intervals. Supports both interval refresh and stale checking operations.
149    /// If an existing task exists with a longer interval, it will be replaced.
150    /// Tasks with shorter intervals are preserved to avoid unnecessary re-creation.
151    ///
152    /// ## Cross-Platform Implementation
153    ///
154    /// Uses `dioxus::spawn` to create tasks that work on both web and desktop platforms.
155    /// Tasks can be cancelled explicitly using stop_* methods, which set a cancellation flag.
156    pub fn start_periodic_task<F>(
157        &self,
158        key: &str,
159        task_type: TaskType,
160        interval: Duration,
161        task_fn: F,
162    ) where
163        F: Fn() + 'static,
164    {
165        if let Ok(mut tasks) = self.periodic_tasks.lock() {
166            let task_key = format!("{key}:{task_type:?}");
167
168            // For certain task types, don't create multiple tasks for the same provider
169            if (task_type == TaskType::StaleCheck || task_type == TaskType::CacheExpiration)
170                && tasks
171                    .iter()
172                    .any(|(k, (t, _, _))| k.starts_with(&format!("{key}:")) && *t == task_type)
173            {
174                return;
175            }
176
177            // Cancel existing task if it exists and the new interval is shorter (for interval tasks)
178            let should_create_new_task = match tasks.get(&task_key) {
179                None => true,
180                Some((_, current_interval, cancel_flag)) => {
181                    if task_type == TaskType::IntervalRefresh && interval < *current_interval {
182                        // Signal existing task to stop
183                        cancel_flag.store(true, std::sync::atomic::Ordering::SeqCst);
184                        tasks.remove(&task_key);
185                        true
186                    } else {
187                        false // Don't replace stale check or cache expiration tasks
188                    }
189                }
190            };
191
192            if should_create_new_task {
193                // Adjust interval for different task types
194                let actual_interval = match task_type {
195                    TaskType::StaleCheck => Duration::max(
196                        Duration::min(interval / 4, Duration::from_secs(30)),
197                        Duration::from_secs(1),
198                    ),
199                    TaskType::CacheExpiration => Duration::max(
200                        Duration::min(interval / 4, Duration::from_secs(30)),
201                        Duration::from_secs(1),
202                    ),
203                    _ => interval,
204                };
205
206                // Create cancellation flag for this task
207                let cancel_flag = Arc::new(AtomicBool::new(false));
208                let cancel_flag_clone = cancel_flag.clone();
209                let task_fn = Arc::new(task_fn);
210
211                spawn(async move {
212                    loop {
213                        // Check if task should be cancelled before sleeping
214                        if cancel_flag_clone.load(std::sync::atomic::Ordering::SeqCst) {
215                            break;
216                        }
217
218                        time::sleep(actual_interval).await;
219
220                        // Check if task should be cancelled before running
221                        if cancel_flag_clone.load(std::sync::atomic::Ordering::SeqCst) {
222                            break;
223                        }
224
225                        task_fn();
226                    }
227                });
228
229                tasks.insert(task_key, (task_type, interval, cancel_flag));
230            }
231        }
232    }
233
234    /// Start an interval task for automatic provider refresh
235    ///
236    /// This is a convenience method for starting interval refresh tasks.
237    pub fn start_interval_task<F>(&self, key: &str, interval: Duration, refresh_fn: F)
238    where
239        F: Fn() + 'static,
240    {
241        self.start_periodic_task(key, TaskType::IntervalRefresh, interval, refresh_fn);
242    }
243
244    /// Start a stale check task for SWR behavior
245    ///
246    /// This is a convenience method for starting stale checking tasks.
247    pub fn start_stale_check_task<F>(&self, key: &str, stale_time: Duration, stale_check_fn: F)
248    where
249        F: Fn() + 'static,
250    {
251        self.start_periodic_task(key, TaskType::StaleCheck, stale_time, stale_check_fn);
252    }
253
254    /// Stop a periodic task
255    ///
256    /// Signals the task to stop by setting its cancellation flag and removes it from the registry.
257    /// The task will stop after its current iteration completes.
258    pub fn stop_periodic_task(&self, key: &str, task_type: TaskType) {
259        if let Ok(mut tasks) = self.periodic_tasks.lock() {
260            let task_key = format!("{key}:{task_type:?}");
261            if let Some((_, _, cancel_flag)) = tasks.remove(&task_key) {
262                // Signal the task to stop
263                cancel_flag.store(true, std::sync::atomic::Ordering::SeqCst);
264            }
265        }
266    }
267
268    /// Stop an interval task
269    ///
270    /// This is a convenience method for stopping interval refresh tasks.
271    pub fn stop_interval_task(&self, key: &str) {
272        self.stop_periodic_task(key, TaskType::IntervalRefresh);
273    }
274
275    /// Stop a stale check task
276    ///
277    /// This is a convenience method for stopping stale checking tasks.
278    pub fn stop_stale_check_task(&self, key: &str) {
279        self.stop_periodic_task(key, TaskType::StaleCheck);
280    }
281
282    /// Check if a revalidation is currently in progress for a provider key
283    ///
284    /// This prevents duplicate revalidations from being started simultaneously.
285    pub fn is_revalidation_in_progress(&self, key: &str) -> bool {
286        if let Ok(revalidations) = self.ongoing_revalidations.lock() {
287            revalidations.contains(key)
288        } else {
289            false
290        }
291    }
292
293    /// Start a revalidation for a provider key
294    ///
295    /// Returns true if the revalidation was started, false if one was already in progress.
296    /// This prevents duplicate revalidations from running simultaneously.
297    pub fn start_revalidation(&self, key: &str) -> bool {
298        if let Ok(mut revalidations) = self.ongoing_revalidations.lock() {
299            if revalidations.contains(key) {
300                false
301            } else {
302                revalidations.insert(key.to_string());
303                true
304            }
305        } else {
306            false
307        }
308    }
309
310    /// Complete a revalidation for a provider key
311    ///
312    /// This should be called when a revalidation finishes, regardless of success or failure.
313    pub fn complete_revalidation(&self, key: &str) {
314        if let Ok(mut revalidations) = self.ongoing_revalidations.lock() {
315            revalidations.remove(key);
316        }
317    }
318
319    /// Get statistics about the refresh registry
320    pub fn stats(&self) -> RefreshRegistryStats {
321        let refresh_count = if let Ok(counters) = self.refresh_counters.lock() {
322            counters.len()
323        } else {
324            0
325        };
326
327        let context_count = if let Ok(contexts) = self.reactive_contexts.lock() {
328            contexts.len()
329        } else {
330            0
331        };
332
333        let task_count = if let Ok(tasks) = self.periodic_tasks.lock() {
334            tasks.len()
335        } else {
336            0
337        };
338
339        let revalidation_count = if let Ok(revalidations) = self.ongoing_revalidations.lock() {
340            revalidations.len()
341        } else {
342            0
343        };
344
345        RefreshRegistryStats {
346            refresh_count,
347            context_count,
348            task_count,
349            revalidation_count,
350        }
351    }
352
353    /// Clean up unused subscriptions and tasks
354    pub fn cleanup(&self) -> RefreshCleanupStats {
355        let mut stats = RefreshCleanupStats::default();
356
357        // Clean up unused reactive contexts
358        if let Ok(mut contexts) = self.reactive_contexts.lock() {
359            let initial_context_count = contexts.len();
360            contexts.retain(|_, context_set| {
361                if let Ok(set) = context_set.lock() {
362                    !set.is_empty()
363                } else {
364                    false
365                }
366            });
367            stats.contexts_removed = initial_context_count - contexts.len();
368        }
369
370        // Clean up completed revalidations (should be empty, but just in case)
371        if let Ok(mut revalidations) = self.ongoing_revalidations.lock() {
372            stats.revalidations_cleared = revalidations.len();
373            revalidations.clear();
374        }
375
376        stats
377    }
378}
379
380/// Statistics for the refresh registry
381#[derive(Debug, Clone, Default)]
382pub struct RefreshRegistryStats {
383    pub refresh_count: usize,
384    pub context_count: usize,
385    pub task_count: usize,
386    pub revalidation_count: usize,
387}
388
389/// Statistics for refresh registry cleanup operations
390#[derive(Debug, Clone, Default)]
391pub struct RefreshCleanupStats {
392    pub contexts_removed: usize,
393    pub revalidations_cleared: usize,
394}