use crate::backend::FrameInfo;
use crate::context::{ConsoleMsg, NetRequest};
use std::sync::Arc;
use tokio::sync::broadcast;
#[derive(Debug, Clone)]
pub enum PageEvent {
Console(ConsoleMsg),
Request(NetRequest),
Response(NetResponse),
Dialog(PendingDialog),
FrameAttached(FrameInfo),
FrameDetached { frame_id: String },
FrameNavigated(FrameInfo),
Load,
DomContentLoaded,
Close,
PageError(String),
Download(DownloadInfo),
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct DownloadInfo {
pub guid: String,
pub url: String,
pub suggested_filename: String,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct NetResponse {
pub request_id: String,
pub url: String,
pub status: i64,
pub status_text: String,
pub mime_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<rustc_hash::FxHashMap<String, String>>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct PendingDialog {
pub dialog_type: String,
pub message: String,
pub default_value: String,
}
#[derive(Debug, Clone)]
pub enum DialogAction {
Accept(Option<String>),
Dismiss,
}
pub type DialogHandler = Arc<dyn Fn(&PendingDialog) -> DialogAction + Send + Sync>;
pub type ExposedFn = Arc<dyn Fn(Vec<serde_json::Value>) -> serde_json::Value + Send + Sync>;
pub type EventCallback = Arc<dyn Fn(PageEvent) + Send + Sync>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ListenerId(pub u64);
#[must_use]
pub fn default_dialog_handler() -> DialogHandler {
Arc::new(|dialog| {
if dialog.dialog_type == "prompt" {
DialogAction::Accept(Some(dialog.default_value.clone()))
} else {
DialogAction::Accept(None)
}
})
}
fn event_name_matches(name: &str, event: &PageEvent) -> bool {
matches!(
(name, event),
("console", PageEvent::Console(_))
| ("request", PageEvent::Request(_))
| ("response", PageEvent::Response(_))
| ("dialog", PageEvent::Dialog(_))
| ("frameattached", PageEvent::FrameAttached(_))
| ("framedetached", PageEvent::FrameDetached { .. })
| ("framenavigated", PageEvent::FrameNavigated(_))
| ("load", PageEvent::Load)
| ("domcontentloaded", PageEvent::DomContentLoaded)
| ("close", PageEvent::Close)
| ("pageerror", PageEvent::PageError(_))
| ("download", PageEvent::Download(_))
)
}
#[derive(Clone)]
pub struct EventEmitter {
tx: broadcast::Sender<PageEvent>,
listeners: Arc<std::sync::Mutex<rustc_hash::FxHashMap<u64, tokio::task::AbortHandle>>>,
next_listener_id: Arc<std::sync::atomic::AtomicU64>,
runtime_handle: Arc<std::sync::Mutex<Option<tokio::runtime::Handle>>>,
}
impl EventEmitter {
#[must_use]
pub fn new() -> Self {
let (tx, _) = broadcast::channel(512);
let handle = tokio::runtime::Handle::try_current().ok();
Self {
tx,
listeners: Arc::new(std::sync::Mutex::new(rustc_hash::FxHashMap::default())),
next_listener_id: Arc::new(std::sync::atomic::AtomicU64::new(1)),
runtime_handle: Arc::new(std::sync::Mutex::new(handle)),
}
}
fn spawn_listener(&self, future: impl std::future::Future<Output = ()> + Send + 'static) -> tokio::task::AbortHandle {
if let Ok(handle) = tokio::runtime::Handle::try_current() {
return handle.spawn(future).abort_handle();
}
if let Ok(guard) = self.runtime_handle.lock() {
if let Some(handle) = guard.as_ref() {
return handle.spawn(future).abort_handle();
}
}
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let Ok(rt) = tokio::runtime::Builder::new_current_thread().enable_all().build() else {
return;
};
let handle = rt.spawn(future);
let _ = tx.send(handle.abort_handle());
rt.block_on(handle).ok();
});
rx.recv().unwrap_or_else(|_| {
tokio::runtime::Handle::current().spawn(async {}).abort_handle()
})
}
pub fn emit(&self, event: PageEvent) {
let _ = self.tx.send(event);
}
#[must_use]
pub fn receiver_count(&self) -> usize {
self.tx.receiver_count()
}
#[must_use]
pub fn subscribe(&self) -> broadcast::Receiver<PageEvent> {
self.tx.subscribe()
}
pub async fn wait_for<F>(&self, predicate: F, timeout_ms: u64) -> Result<PageEvent, String>
where
F: Fn(&PageEvent) -> bool,
{
let mut rx = self.tx.subscribe();
let deadline = tokio::time::Instant::now() + std::time::Duration::from_millis(timeout_ms);
loop {
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
return Err("Timeout waiting for event".into());
}
match tokio::time::timeout(remaining, rx.recv()).await {
Ok(Ok(event)) if predicate(&event) => return Ok(event),
Ok(Ok(_)) => {},
Ok(Err(_)) => return Err("Event channel closed".into()),
Err(_) => return Err("Timeout waiting for event".into()),
}
}
}
pub async fn wait_for_event(&self, event_name: &str, timeout_ms: u64) -> Result<PageEvent, String> {
let name = event_name.to_string();
self.wait_for(move |e| event_name_matches(&name, e), timeout_ms).await
}
pub fn on(&self, event_name: &str, callback: EventCallback) -> ListenerId {
let id = self.next_listener_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let mut rx = self.tx.subscribe();
let name = event_name.to_string();
let abort_handle = self.spawn_listener(async move {
while let Ok(event) = rx.recv().await {
if event_name_matches(&name, &event) {
callback(event);
}
}
});
if let Ok(mut guard) = self.listeners.lock() {
guard.insert(id, abort_handle);
}
ListenerId(id)
}
pub fn once(&self, event_name: &str, callback: EventCallback) -> ListenerId {
let listeners = self.listeners.clone();
let id = self.next_listener_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let mut rx = self.tx.subscribe();
let name = event_name.to_string();
let abort_handle = self.spawn_listener(async move {
while let Ok(event) = rx.recv().await {
if event_name_matches(&name, &event) {
callback(event);
if let Ok(mut guard) = listeners.lock() {
guard.remove(&id);
}
break;
}
}
});
if let Ok(mut guard) = self.listeners.lock() {
guard.insert(id, abort_handle);
}
ListenerId(id)
}
pub fn off(&self, id: ListenerId) {
if let Ok(mut guard) = self.listeners.lock() {
if let Some(handle) = guard.remove(&id.0) {
handle.abort();
}
}
}
pub fn remove_all_listeners(&self) {
if let Ok(mut listeners) = self.listeners.lock() {
for (_, handle) in listeners.drain() {
handle.abort();
}
}
}
}
impl Default for EventEmitter {
fn default() -> Self {
Self::new()
}
}