reratui_hooks/future/mod.rs
1use parking_lot::{Mutex, RwLock};
2use std::fmt;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicUsize, Ordering};
5use tokio::task::JoinHandle;
6
7#[cfg(test)]
8mod tests;
9
10use crate::effect::EffectDependencies;
11use crate::hook_context::with_hook_context;
12use crate::panic_handler::spawn_catch_panic;
13
14/// Default error type for futures - provides good ergonomics for most use cases
15pub type DefaultError = Box<dyn std::error::Error + Send + Sync>;
16
17/// Type alias for FutureHandle with default error type
18pub type DefaultFutureHandle<T> = FutureHandle<T, DefaultError>;
19
20/// Progress callback type for reporting future progress
21///
22/// This callback receives a progress value between 0.0 and 1.0
23pub type ProgressCallback = Arc<dyn Fn(f32) + Send + Sync>;
24
25/// Maximum number of concurrent futures allowed per component
26/// This prevents resource exhaustion attacks
27const MAX_CONCURRENT_FUTURES_PER_COMPONENT: usize = 50;
28
29/// Global counter for tracking total active futures across all components
30/// This provides system-wide protection against resource exhaustion
31static GLOBAL_ACTIVE_FUTURES: AtomicUsize = AtomicUsize::new(0);
32
33/// Maximum total concurrent futures across the entire application
34const MAX_GLOBAL_CONCURRENT_FUTURES: usize = 1000;
35
36/// Represents the current state of a future operation
37///
38/// This enum provides a comprehensive view of the future's lifecycle,
39/// similar to Promise states in JavaScript or Result types in Rust.
40#[derive(Debug, Default, Clone, PartialEq)]
41pub enum FutureState<T, E = Box<dyn std::error::Error + Send + Sync>> {
42 /// The future has not been started yet (lazy/manual trigger)
43 ///
44 /// This state is useful for futures that need explicit triggering
45 /// or lazy initialization. Different from Pending which means "in progress".
46 #[default]
47 Idle,
48 /// The future is currently pending (actively running)
49 Pending,
50 /// The future has resolved successfully with a value
51 Resolved(T),
52 /// The future has failed with an error
53 Error(E),
54}
55
56impl<T, E> FutureState<T, E> {
57 /// Returns true if the future has not been started yet
58 pub fn is_idle(&self) -> bool {
59 matches!(self, FutureState::Idle)
60 }
61
62 /// Returns true if the future is currently pending (actively running)
63 pub fn is_pending(&self) -> bool {
64 matches!(self, FutureState::Pending)
65 }
66
67 /// Returns true if the future has resolved successfully
68 pub fn is_resolved(&self) -> bool {
69 matches!(self, FutureState::Resolved(_))
70 }
71
72 /// Returns true if the future has failed with an error
73 pub fn is_error(&self) -> bool {
74 matches!(self, FutureState::Error(_))
75 }
76
77 /// Returns the resolved value if available, otherwise None
78 pub fn value(&self) -> Option<&T> {
79 match self {
80 FutureState::Resolved(v) => Some(v),
81 _ => None,
82 }
83 }
84
85 /// Returns the error if available, otherwise None
86 pub fn error(&self) -> Option<&E> {
87 match self {
88 FutureState::Error(error) => Some(error),
89 _ => None,
90 }
91 }
92
93 /// Maps the resolved value to a new type using the provided function
94 pub fn map<U, F>(self, f: F) -> FutureState<U, E>
95 where
96 F: FnOnce(T) -> U,
97 {
98 match self {
99 FutureState::Idle => FutureState::Idle,
100 FutureState::Pending => FutureState::Pending,
101 FutureState::Resolved(value) => FutureState::Resolved(f(value)),
102 FutureState::Error(error) => FutureState::Error(error),
103 }
104 }
105
106 /// Maps the error to a new type using the provided function
107 pub fn map_err<F, G>(self, f: F) -> FutureState<T, G>
108 where
109 F: FnOnce(E) -> G,
110 {
111 match self {
112 FutureState::Idle => FutureState::Idle,
113 FutureState::Pending => FutureState::Pending,
114 FutureState::Resolved(value) => FutureState::Resolved(value),
115 FutureState::Error(error) => FutureState::Error(f(error)),
116 }
117 }
118}
119
120/// Enhanced API: Convert Result directly to FutureState
121///
122/// This provides better ergonomics when working with Results,
123/// allowing direct conversion without manual pattern matching.
124impl<T, E> From<Result<T, E>> for FutureState<T, E> {
125 fn from(result: Result<T, E>) -> Self {
126 match result {
127 Ok(value) => FutureState::Resolved(value),
128 Err(error) => FutureState::Error(error),
129 }
130 }
131}
132
133/// A handle to a future operation that provides access to its current state
134///
135/// This handle allows components to read the current state of an async operation
136/// and provides utility methods for working with the future's result.
137#[derive(Debug, Default)]
138pub struct FutureHandle<T, E = Box<dyn std::error::Error + Send + Sync>> {
139 /// Reference to the shared future state container
140 /// Using RwLock for better read performance since state is read frequently
141 state: Arc<RwLock<FutureState<T, E>>>,
142 /// Handle to the running task (for cancellation)
143 task_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
144}
145
146impl<T, E> FutureHandle<T, E>
147where
148 T: Clone,
149 E: Clone,
150{
151 /// Create a new future handle with initial idle state
152 fn new() -> Self {
153 Self {
154 state: Arc::new(RwLock::new(FutureState::Idle)),
155 task_handle: Arc::new(Mutex::new(None)),
156 }
157 }
158
159 /// Get the current state of the future
160 pub fn state(&self) -> FutureState<T, E> {
161 self.state.read().clone()
162 }
163
164 /// Returns true if the future has not been started yet
165 ///
166 /// This method is optimized to avoid cloning the entire state.
167 /// Uses read lock for better performance in concurrent scenarios.
168 pub fn is_idle(&self) -> bool {
169 matches!(&*self.state.read(), FutureState::Idle)
170 }
171
172 /// Returns true if the future is currently pending (actively running)
173 ///
174 /// This method is optimized to avoid cloning the entire state.
175 /// Uses read lock for better performance in concurrent scenarios.
176 pub fn is_pending(&self) -> bool {
177 matches!(&*self.state.read(), FutureState::Pending)
178 }
179
180 /// Returns true if the future has resolved successfully
181 ///
182 /// This method is optimized to avoid cloning the entire state.
183 /// Uses read lock for better performance in concurrent scenarios.
184 pub fn is_resolved(&self) -> bool {
185 matches!(&*self.state.read(), FutureState::Resolved(_))
186 }
187
188 /// Returns true if the future has failed with an error
189 ///
190 /// This method is optimized to avoid cloning the entire state.
191 /// Uses read lock for better performance in concurrent scenarios.
192 pub fn is_error(&self) -> bool {
193 matches!(&*self.state.read(), FutureState::Error(_))
194 }
195
196 /// Returns the resolved value if available, otherwise None
197 ///
198 /// This method is optimized to avoid unnecessary cloning of the entire state.
199 /// It directly accesses the state and clones only the value if present.
200 /// Uses read lock for better performance in concurrent scenarios.
201 pub fn value(&self) -> Option<T> {
202 match &*self.state.read() {
203 FutureState::Resolved(value) => Some(value.clone()),
204 _ => None,
205 }
206 }
207
208 /// Returns the error if available, otherwise None
209 ///
210 /// This method is optimized to avoid unnecessary cloning of the entire state.
211 /// It directly accesses the state and clones only the error if present.
212 /// Uses read lock for better performance in concurrent scenarios.
213 pub fn error(&self) -> Option<E> {
214 match &*self.state.read() {
215 FutureState::Error(error) => Some(error.clone()),
216 _ => None,
217 }
218 }
219
220 /// Cancel the running future task if it exists
221 ///
222 /// This method can be used to manually cancel a long-running future
223 /// to free up resources and prevent unnecessary work.
224 pub fn cancel(&self) {
225 if let Some(task_handle) = self.task_handle.lock().take() {
226 task_handle.abort();
227 }
228 }
229
230 /// Internal method to update the state
231 /// Uses write lock for state mutations
232 fn set_state(&self, new_state: FutureState<T, E>) {
233 *self.state.write() = new_state;
234 }
235
236 /// Internal method to set the task handle
237 fn set_task_handle(&self, handle: JoinHandle<()>) {
238 *self.task_handle.lock() = Some(handle);
239 }
240}
241
242impl<T, E> Clone for FutureHandle<T, E> {
243 fn clone(&self) -> Self {
244 Self {
245 state: self.state.clone(),
246 task_handle: self.task_handle.clone(),
247 }
248 }
249}
250
251/// Enhanced Resource Management: Automatic cleanup on drop
252///
253/// This ensures that futures are properly cancelled when the handle
254/// is dropped, preventing resource leaks and zombie tasks.
255impl<T, E> Drop for FutureHandle<T, E> {
256 fn drop(&mut self) {
257 // Ensure cleanup on drop - cancel any running task
258 // We directly access the task_handle to avoid trait bound issues
259 if let Some(task_handle) = self.task_handle.lock().take() {
260 task_handle.abort();
261 }
262 }
263}
264
265/// Internal state for tracking future operations
266struct FutureHookState<T, E> {
267 /// Previous dependencies for comparison
268 prev_deps: Option<Box<dyn EffectDependencies>>,
269 /// Handle to the current future operation
270 handle: FutureHandle<T, E>,
271 /// Whether this hook has been initialized
272 initialized: bool,
273 /// Counter for active futures in this component
274 active_futures: Arc<AtomicUsize>,
275}
276
277impl<T, E> FutureHookState<T, E>
278where
279 T: Clone,
280 E: Clone,
281{
282 fn new() -> Self {
283 Self {
284 prev_deps: None,
285 handle: FutureHandle::new(),
286 initialized: false,
287 active_futures: Arc::new(AtomicUsize::new(0)),
288 }
289 }
290}
291
292/// Implement Drop to ensure proper cleanup of dependencies
293impl<T, E> Drop for FutureHookState<T, E> {
294 fn drop(&mut self) {
295 // Clear dependencies to prevent memory leaks
296 self.prev_deps = None;
297
298 // Cancel any running task and decrement counters
299 if let Some(task_handle) = self.handle.task_handle.lock().take() {
300 task_handle.abort();
301
302 // Security: Decrement counters when task is cancelled during drop
303 let active_count = self.active_futures.load(Ordering::Relaxed);
304 if active_count > 0 {
305 self.active_futures.fetch_sub(1, Ordering::Relaxed);
306 GLOBAL_ACTIVE_FUTURES.fetch_sub(1, Ordering::Relaxed);
307 }
308 }
309 }
310}
311
312/// Error type for future operations that don't return a Result
313#[derive(Debug, Clone)]
314pub struct FutureError {
315 message: String,
316}
317
318impl fmt::Display for FutureError {
319 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320 write!(f, "Future error: {}", self.message)
321 }
322}
323
324impl std::error::Error for FutureError {}
325
326/// React-style useFuture hook that provides async future management for components
327///
328/// This function provides future-based async operations with React-like behavior:
329/// - Futures are executed when dependencies change or on first render
330/// - Supports dependency arrays for conditional re-execution
331/// - Provides comprehensive state management (pending, resolved, error)
332/// - Handles future cancellation on dependency changes or component unmount
333/// - Thread-safe and optimized for concurrent access
334/// - Supports both Result-returning and direct value futures
335///
336/// # Examples
337///
338/// ## Basic Future (runs once on mount)
339/// ```rust,no_run
340/// use reratui_hooks::future::{use_future, FutureState};
341/// use std::time::Duration;
342///
343/// // Simple example showing basic future usage
344/// let future_handle = use_future::<(), _, _, _, _>(|| async {
345/// // Simulate async work
346/// tokio::time::sleep(Duration::from_millis(100)).await;
347/// Ok::<String, String>("Hello, World!".to_string())
348/// }, None);
349///
350/// match future_handle.state() {
351/// FutureState::Idle => println!("Not started"),
352/// FutureState::Pending => println!("Loading..."),
353/// FutureState::Resolved(data) => println!("Data: {}", data),
354/// FutureState::Error(err) => println!("Error: {}", err),
355/// }
356/// ```
357///
358/// ## Future with Dependencies
359/// ```rust,no_run
360/// use reratui_hooks::future::{use_future, FutureState};
361/// use std::time::Duration;
362///
363/// // Example with dependency tracking
364/// let user_id = 123;
365/// let user_future = use_future::<i32, _, _, _, _>(move || async move {
366/// // Simulate API call
367/// tokio::time::sleep(Duration::from_millis(200)).await;
368/// Ok::<String, String>(format!("User data for ID: {}", user_id))
369/// }, Some(user_id));
370///
371/// match user_future.state() {
372/// FutureState::Idle => println!("Not started"),
373/// FutureState::Pending => println!("Loading user..."),
374/// FutureState::Resolved(user) => println!("User: {}", user),
375/// FutureState::Error(err) => println!("Failed to load user: {}", err),
376/// }
377/// ```
378///
379/// ## Future with Manual Triggering
380/// ```rust,no_run
381/// use reratui_hooks::future::{use_future, FutureState};
382/// use std::time::Duration;
383///
384/// // Example with manual triggering
385/// let trigger = 1;
386/// let data_future = use_future::<i32, _, _, _, _>(move || async move {
387/// // Simulate work
388/// tokio::time::sleep(Duration::from_millis(100)).await;
389/// Ok::<String, String>(format!("Data fetched at trigger: {}", trigger))
390/// }, Some(trigger));
391///
392/// match data_future.state() {
393/// FutureState::Idle => println!("Not started"),
394/// FutureState::Pending => println!("Fetching..."),
395/// FutureState::Resolved(data) => println!("{}", data),
396/// FutureState::Error(err) => println!("Error: {}", err),
397/// }
398/// ```
399///
400/// # Error Handling
401///
402/// This function will panic if called outside of a component render context.
403/// Always ensure useFuture is called within a component function.
404///
405/// # Performance Notes
406///
407/// - Futures are automatically cancelled when dependencies change
408/// - State updates are thread-safe and optimized for concurrent access
409/// - Dependency comparison uses PartialEq for efficient change detection
410/// - Multiple futures in the same component are managed independently
411/// - Memory usage is minimal with Arc-based sharing
412pub fn use_future<Deps, F, Fut, T, E>(
413 future_factory: F,
414 deps: impl Into<Option<Deps>>,
415) -> FutureHandle<T, E>
416where
417 Deps: EffectDependencies + Clone + PartialEq + 'static,
418 F: FnOnce() -> Fut + Send + 'static,
419 Fut: std::future::Future<Output = Result<T, E>> + Send + 'static,
420 T: Clone + Send + Sync + 'static,
421 E: Clone + Send + Sync + From<String> + 'static,
422{
423 let deps = deps.into();
424 with_hook_context(|ctx| {
425 let hook_index = ctx.next_hook_index();
426 let mut states = ctx.states.borrow_mut();
427
428 // Get or create future state for this hook
429 let future_state = states
430 .entry(hook_index)
431 .or_insert_with(|| Box::new(FutureHookState::<T, E>::new()))
432 .downcast_mut::<FutureHookState<T, E>>()
433 .expect("Future state type mismatch");
434
435 // Determine if future should run
436 let should_run = match &deps {
437 None => {
438 // No dependencies - run on every render (usually not recommended for futures)
439 true
440 }
441 Some(current_deps) => {
442 // Check if dependencies have changed
443 match &future_state.prev_deps {
444 None => {
445 // First run - always execute
446 true
447 }
448 Some(prev_deps) => {
449 // Optimized dependency comparison with hash-based fast path
450 if prev_deps.deps_hash() != current_deps.deps_hash() {
451 // Hash mismatch - dependencies definitely changed
452 true
453 } else {
454 // Hash match - might be collision, do detailed comparison
455 !current_deps.deps_eq(prev_deps.as_ref())
456 }
457 }
458 }
459 }
460 };
461
462 if should_run {
463 // Security Check: Prevent resource exhaustion attacks
464 let component_futures = future_state.active_futures.load(Ordering::Relaxed);
465 let global_futures = GLOBAL_ACTIVE_FUTURES.load(Ordering::Relaxed);
466
467 if component_futures >= MAX_CONCURRENT_FUTURES_PER_COMPONENT {
468 future_state.handle.set_state(FutureState::Error(
469 format!(
470 "Component future limit exceeded: {}/{}",
471 component_futures, MAX_CONCURRENT_FUTURES_PER_COMPONENT
472 )
473 .into(),
474 ));
475 return future_state.handle.clone();
476 }
477
478 if global_futures >= MAX_GLOBAL_CONCURRENT_FUTURES {
479 future_state.handle.set_state(FutureState::Error(
480 format!(
481 "Global future limit exceeded: {}/{}",
482 global_futures, MAX_GLOBAL_CONCURRENT_FUTURES
483 )
484 .into(),
485 ));
486 return future_state.handle.clone();
487 }
488
489 // Cancel any existing future
490 future_state.handle.cancel();
491
492 // Reset state to pending
493 future_state.handle.set_state(FutureState::Pending);
494
495 // Store new dependencies
496 if let Some(current_deps) = &deps {
497 future_state.prev_deps = Some(current_deps.clone_deps());
498 } else {
499 future_state.prev_deps = None;
500 }
501
502 // Increment counters before spawning
503 future_state.active_futures.fetch_add(1, Ordering::Relaxed);
504 GLOBAL_ACTIVE_FUTURES.fetch_add(1, Ordering::Relaxed);
505
506 // Create clones for the async task
507 let handle_clone_for_success = future_state.handle.clone();
508 let handle_clone_for_error = future_state.handle.clone();
509 let handle_clone_for_panic = future_state.handle.clone();
510 let active_futures_clone = future_state.active_futures.clone();
511
512 // Spawn the future
513 let task_handle = tokio::spawn(async move {
514 let result = spawn_catch_panic(async move {
515 match future_factory().await {
516 Ok(value) => {
517 handle_clone_for_success.set_state(FutureState::Resolved(value));
518 }
519 Err(error) => {
520 handle_clone_for_error.set_state(FutureState::Error(error));
521 }
522 }
523 })
524 .await;
525
526 // Security Fix: Better panic handling with detailed error information
527 if let Err(join_error) = result {
528 let panic_message = if join_error.is_panic() {
529 format!("Future panicked: {}", join_error)
530 } else if join_error.is_cancelled() {
531 "Future was cancelled".to_string()
532 } else {
533 format!("Future failed: {}", join_error)
534 };
535
536 handle_clone_for_panic.set_state(FutureState::Error(panic_message.into()));
537 }
538
539 // Security: Always decrement counters when future completes (success, error, or panic)
540 active_futures_clone.fetch_sub(1, Ordering::Relaxed);
541 GLOBAL_ACTIVE_FUTURES.fetch_sub(1, Ordering::Relaxed);
542 });
543
544 // Store the task handle for potential cancellation
545 future_state.handle.set_task_handle(task_handle);
546 future_state.initialized = true;
547 }
548
549 // Return a clone of the handle
550 future_state.handle.clone()
551 })
552}