Skip to main content

claude_code_statusline_core/
timeout.rs

1//! Timeout execution utilities
2//!
3//! This module provides functions for running operations with time limits,
4//! ensuring that slow operations don't block the status line generation.
5
6use crate::error::CoreError;
7use std::sync::mpsc;
8use std::thread;
9use std::time::Duration;
10
11/// Executes a function with a timeout constraint
12///
13/// Spawns the function in a separate thread and waits for completion
14/// up to the specified duration. This prevents slow operations from
15/// blocking the main thread indefinitely.
16///
17/// # Arguments
18///
19/// * `dur` - Maximum duration to wait for the function to complete
20/// * `f` - The function to execute with timeout protection
21///
22/// # Returns
23///
24/// * `Ok(Some(T))` - Function completed successfully within timeout
25/// * `Ok(None)` - Function timed out
26/// * `Err` - Function panicked or returned an error
27///
28/// # Examples
29///
30/// ```
31/// use claude_code_statusline_core::timeout::run_with_timeout;
32/// use std::time::Duration;
33///
34/// let result = run_with_timeout(Duration::from_millis(100), || {
35///     Ok("Success".to_string())
36/// });
37/// assert!(result.unwrap().is_some());
38///
39/// let timeout = run_with_timeout(Duration::from_millis(10), || {
40///     std::thread::sleep(Duration::from_millis(100));
41///     Ok("Too slow")
42/// });
43/// assert!(timeout.unwrap().is_none());
44/// ```
45///
46/// # Implementation Notes
47///
48/// - Uses channels for thread communication
49/// - Catches panics and converts them to errors
50/// - Thread is detached after timeout (may continue running)
51pub fn run_with_timeout<F, T>(dur: Duration, f: F) -> Result<Option<T>, CoreError>
52where
53    F: Send + 'static + FnOnce() -> Result<T, CoreError>,
54    T: Send + 'static,
55{
56    let (tx, rx) = mpsc::channel();
57
58    thread::spawn(move || {
59        let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
60        // Map panic into typed error; send result through channel if possible
61        let _ = match res {
62            Ok(Ok(val)) => tx.send(Ok(val)),
63            Ok(Err(err)) => tx.send(Err(err)),
64            Err(_) => tx.send(Err(CoreError::TaskPanic)),
65        };
66    });
67
68    match rx.recv_timeout(dur) {
69        Ok(Ok(v)) => Ok(Some(v)),
70        Ok(Err(e)) => Err(e),
71        Err(mpsc::RecvTimeoutError::Timeout) => Ok(None),
72        Err(mpsc::RecvTimeoutError::Disconnected) => Err(CoreError::WorkerDisconnected),
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn completes_before_timeout() {
82        let out = run_with_timeout(Duration::from_millis(200), || {
83            std::thread::sleep(Duration::from_millis(50));
84            Ok::<_, CoreError>(42)
85        })
86        .unwrap();
87        assert_eq!(out, Some(42));
88    }
89
90    #[test]
91    fn returns_none_on_timeout() {
92        let out = run_with_timeout(Duration::from_millis(30), || {
93            std::thread::sleep(Duration::from_millis(100));
94            Ok::<_, CoreError>(99)
95        })
96        .unwrap();
97        assert_eq!(out, None);
98    }
99
100    #[test]
101    fn propagates_error() {
102        let err = run_with_timeout(Duration::from_millis(100), || {
103            Err::<i32, _>(CoreError::InvalidConfig("boom".to_string()))
104        })
105        .unwrap_err();
106        assert!(format!("{err}").contains("boom"));
107    }
108}