use std::{
env, fmt,
path::PathBuf,
str::FromStr,
sync::{Arc, Mutex as StdMutex},
};
use tokio::sync::OnceCell;
use void_crawl_core::{
BrowserPool, BrowserSession, PoolConfig, ProfileHandle, Result, VoidCrawlError,
};
use crate::sessions::SessionRegistry;
pub struct PinnedProfile {
pub handle: ProfileHandle,
pub session: StdMutex<Option<BrowserSession>>,
pub name: String,
pub user_data_root: PathBuf,
}
impl fmt::Debug for PinnedProfile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PinnedProfile")
.field("name", &self.name)
.field("user_data_root", &self.user_data_root)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone)]
pub struct AppState {
pool: Arc<OnceCell<Arc<BrowserPool>>>,
pub sessions: Arc<SessionRegistry>,
pub pinned: Option<Arc<PinnedProfile>>,
}
impl AppState {
pub fn new(sessions: Arc<SessionRegistry>) -> Self {
Self { pool: Arc::new(OnceCell::new()), sessions, pinned: None }
}
pub fn with_pinned_profile(sessions: Arc<SessionRegistry>, pinned: PinnedProfile) -> Self {
Self { pool: Arc::new(OnceCell::new()), sessions, pinned: Some(Arc::new(pinned)) }
}
pub fn with_pool(pool: Arc<BrowserPool>, sessions: Arc<SessionRegistry>) -> Self {
let cell = OnceCell::new_with(Some(pool));
Self { pool: Arc::new(cell), sessions, pinned: None }
}
pub async fn pool(&self) -> Result<Arc<BrowserPool>> {
let pinned = self.pinned.clone();
self.pool
.get_or_try_init(|| async move {
let pool = if let Some(p) = pinned {
let session = p
.session
.lock()
.map_err(|_| {
VoidCrawlError::Other("PinnedProfile session mutex poisoned".into())
})?
.take()
.ok_or_else(|| {
VoidCrawlError::Other("PinnedProfile session already consumed".into())
})?;
Arc::new(BrowserPool::new(pool_config_from_env(), vec![session]))
} else {
Arc::new(BrowserPool::from_env().await?)
};
Arc::clone(&pool).start_eviction_task();
Ok(pool)
})
.await
.map(Arc::clone)
}
pub fn pool_if_initialized(&self) -> Option<Arc<BrowserPool>> {
self.pool.get().map(Arc::clone)
}
}
fn pool_config_from_env() -> PoolConfig {
fn parse<T: FromStr>(key: &str, default: T) -> T {
env::var(key).ok().and_then(|s| s.parse().ok()).unwrap_or(default)
}
PoolConfig {
browsers: 1,
tabs_per_browser: parse("TABS_PER_BROWSER", 4),
tab_max_uses: parse("TAB_MAX_USES", 50),
tab_max_idle_secs: parse("TAB_MAX_IDLE_SECS", 60),
acquire_timeout_secs: parse("ACQUIRE_TIMEOUT_SECS", 30),
auto_evict: env::var("AUTO_EVICT").map_or(true, |v| v != "0"),
}
}