use crate::cdp::messages::{CdpEvent, CdpRequest, Message};
use crate::cdp::transport::{self, WebSocketWriter};
use crate::{Error, Result};
use parking_lot::Mutex;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use tokio::sync::{broadcast, mpsc, oneshot};
const EVENT_CHANNEL_CAPACITY: usize = 128;
type SessionKey = Option<String>;
pub struct CdpConnection {
next_id: AtomicU32,
callbacks: Mutex<HashMap<u32, oneshot::Sender<Result<Value>>>>,
event_channels: Mutex<HashMap<SessionKey, broadcast::Sender<CdpEvent>>>,
writer: tokio::sync::Mutex<WebSocketWriter>,
}
impl CdpConnection {
pub async fn connect(
ws_url: &str,
headers: Option<HashMap<String, String>>,
) -> Result<(Arc<Self>, broadcast::Receiver<CdpEvent>)> {
let (writer, message_rx) = transport::connect(ws_url, headers).await?;
let conn = Arc::new(Self {
next_id: AtomicU32::new(1),
callbacks: Mutex::new(HashMap::new()),
event_channels: Mutex::new(HashMap::new()),
writer: tokio::sync::Mutex::new(writer),
});
let browser_rx = conn.subscribe(None);
let conn_for_loop = Arc::clone(&conn);
tokio::spawn(async move {
conn_for_loop.dispatch_loop(message_rx).await;
});
Ok((conn, browser_rx))
}
pub async fn send_command(
self: &Arc<Self>,
session_id: Option<&str>,
method: &str,
params: Value,
) -> Result<Value> {
let id = self.next_id.fetch_add(1, Ordering::SeqCst);
let (tx, rx) = oneshot::channel::<Result<Value>>();
self.callbacks.lock().insert(id, tx);
let request = CdpRequest {
id,
method: method.to_string(),
params,
session_id: session_id.map(|s| s.to_string()),
};
let value = serde_json::to_value(&request)?;
let send_result = self.writer.lock().await.send(&value).await;
if let Err(e) = send_result {
self.callbacks.lock().remove(&id);
return Err(e);
}
match rx.await {
Ok(res) => res,
Err(_) => {
self.callbacks.lock().remove(&id);
Err(Error::ChannelClosed)
}
}
}
pub fn subscribe(&self, session_id: SessionKey) -> broadcast::Receiver<CdpEvent> {
self.event_channels
.lock()
.entry(session_id)
.or_insert_with(|| broadcast::channel(EVENT_CHANNEL_CAPACITY).0)
.subscribe()
}
pub async fn close(&self) -> Result<()> {
self.writer.lock().await.close().await
}
async fn dispatch_loop(self: Arc<Self>, mut message_rx: mpsc::Receiver<Value>) {
while let Some(v) = message_rx.recv().await {
match serde_json::from_value::<Message>(v) {
Ok(Message::Response(r)) => {
let result = match r.error {
Some(e) => Err(Error::CdpError {
code: e.code,
message: e.message,
data: e.data,
}),
None => Ok(r.result.unwrap_or(Value::Null)),
};
if let Some(cb) = self.callbacks.lock().remove(&r.id) {
let _ = cb.send(result);
} else {
tracing::trace!(id = r.id, "orphan CDP response");
}
}
Ok(Message::Event(e)) => {
let key: SessionKey = e.session_id.clone();
let channel = self
.event_channels
.lock()
.entry(key)
.or_insert_with(|| broadcast::channel(EVENT_CHANNEL_CAPACITY).0)
.clone();
let _ = channel.send(e);
}
Err(err) => {
tracing::error!(error = %err, "failed to parse CDP message");
}
}
}
tracing::debug!("CDP transport closed; failing pending callbacks");
self.callbacks.lock().clear();
}
}