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}