use std::time::Duration;
use url::Url;
use crate::types::error::{Error, Result};
#[derive(Debug, Clone)]
pub struct BrowserResponse {
pub final_url: Url,
pub html: String,
pub title: Option<String>,
pub console_logs: Vec<ConsoleMessage>,
pub network_requests: Vec<NetworkRequest>,
pub render_time_ms: u64,
pub screenshot: Option<Vec<u8>>,
}
#[derive(Debug, Clone)]
pub struct ConsoleMessage {
pub level: ConsoleLevel,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConsoleLevel {
Log,
Warn,
Error,
Debug,
}
#[derive(Debug, Clone)]
pub struct NetworkRequest {
pub url: String,
pub method: String,
pub resource_type: ResourceType,
pub status: Option<u16>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResourceType {
Document,
Script,
Stylesheet,
Image,
Font,
Xhr,
WebSocket,
Other,
}
#[derive(Debug, Clone)]
pub struct RenderOptions {
pub timeout: Duration,
pub wait_for_network_idle: bool,
pub network_idle_timeout_ms: u64,
pub wait_for_selector: Option<String>,
pub execute_script: Option<String>,
pub capture_screenshot: bool,
pub viewport_width: u32,
pub viewport_height: u32,
pub user_agent: Option<String>,
pub block_resources: Vec<ResourceType>,
pub extra_headers: Vec<(String, String)>,
}
impl Default for RenderOptions {
fn default() -> Self {
Self {
timeout: Duration::from_secs(30),
wait_for_network_idle: true,
network_idle_timeout_ms: 500,
wait_for_selector: None,
execute_script: None,
capture_screenshot: false,
viewport_width: 1920,
viewport_height: 1080,
user_agent: None,
block_resources: vec![ResourceType::Image, ResourceType::Font],
extra_headers: Vec::new(),
}
}
}
#[allow(async_fn_in_trait)]
pub trait BrowserBackend: Send + Sync {
async fn render(&self, url: &Url, options: &RenderOptions) -> Result<BrowserResponse>;
async fn health_check(&self) -> Result<()>;
async fn close(&self) -> Result<()>;
}
pub struct BrowserPool {
backend_type: BrowserBackendType,
max_concurrent: usize,
default_options: RenderOptions,
active_count: std::sync::atomic::AtomicUsize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BrowserBackendType {
ChromeCdp,
Playwright,
Puppeteer,
None,
}
impl Default for BrowserPool {
fn default() -> Self {
Self::new(BrowserBackendType::None, 4)
}
}
impl BrowserPool {
pub fn new(backend_type: BrowserBackendType, max_concurrent: usize) -> Self {
Self {
backend_type,
max_concurrent,
default_options: RenderOptions::default(),
active_count: std::sync::atomic::AtomicUsize::new(0),
}
}
pub fn with_options(mut self, options: RenderOptions) -> Self {
self.default_options = options;
self
}
pub fn backend_type(&self) -> &BrowserBackendType {
&self.backend_type
}
pub fn max_concurrent(&self) -> usize {
self.max_concurrent
}
pub fn has_available_slot(&self) -> bool {
self.active_count.load(std::sync::atomic::Ordering::Relaxed) < self.max_concurrent
}
pub fn acquire(&self) -> Option<BrowserSlot<'_>> {
let current = self.active_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
if current >= self.max_concurrent {
self.active_count.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
None
} else {
Some(BrowserSlot { pool: self })
}
}
pub async fn render(&self, url: &Url, options: Option<&RenderOptions>) -> Result<BrowserResponse> {
let _slot = self.acquire().ok_or_else(|| {
Error::Config("No browser slots available".to_string())
})?;
let opts = options.unwrap_or(&self.default_options);
match self.backend_type {
BrowserBackendType::None => {
Err(Error::Config("No browser backend configured".to_string()))
}
BrowserBackendType::ChromeCdp => {
self.render_chrome_cdp(url, opts).await
}
BrowserBackendType::Playwright => {
self.render_playwright(url, opts).await
}
BrowserBackendType::Puppeteer => {
self.render_puppeteer(url, opts).await
}
}
}
async fn render_chrome_cdp(&self, url: &Url, options: &RenderOptions) -> Result<BrowserResponse> {
let _ = (url, options);
Err(Error::Config("Chrome CDP backend not yet implemented".to_string()))
}
async fn render_playwright(&self, url: &Url, options: &RenderOptions) -> Result<BrowserResponse> {
let _ = (url, options);
Err(Error::Config("Playwright backend not yet implemented".to_string()))
}
async fn render_puppeteer(&self, url: &Url, options: &RenderOptions) -> Result<BrowserResponse> {
let _ = (url, options);
Err(Error::Config("Puppeteer backend not yet implemented".to_string()))
}
pub fn active_count(&self) -> usize {
self.active_count.load(std::sync::atomic::Ordering::Relaxed)
}
}
pub struct BrowserSlot<'a> {
pool: &'a BrowserPool,
}
impl Drop for BrowserSlot<'_> {
fn drop(&mut self) {
self.pool.active_count.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
}
}
pub struct StubBrowser {
html: String,
}
impl StubBrowser {
pub fn new(html: impl Into<String>) -> Self {
Self { html: html.into() }
}
}
impl BrowserBackend for StubBrowser {
async fn render(&self, url: &Url, _options: &RenderOptions) -> Result<BrowserResponse> {
Ok(BrowserResponse {
final_url: url.clone(),
html: self.html.clone(),
title: None,
console_logs: Vec::new(),
network_requests: Vec::new(),
render_time_ms: 0,
screenshot: None,
})
}
async fn health_check(&self) -> Result<()> {
Ok(())
}
async fn close(&self) -> Result<()> {
Ok(())
}
}