html2pdf_api/handle.rs
1//! RAII handle for browser instances.
2//!
3//! This module provides [`BrowserHandle`], which wraps a browser instance
4//! and automatically returns it to the pool when dropped.
5//!
6//! # Overview
7//!
8//! The handle implements the RAII (Resource Acquisition Is Initialization)
9//! pattern to ensure browsers are always returned to the pool, even if:
10//! - Your code returns early
11//! - An error occurs
12//! - A panic happens
13//!
14//! # Usage Pattern
15//!
16//! ```rust,ignore
17//! use html2pdf_api::BrowserPool;
18//!
19//! let pool = BrowserPool::builder()
20//! .factory(Box::new(ChromeBrowserFactory::with_defaults()))
21//! .build()?;
22//!
23//! // Get a browser handle
24//! let browser = pool.get()?;
25//!
26//! // Use it like a regular Browser (via Deref)
27//! let tab = browser.new_tab()?;
28//! tab.navigate_to("https://example.com")?;
29//!
30//! // Browser automatically returned when `browser` goes out of scope
31//! ```
32//!
33//! # Deref Behavior
34//!
35//! `BrowserHandle` implements [`Deref<Target = Browser>`](std::ops::Deref),
36//! allowing transparent access to all [`Browser`] methods:
37//!
38//! ```rust,ignore
39//! let browser = pool.get()?;
40//!
41//! // These all work directly on the handle:
42//! let tab = browser.new_tab()?; // Browser::new_tab
43//! let tabs = browser.get_tabs(); // Browser::get_tabs
44//! let version = browser.get_version()?; // Browser::get_version
45//! ```
46
47use std::sync::Arc;
48
49use headless_chrome::Browser;
50
51use crate::pool::BrowserPoolInner;
52use crate::tracked::TrackedBrowser;
53
54/// RAII handle for browser instances.
55///
56/// Automatically returns the browser to the pool when dropped.
57/// This ensures browsers are always returned even if the code panics.
58///
59/// # Thread Safety
60///
61/// `BrowserHandle` is `Send` but not `Sync`. This means:
62/// - ✅ You can move it to another thread
63/// - ❌ You cannot share it between threads simultaneously
64///
65/// This matches the typical usage pattern where a single request/task
66/// uses a browser exclusively.
67///
68/// # Usage
69///
70/// ```rust,ignore
71/// let browser_handle = pool.get()?;
72///
73/// // Use browser via Deref
74/// let tab = browser_handle.new_tab()?;
75/// // ... do work ...
76///
77/// // Browser automatically returned to pool when handle goes out of scope
78/// ```
79///
80/// # Explicit Drop
81///
82/// If you need to return the browser early (before end of scope),
83/// you can explicitly drop the handle:
84///
85/// ```rust,ignore
86/// let browser = pool.get()?;
87/// let tab = browser.new_tab()?;
88/// // ... do work ...
89///
90/// // Return browser early
91/// drop(browser);
92///
93/// // Browser is now back in the pool and available for others
94/// // Attempting to use `browser` here would be a compile error
95/// ```
96///
97/// # Panic Safety
98///
99/// The RAII pattern ensures browsers are returned even during panics:
100///
101/// ```rust,ignore
102/// let browser = pool.get()?;
103///
104/// // Even if this panics...
105/// some_function_that_might_panic();
106///
107/// // ...the browser is still returned to the pool during unwinding
108/// ```
109pub struct BrowserHandle {
110 /// The tracked browser (Option allows taking in Drop).
111 ///
112 /// This is `Option` so we can `take()` it in the `Drop` implementation
113 /// without requiring `&mut self` to be valid after drop.
114 tracked: Option<Arc<TrackedBrowser>>,
115
116 /// Reference to pool for returning browser.
117 ///
118 /// We keep an `Arc` reference to the pool's inner state so we can
119 /// return the browser even if the original `BrowserPool` has been dropped.
120 pool: Arc<BrowserPoolInner>,
121}
122
123impl BrowserHandle {
124 /// Create a new browser handle.
125 ///
126 /// This is called internally by [`BrowserPool::get()`](crate::BrowserPool::get).
127 /// Users should not need to call this directly.
128 ///
129 /// # Parameters
130 ///
131 /// * `tracked` - The tracked browser instance.
132 /// * `pool` - Arc reference to the pool's inner state.
133 pub(crate) fn new(tracked: Arc<TrackedBrowser>, pool: Arc<BrowserPoolInner>) -> Self {
134 Self {
135 tracked: Some(tracked),
136 pool,
137 }
138 }
139
140 /// Get the browser's unique ID.
141 ///
142 /// Useful for logging and debugging.
143 ///
144 /// # Returns
145 ///
146 /// The unique ID assigned to this browser instance.
147 ///
148 /// # Example
149 ///
150 /// ```rust,ignore
151 /// let browser = pool.get()?;
152 /// log::info!("Using browser {}", browser.id());
153 /// ```
154 pub fn id(&self) -> u64 {
155 self.tracked.as_ref().map(|t| t.id()).unwrap_or(0)
156 }
157
158 /// Get the browser's age (time since creation).
159 ///
160 /// Useful for monitoring and debugging.
161 ///
162 /// # Returns
163 ///
164 /// Duration since the browser was created.
165 ///
166 /// # Example
167 ///
168 /// ```rust,ignore
169 /// let browser = pool.get()?;
170 /// log::debug!("Browser age: {:?}", browser.age());
171 /// ```
172 pub fn age(&self) -> std::time::Duration {
173 self.tracked.as_ref().map(|t| t.age()).unwrap_or_default()
174 }
175
176 /// Get the browser's age in minutes.
177 ///
178 /// Convenience method for human-readable logging.
179 ///
180 /// # Example
181 ///
182 /// ```rust,ignore
183 /// let browser = pool.get()?;
184 /// log::info!("Browser {} is {} minutes old", browser.id(), browser.age_minutes());
185 /// ```
186 pub fn age_minutes(&self) -> u64 {
187 self.tracked.as_ref().map(|t| t.age_minutes()).unwrap_or(0)
188 }
189}
190
191impl std::ops::Deref for BrowserHandle {
192 type Target = Browser;
193
194 /// Transparently access the underlying Browser.
195 ///
196 /// This allows using all [`Browser`] methods directly on the handle:
197 ///
198 /// ```rust,ignore
199 /// let browser = pool.get()?;
200 ///
201 /// // new_tab() is a Browser method, but works on BrowserHandle
202 /// let tab = browser.new_tab()?;
203 /// ```
204 ///
205 /// # Panics
206 ///
207 /// Panics if called after the browser has been returned to the pool.
208 /// This should never happen in normal usage since the handle owns
209 /// the browser until it's dropped.
210 fn deref(&self) -> &Self::Target {
211 self.tracked.as_ref().unwrap().browser()
212 }
213}
214
215impl Drop for BrowserHandle {
216 /// Automatically return browser to pool when handle is dropped.
217 ///
218 /// This is the critical RAII pattern that ensures browsers are always
219 /// returned to the pool, even if the code using them panics.
220 ///
221 /// # Implementation Details
222 ///
223 /// - Uses `Option::take()` to move the browser out of the handle
224 /// - Calls `BrowserPoolInner::return_browser()` to return it
225 /// - Safe to call multiple times (subsequent calls are no-ops)
226 fn drop(&mut self) {
227 if let Some(tracked) = self.tracked.take() {
228 log::debug!(
229 "♻️ BrowserHandle {} being dropped, returning to pool...",
230 tracked.id()
231 );
232
233 // Return to pool using static method (avoids &mut self issues)
234 BrowserPoolInner::return_browser(&self.pool, tracked);
235 }
236 }
237}
238
239impl std::fmt::Debug for BrowserHandle {
240 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241 match &self.tracked {
242 Some(tracked) => f
243 .debug_struct("BrowserHandle")
244 .field("id", &tracked.id())
245 .field("age_minutes", &tracked.age_minutes())
246 .finish(),
247 None => f
248 .debug_struct("BrowserHandle")
249 .field("state", &"returned")
250 .finish(),
251 }
252 }
253}
254
255// ============================================================================
256// Unit Tests
257// ============================================================================
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use crate::config::BrowserPoolConfig;
263 use crate::factory::mock::MockBrowserFactory;
264 use crate::pool::BrowserPoolInner;
265
266 fn create_test_pool_inner() -> Arc<BrowserPoolInner> {
267 Arc::new(BrowserPoolInner::new_for_test(
268 BrowserPoolConfig::default(),
269 Box::new(MockBrowserFactory::always_fails("test")),
270 tokio::runtime::Handle::current(),
271 ))
272 }
273
274 /// Verifies that BrowserHandle methods return graceful fallbacks when empty (post-drop).
275 #[tokio::test]
276 async fn test_handle_id_returns_zero_when_tracked_is_none() {
277 let handle = BrowserHandle {
278 tracked: None,
279 pool: create_test_pool_inner(),
280 };
281 assert_eq!(handle.id(), 0);
282 assert_eq!(handle.age_minutes(), 0);
283 assert_eq!(handle.age(), std::time::Duration::default());
284 }
285
286 /// Verifies that Debug is formatted safely for a returned handle.
287 #[tokio::test]
288 async fn test_handle_debug_when_returned() {
289 let handle = BrowserHandle {
290 tracked: None,
291 pool: create_test_pool_inner(),
292 };
293 let debug_output = format!("{:?}", handle);
294 assert!(debug_output.contains("returned"));
295 assert!(!debug_output.contains("age_minutes"));
296 }
297
298 /// Verifies Debug incorporates age if active, although requires real chrome dependencies to fully run locally.
299 #[test]
300 #[ignore = "Requires launching a real Chrome process for TrackedBrowser initialization"]
301 fn test_handle_debug_when_active() {
302 // Assertions verifying that `Debug` includes `age_minutes`
303 // when `tracked: Some(...)` would live here.
304 }
305}