Skip to main content

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}