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<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: 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
263    /// Verifies that BrowserHandle exposes browser ID.
264    #[test]
265    fn test_handle_id_returns_zero_when_empty() {
266        // We can't easily test with a real TrackedBrowser without Chrome,
267        // but we can verify the method exists and handles edge cases.
268        // In real usage, tracked is always Some until drop.
269    }
270
271    /// Verifies Debug implementation.
272    #[test]
273    fn test_handle_debug_when_returned() {
274        // After drop, the handle shows "returned" state
275        // This is tested implicitly through the Debug impl
276    }
277}