1use std::time::Duration;
4use url::Url;
5
6use crate::types::error::{Error, Result};
7
8#[derive(Debug, Clone)]
10pub struct BrowserResponse {
11 pub final_url: Url,
13 pub html: String,
15 pub title: Option<String>,
17 pub console_logs: Vec<ConsoleMessage>,
19 pub network_requests: Vec<NetworkRequest>,
21 pub render_time_ms: u64,
23 pub screenshot: Option<Vec<u8>>,
25}
26
27#[derive(Debug, Clone)]
29pub struct ConsoleMessage {
30 pub level: ConsoleLevel,
32 pub text: String,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum ConsoleLevel {
39 Log,
41 Warn,
43 Error,
45 Debug,
47}
48
49#[derive(Debug, Clone)]
51pub struct NetworkRequest {
52 pub url: String,
54 pub method: String,
56 pub resource_type: ResourceType,
58 pub status: Option<u16>,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum ResourceType {
65 Document,
67 Script,
69 Stylesheet,
71 Image,
73 Font,
75 Xhr,
77 WebSocket,
79 Other,
81}
82
83#[derive(Debug, Clone)]
85pub struct RenderOptions {
86 pub timeout: Duration,
88 pub wait_for_network_idle: bool,
90 pub network_idle_timeout_ms: u64,
92 pub wait_for_selector: Option<String>,
94 pub execute_script: Option<String>,
96 pub capture_screenshot: bool,
98 pub viewport_width: u32,
100 pub viewport_height: u32,
102 pub user_agent: Option<String>,
104 pub block_resources: Vec<ResourceType>,
106 pub extra_headers: Vec<(String, String)>,
108}
109
110impl Default for RenderOptions {
111 fn default() -> Self {
112 Self {
113 timeout: Duration::from_secs(30),
114 wait_for_network_idle: true,
115 network_idle_timeout_ms: 500,
116 wait_for_selector: None,
117 execute_script: None,
118 capture_screenshot: false,
119 viewport_width: 1920,
120 viewport_height: 1080,
121 user_agent: None,
122 block_resources: vec![ResourceType::Image, ResourceType::Font],
123 extra_headers: Vec::new(),
124 }
125 }
126}
127
128#[allow(async_fn_in_trait)]
130pub trait BrowserBackend: Send + Sync {
131 async fn render(&self, url: &Url, options: &RenderOptions) -> Result<BrowserResponse>;
133
134 async fn health_check(&self) -> Result<()>;
136
137 async fn close(&self) -> Result<()>;
139}
140
141pub struct BrowserPool {
143 backend_type: BrowserBackendType,
145 max_concurrent: usize,
147 default_options: RenderOptions,
149 active_count: std::sync::atomic::AtomicUsize,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
155pub enum BrowserBackendType {
156 ChromeCdp,
158 Playwright,
160 Puppeteer,
162 None,
164}
165
166impl Default for BrowserPool {
167 fn default() -> Self {
168 Self::new(BrowserBackendType::None, 4)
169 }
170}
171
172impl BrowserPool {
173 pub fn new(backend_type: BrowserBackendType, max_concurrent: usize) -> Self {
175 Self {
176 backend_type,
177 max_concurrent,
178 default_options: RenderOptions::default(),
179 active_count: std::sync::atomic::AtomicUsize::new(0),
180 }
181 }
182
183 pub fn with_options(mut self, options: RenderOptions) -> Self {
185 self.default_options = options;
186 self
187 }
188
189 pub fn backend_type(&self) -> &BrowserBackendType {
191 &self.backend_type
192 }
193
194 pub fn max_concurrent(&self) -> usize {
196 self.max_concurrent
197 }
198
199 pub fn has_available_slot(&self) -> bool {
201 self.active_count.load(std::sync::atomic::Ordering::Relaxed) < self.max_concurrent
202 }
203
204 pub fn acquire(&self) -> Option<BrowserSlot<'_>> {
206 let current = self.active_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
207 if current >= self.max_concurrent {
208 self.active_count.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
209 None
210 } else {
211 Some(BrowserSlot { pool: self })
212 }
213 }
214
215 pub async fn render(&self, url: &Url, options: Option<&RenderOptions>) -> Result<BrowserResponse> {
217 let _slot = self.acquire().ok_or_else(|| {
218 Error::Config("No browser slots available".to_string())
219 })?;
220
221 let opts = options.unwrap_or(&self.default_options);
222
223 match self.backend_type {
224 BrowserBackendType::None => {
225 Err(Error::Config("No browser backend configured".to_string()))
227 }
228 BrowserBackendType::ChromeCdp => {
229 self.render_chrome_cdp(url, opts).await
230 }
231 BrowserBackendType::Playwright => {
232 self.render_playwright(url, opts).await
233 }
234 BrowserBackendType::Puppeteer => {
235 self.render_puppeteer(url, opts).await
236 }
237 }
238 }
239
240 async fn render_chrome_cdp(&self, url: &Url, options: &RenderOptions) -> Result<BrowserResponse> {
242 let _ = (url, options);
246 Err(Error::Config("Chrome CDP backend not yet implemented".to_string()))
247 }
248
249 async fn render_playwright(&self, url: &Url, options: &RenderOptions) -> Result<BrowserResponse> {
251 let _ = (url, options);
254 Err(Error::Config("Playwright backend not yet implemented".to_string()))
255 }
256
257 async fn render_puppeteer(&self, url: &Url, options: &RenderOptions) -> Result<BrowserResponse> {
259 let _ = (url, options);
261 Err(Error::Config("Puppeteer backend not yet implemented".to_string()))
262 }
263
264 pub fn active_count(&self) -> usize {
266 self.active_count.load(std::sync::atomic::Ordering::Relaxed)
267 }
268}
269
270pub struct BrowserSlot<'a> {
272 pool: &'a BrowserPool,
273}
274
275impl Drop for BrowserSlot<'_> {
276 fn drop(&mut self) {
277 self.pool.active_count.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
278 }
279}
280
281pub struct StubBrowser {
283 html: String,
285}
286
287impl StubBrowser {
288 pub fn new(html: impl Into<String>) -> Self {
290 Self { html: html.into() }
291 }
292}
293
294impl BrowserBackend for StubBrowser {
295 async fn render(&self, url: &Url, _options: &RenderOptions) -> Result<BrowserResponse> {
296 Ok(BrowserResponse {
297 final_url: url.clone(),
298 html: self.html.clone(),
299 title: None,
300 console_logs: Vec::new(),
301 network_requests: Vec::new(),
302 render_time_ms: 0,
303 screenshot: None,
304 })
305 }
306
307 async fn health_check(&self) -> Result<()> {
308 Ok(())
309 }
310
311 async fn close(&self) -> Result<()> {
312 Ok(())
313 }
314}