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}