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}