use crate::backend::{AnyPage, CookieData};
use crate::page::Page;
use crate::state::SessionKey;
use arc_swap::ArcSwap;
use rustc_hash::FxHashMap as HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ConsoleMsg {
pub r#type: String,
pub text: String,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct NetRequest {
pub id: String,
pub method: String,
pub url: String,
pub resource_type: String,
pub status: Option<i64>,
pub mime_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub post_data: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct DialogEvent {
pub dialog_type: String,
pub message: String,
pub action: String,
}
pub struct BrowserContext {
pub pages: Vec<AnyPage>,
pub active_page_idx: usize,
pub ref_map: Arc<ArcSwap<HashMap<String, i64>>>,
pub console_log: Arc<RwLock<Vec<ConsoleMsg>>>,
pub network_log: Arc<RwLock<Vec<NetRequest>>>,
pub dialog_log: Arc<RwLock<Vec<DialogEvent>>>,
name: String,
pub cdp_context_id: Option<String>,
}
impl BrowserContext {
pub(crate) fn new(name: String) -> Self {
Self {
pages: Vec::new(),
active_page_idx: 0,
ref_map: Arc::new(ArcSwap::from_pointee(HashMap::default())),
console_log: Arc::new(RwLock::new(Vec::new())),
network_log: Arc::new(RwLock::new(Vec::new())),
dialog_log: Arc::new(RwLock::new(Vec::new())),
name,
cdp_context_id: None,
}
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn active_page(&self) -> Option<&AnyPage> {
self.pages.get(self.active_page_idx)
}
pub async fn cookies(&self) -> Result<Vec<CookieData>, String> {
if let Some(page) = self.active_page() {
page.get_cookies().await
} else {
Ok(Vec::new())
}
}
pub async fn add_cookies(&self, cookies: Vec<CookieData>) -> Result<(), String> {
let page = self.active_page().ok_or("No page in context")?;
for cookie in cookies {
page.set_cookie(cookie).await?;
}
Ok(())
}
pub async fn clear_cookies(&self) -> Result<(), String> {
if let Some(page) = self.active_page() {
page.clear_cookies().await?;
}
Ok(())
}
pub async fn delete_cookie(&self, name: &str, domain: Option<&str>) -> Result<(), String> {
let cookies = self.cookies().await?;
if let Some(page) = self.active_page() {
page.clear_cookies().await?;
for cookie in cookies {
let name_matches = cookie.name == name;
let domain_matches = domain.is_none_or(|d| cookie.domain == d);
if !(name_matches && domain_matches) {
page.set_cookie(cookie).await?;
}
}
}
Ok(())
}
pub async fn console_messages(&self, level: Option<&str>, limit: usize) -> Vec<ConsoleMsg> {
let msgs = self.console_log.read().await;
msgs
.iter()
.filter(|m| level.is_none_or(|l| l == "all" || m.r#type == l))
.rev()
.take(limit)
.cloned()
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect()
}
pub async fn network_requests(&self, limit: usize) -> Vec<NetRequest> {
let reqs = self.network_log.read().await;
reqs
.iter()
.rev()
.take(limit)
.cloned()
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect()
}
pub async fn dialog_messages(&self, limit: usize) -> Vec<DialogEvent> {
let msgs = self.dialog_log.read().await;
let start = msgs.len().saturating_sub(limit);
msgs[start..].to_vec()
}
}
use crate::state::BrowserState;
#[derive(Clone)]
pub struct ContextRef {
pub(crate) state: Arc<RwLock<BrowserState>>,
pub(crate) name: Arc<str>,
pub(crate) key: SessionKey,
default_timeout_ms: u64,
default_navigation_timeout_ms: u64,
}
impl ContextRef {
pub fn new(state: Arc<RwLock<BrowserState>>, name: String) -> Self {
let key = SessionKey::parse(&name);
Self {
state,
name: Arc::from(name),
key,
default_timeout_ms: 0,
default_navigation_timeout_ms: 0,
}
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
pub async fn new_page(&self) -> Result<Arc<Page>, String> {
{
let mut state = self.state.write().await;
Box::pin(state.ensure_instance(&self.key.instance)).await?;
}
let plan = {
let state = self.state.read().await;
state.page_open_plan(&self.key)?
};
let (any_page, browser_context_id) = if &*self.key.context == "default" {
(
Box::pin(plan.browser.new_page(
"about:blank",
plan.browser_context_id.as_deref(),
plan.viewport.as_ref(),
))
.await?,
None,
)
} else if let Some(existing_ctx_id) = plan.browser_context_id.clone() {
(
Box::pin(
plan
.browser
.new_page("about:blank", Some(&existing_ctx_id), plan.viewport.as_ref()),
)
.await?,
Some(existing_ctx_id),
)
} else {
let ctx_id = plan.browser.new_context().await?;
let page = Box::pin(
plan
.browser
.new_page("about:blank", Some(&ctx_id), plan.viewport.as_ref()),
)
.await?;
(page, Some(ctx_id))
};
{
let mut state = self.state.write().await;
state.register_opened_page(&self.key, any_page.clone(), browser_context_id)?;
}
Ok(Page::with_context(any_page, self.clone()))
}
pub async fn pages(&self) -> Result<Vec<Arc<Page>>, String> {
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
Ok(
ctx
.pages
.iter()
.map(|p| Page::with_context(p.clone(), self.clone()))
.collect(),
)
}
pub async fn cookies(&self) -> Result<Vec<CookieData>, String> {
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
ctx.cookies().await
}
pub async fn add_cookies(&self, cookies: Vec<CookieData>) -> Result<(), String> {
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
ctx.add_cookies(cookies).await
}
pub async fn clear_cookies(&self) -> Result<(), String> {
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
ctx.clear_cookies().await
}
pub async fn clear_cookies_filtered(&self, options: &crate::backend::ClearCookieOptions) -> Result<(), String> {
if options.name.is_none() && options.domain.is_none() && options.path.is_none() {
return self.clear_cookies().await;
}
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
let cookies = ctx.cookies().await?;
if let Some(page) = ctx.active_page() {
page.clear_cookies().await?;
for c in cookies {
let name_match = options.name.as_ref().is_none_or(|n| &c.name == n);
let domain_match = options.domain.as_ref().is_none_or(|d| &c.domain == d);
let path_match = options.path.as_ref().is_none_or(|p| &c.path == p);
if !(name_match && domain_match && path_match) {
page.set_cookie(c).await?;
}
}
}
Ok(())
}
pub async fn delete_cookie(&self, name: &str, domain: Option<&str>) -> Result<(), String> {
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
ctx.delete_cookie(name, domain).await
}
pub fn set_default_timeout(&mut self, ms: u64) {
self.default_timeout_ms = ms;
}
pub fn set_default_navigation_timeout(&mut self, ms: u64) {
self.default_navigation_timeout_ms = ms;
}
pub async fn grant_permissions(&self, permissions: &[String], origin: Option<&str>) -> Result<(), String> {
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
if let Some(page) = ctx.active_page() {
page.grant_permissions(permissions, origin).await
} else {
Err("No page in context".into())
}
}
pub async fn clear_permissions(&self) -> Result<(), String> {
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
if let Some(page) = ctx.active_page() {
page.reset_permissions().await
} else {
Ok(())
}
}
pub async fn close(&self) -> Result<(), String> {
let mut state = self.state.write().await;
state.remove_context(&self.name).await;
Ok(())
}
#[must_use]
pub fn state(&self) -> &Arc<RwLock<BrowserState>> {
&self.state
}
pub async fn add_init_script(&self, source: &str) -> Result<Vec<String>, String> {
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
let mut ids = Vec::new();
for page in &ctx.pages {
ids.push(page.add_init_script(source).await?);
}
Ok(ids)
}
pub async fn set_geolocation(&self, lat: f64, lng: f64, accuracy: f64) -> Result<(), String> {
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
for page in &ctx.pages {
page.set_geolocation(lat, lng, accuracy).await?;
}
Ok(())
}
pub async fn set_extra_http_headers(&self, headers: &rustc_hash::FxHashMap<String, String>) -> Result<(), String> {
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
for page in &ctx.pages {
page.set_extra_http_headers(headers).await?;
}
Ok(())
}
pub async fn set_offline(&self, offline: bool) -> Result<(), String> {
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
for page in &ctx.pages {
page.set_network_state(offline, 0.0, -1.0, -1.0).await?;
}
Ok(())
}
pub async fn route(&self, pattern: &str, handler: crate::route::RouteHandler) -> Result<(), String> {
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
for page in &ctx.pages {
page.route(pattern, handler.clone()).await?;
}
Ok(())
}
pub async fn unroute(&self, pattern: &str) -> Result<(), String> {
let state = self.state.read().await;
let ctx = state.context(&self.name)?;
for page in &ctx.pages {
page.unroute(pattern).await?;
}
Ok(())
}
}