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