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}