Skip to main content

chromiumoxide/handler/
mod.rs

1use crate::listeners::{EventListenerRequest, EventListeners};
2use chromiumoxide_cdp::cdp::browser_protocol::browser::*;
3use chromiumoxide_cdp::cdp::browser_protocol::target::*;
4use chromiumoxide_cdp::cdp::events::CdpEvent;
5use chromiumoxide_cdp::cdp::events::CdpEventMessage;
6use chromiumoxide_types::{CallId, Message, Method, Response};
7use chromiumoxide_types::{MethodId, Request as CdpRequest};
8use fnv::FnvHashMap;
9use futures_util::Stream;
10use hashbrown::{HashMap, HashSet};
11use spider_network_blocker::intercept_manager::NetworkInterceptManager;
12use std::pin::Pin;
13use std::task::{Context, Poll};
14use std::time::{Duration, Instant};
15use tokio::sync::mpsc::Receiver;
16use tokio::sync::oneshot::Sender as OneshotSender;
17use tokio_tungstenite::tungstenite::error::ProtocolError;
18use tokio_tungstenite::tungstenite::Error;
19
20use std::sync::Arc;
21use tokio::sync::Notify;
22
23use crate::cmd::{to_command_response, CommandMessage};
24use crate::conn::Connection;
25use crate::error::{CdpError, Result};
26use crate::handler::browser::BrowserContext;
27use crate::handler::frame::FrameRequestedNavigation;
28use crate::handler::frame::{NavigationError, NavigationId, NavigationOk};
29use crate::handler::job::PeriodicJob;
30use crate::handler::session::Session;
31use crate::handler::target::TargetEvent;
32use crate::handler::target::{Target, TargetConfig};
33use crate::handler::viewport::Viewport;
34use crate::page::Page;
35pub(crate) use page::PageInner;
36
37/// Standard timeout in MS
38pub const REQUEST_TIMEOUT: u64 = 30_000;
39
40pub mod blockers;
41pub mod browser;
42pub mod commandfuture;
43pub mod domworld;
44pub mod emulation;
45pub mod frame;
46pub mod http;
47pub mod httpfuture;
48mod job;
49pub mod network;
50pub mod network_utils;
51pub mod page;
52pub mod sender;
53mod session;
54pub mod target;
55pub mod target_message_future;
56pub mod viewport;
57
58/// The handler that monitors the state of the chromium browser and drives all
59/// the requests and events.
60#[must_use = "streams do nothing unless polled"]
61#[derive(Debug)]
62pub struct Handler {
63    pub default_browser_context: BrowserContext,
64    pub browser_contexts: HashSet<BrowserContext>,
65    /// Commands that are being processed and awaiting a response from the
66    /// chromium instance together with the timestamp when the request
67    /// started.
68    pending_commands: FnvHashMap<CallId, (PendingRequest, MethodId, Instant)>,
69    /// Connection to the browser instance
70    from_browser: Receiver<HandlerMessage>,
71    /// Used to loop over all targets in a consistent manner
72    target_ids: Vec<TargetId>,
73    /// The created and attached targets
74    targets: HashMap<TargetId, Target>,
75    /// Currently queued in navigations for targets
76    navigations: FnvHashMap<NavigationId, NavigationRequest>,
77    /// Keeps track of all the current active sessions
78    ///
79    /// There can be multiple sessions per target.
80    sessions: HashMap<SessionId, Session>,
81    /// The websocket connection to the chromium instance.
82    /// `Option` so that `run()` can `.take()` it for splitting.
83    conn: Option<Connection<CdpEventMessage>>,
84    /// Evicts timed out requests periodically
85    evict_command_timeout: PeriodicJob,
86    /// The internal identifier for a specific navigation
87    next_navigation_id: usize,
88    /// How this handler will configure targets etc,
89    config: HandlerConfig,
90    /// All registered event subscriptions
91    event_listeners: EventListeners,
92    /// Keeps track is the browser is closing
93    closing: bool,
94    /// Track the bytes remainder until network request will be blocked.
95    remaining_bytes: Option<u64>,
96    /// The budget is exhausted.
97    budget_exhausted: bool,
98    /// Tracks which targets we've already attached to, to avoid multiple sessions per target.
99    attached_targets: HashSet<TargetId>,
100    /// Optional notify for waking `Handler::run()`'s `tokio::select!` loop
101    /// when a page sends a message.  `None` when using the `Stream` API.
102    page_wake: Option<Arc<Notify>>,
103}
104
105lazy_static::lazy_static! {
106    /// Set the discovery ID target.
107    static ref DISCOVER_ID: (std::borrow::Cow<'static, str>, serde_json::Value) = {
108        let discover = SetDiscoverTargetsParams::new(true);
109        (discover.identifier(), serde_json::to_value(discover).expect("valid discover target params"))
110    };
111    /// Targets params id.
112    static ref TARGET_PARAMS_ID: (std::borrow::Cow<'static, str>, serde_json::Value) = {
113        let msg = GetTargetsParams { filter: None };
114        (msg.identifier(), serde_json::to_value(msg).expect("valid paramtarget"))
115    };
116    /// Set the close targets.
117    static ref CLOSE_PARAMS_ID: (std::borrow::Cow<'static, str>, serde_json::Value) = {
118        let close_msg = CloseParams::default();
119        (close_msg.identifier(), serde_json::to_value(close_msg).expect("valid close params"))
120    };
121}
122
123fn maybe_store_attach_session_id(target: &mut Target, method: &MethodId, resp: &Response) {
124    if method.as_ref() != AttachToTargetParams::IDENTIFIER {
125        return;
126    }
127
128    if let Ok(resp) = to_command_response::<AttachToTargetParams>(resp.clone(), method.clone()) {
129        target.set_session_id(resp.result.session_id);
130    }
131}
132
133impl Handler {
134    /// Create a new `Handler` that drives the connection and listens for
135    /// messages on the receiver `rx`.
136    pub(crate) fn new(
137        mut conn: Connection<CdpEventMessage>,
138        rx: Receiver<HandlerMessage>,
139        config: HandlerConfig,
140    ) -> Self {
141        let discover = DISCOVER_ID.clone();
142        let _ = conn.submit_command(discover.0, None, discover.1);
143        let conn = Some(conn);
144
145        let browser_contexts = config
146            .context_ids
147            .iter()
148            .map(|id| BrowserContext::from(id.clone()))
149            .collect();
150
151        Self {
152            pending_commands: Default::default(),
153            from_browser: rx,
154            default_browser_context: Default::default(),
155            browser_contexts,
156            target_ids: Default::default(),
157            targets: Default::default(),
158            navigations: Default::default(),
159            sessions: Default::default(),
160            conn,
161            evict_command_timeout: PeriodicJob::new(config.request_timeout),
162            next_navigation_id: 0,
163            config,
164            event_listeners: Default::default(),
165            closing: false,
166            remaining_bytes: None,
167            budget_exhausted: false,
168            attached_targets: Default::default(),
169            page_wake: None,
170        }
171    }
172
173    /// Borrow the WebSocket connection, returning an error if it has been
174    /// consumed by [`Handler::run()`].
175    #[inline]
176    fn conn(&mut self) -> Result<&mut Connection<CdpEventMessage>> {
177        self.conn
178            .as_mut()
179            .ok_or_else(|| CdpError::msg("connection consumed by Handler::run()"))
180    }
181
182    /// Return the target with the matching `target_id`
183    pub fn get_target(&self, target_id: &TargetId) -> Option<&Target> {
184        self.targets.get(target_id)
185    }
186
187    /// Iterator over all currently attached targets
188    pub fn targets(&self) -> impl Iterator<Item = &Target> + '_ {
189        self.targets.values()
190    }
191
192    /// The default Browser context
193    pub fn default_browser_context(&self) -> &BrowserContext {
194        &self.default_browser_context
195    }
196
197    /// Iterator over all currently available browser contexts
198    pub fn browser_contexts(&self) -> impl Iterator<Item = &BrowserContext> + '_ {
199        self.browser_contexts.iter()
200    }
201
202    /// received a response to a navigation request like `Page.navigate`
203    fn on_navigation_response(&mut self, id: NavigationId, resp: Response) {
204        if let Some(nav) = self.navigations.remove(&id) {
205            match nav {
206                NavigationRequest::Navigate(mut nav) => {
207                    if nav.navigated {
208                        let _ = nav.tx.send(Ok(resp));
209                    } else {
210                        nav.set_response(resp);
211                        self.navigations
212                            .insert(id, NavigationRequest::Navigate(nav));
213                    }
214                }
215            }
216        }
217    }
218
219    /// A navigation has finished.
220    fn on_navigation_lifecycle_completed(&mut self, res: Result<NavigationOk, NavigationError>) {
221        match res {
222            Ok(ok) => {
223                let id = *ok.navigation_id();
224                if let Some(nav) = self.navigations.remove(&id) {
225                    match nav {
226                        NavigationRequest::Navigate(mut nav) => {
227                            if let Some(resp) = nav.response.take() {
228                                let _ = nav.tx.send(Ok(resp));
229                            } else {
230                                nav.set_navigated();
231                                self.navigations
232                                    .insert(id, NavigationRequest::Navigate(nav));
233                            }
234                        }
235                    }
236                }
237            }
238            Err(err) => {
239                if let Some(nav) = self.navigations.remove(err.navigation_id()) {
240                    match nav {
241                        NavigationRequest::Navigate(nav) => {
242                            let _ = nav.tx.send(Err(err.into()));
243                        }
244                    }
245                }
246            }
247        }
248    }
249
250    /// Received a response to a request.
251    fn on_response(&mut self, resp: Response) {
252        if let Some((req, method, _)) = self.pending_commands.remove(&resp.id) {
253            match req {
254                PendingRequest::CreateTarget(tx) => {
255                    match to_command_response::<CreateTargetParams>(resp, method) {
256                        Ok(resp) => {
257                            if let Some(target) = self.targets.get_mut(&resp.target_id) {
258                                target.set_initiator(tx);
259                            } else {
260                                let _ = tx.send(Err(CdpError::NotFound)).ok();
261                            }
262                        }
263                        Err(err) => {
264                            let _ = tx.send(Err(err)).ok();
265                        }
266                    }
267                }
268                PendingRequest::GetTargets(tx) => {
269                    match to_command_response::<GetTargetsParams>(resp, method) {
270                        Ok(resp) => {
271                            let targets = resp.result.target_infos;
272                            let results = targets.clone();
273
274                            for target_info in targets {
275                                let event: EventTargetCreated = EventTargetCreated { target_info };
276                                self.on_target_created(event);
277                            }
278
279                            let _ = tx.send(Ok(results)).ok();
280                        }
281                        Err(err) => {
282                            let _ = tx.send(Err(err)).ok();
283                        }
284                    }
285                }
286                PendingRequest::Navigate(id) => {
287                    self.on_navigation_response(id, resp);
288                    if self.config.only_html && !self.config.created_first_target {
289                        self.config.created_first_target = true;
290                    }
291                }
292                PendingRequest::ExternalCommand(tx) => {
293                    let _ = tx.send(Ok(resp)).ok();
294                }
295                PendingRequest::InternalCommand(target_id) => {
296                    if let Some(target) = self.targets.get_mut(&target_id) {
297                        maybe_store_attach_session_id(target, &method, &resp);
298                        target.on_response(resp, method.as_ref());
299                    }
300                }
301                PendingRequest::CloseBrowser(tx) => {
302                    self.closing = true;
303                    let _ = tx.send(Ok(CloseReturns {})).ok();
304                }
305            }
306        }
307    }
308
309    /// Submit a command initiated via channel
310    pub(crate) fn submit_external_command(
311        &mut self,
312        msg: CommandMessage,
313        now: Instant,
314    ) -> Result<()> {
315        let call_id =
316            self.conn()?
317                .submit_command(msg.method.clone(), msg.session_id, msg.params)?;
318        self.pending_commands.insert(
319            call_id,
320            (PendingRequest::ExternalCommand(msg.sender), msg.method, now),
321        );
322        Ok(())
323    }
324
325    pub(crate) fn submit_internal_command(
326        &mut self,
327        target_id: TargetId,
328        req: CdpRequest,
329        now: Instant,
330    ) -> Result<()> {
331        let call_id = self.conn()?.submit_command(
332            req.method.clone(),
333            req.session_id.map(Into::into),
334            req.params,
335        )?;
336        self.pending_commands.insert(
337            call_id,
338            (PendingRequest::InternalCommand(target_id), req.method, now),
339        );
340        Ok(())
341    }
342
343    fn submit_fetch_targets(&mut self, tx: OneshotSender<Result<Vec<TargetInfo>>>, now: Instant) {
344        let msg = TARGET_PARAMS_ID.clone();
345
346        if let Some(conn) = self.conn.as_mut() {
347            if let Ok(call_id) = conn.submit_command(msg.0.clone(), None, msg.1) {
348                self.pending_commands
349                    .insert(call_id, (PendingRequest::GetTargets(tx), msg.0, now));
350            }
351        }
352    }
353
354    /// Send the Request over to the server and store its identifier to handle
355    /// the response once received.
356    fn submit_navigation(&mut self, id: NavigationId, req: CdpRequest, now: Instant) {
357        if let Some(conn) = self.conn.as_mut() {
358            if let Ok(call_id) = conn.submit_command(
359                req.method.clone(),
360                req.session_id.map(Into::into),
361                req.params,
362            ) {
363                self.pending_commands
364                    .insert(call_id, (PendingRequest::Navigate(id), req.method, now));
365            }
366        }
367    }
368
369    fn submit_close(&mut self, tx: OneshotSender<Result<CloseReturns>>, now: Instant) {
370        let close_msg = CLOSE_PARAMS_ID.clone();
371
372        if let Some(conn) = self.conn.as_mut() {
373            if let Ok(call_id) = conn.submit_command(close_msg.0.clone(), None, close_msg.1) {
374                self.pending_commands.insert(
375                    call_id,
376                    (PendingRequest::CloseBrowser(tx), close_msg.0, now),
377                );
378            }
379        }
380    }
381
382    /// Process a message received by the target's page via channel
383    fn on_target_message(&mut self, target: &mut Target, msg: CommandMessage, now: Instant) {
384        if msg.is_navigation() {
385            let (req, tx) = msg.split();
386            let id = self.next_navigation_id();
387
388            target.goto(FrameRequestedNavigation::new(
389                id,
390                req,
391                self.config.request_timeout,
392            ));
393
394            self.navigations.insert(
395                id,
396                NavigationRequest::Navigate(NavigationInProgress::new(tx)),
397            );
398        } else {
399            let _ = self.submit_external_command(msg, now);
400        }
401    }
402
403    /// An identifier for queued `NavigationRequest`s.
404    fn next_navigation_id(&mut self) -> NavigationId {
405        let id = NavigationId(self.next_navigation_id);
406        self.next_navigation_id = self.next_navigation_id.wrapping_add(1);
407        id
408    }
409
410    /// Create a new page and send it to the receiver when ready
411    ///
412    /// First a `CreateTargetParams` is send to the server, this will trigger
413    /// `EventTargetCreated` which results in a new `Target` being created.
414    /// Once the response to the request is received the initialization process
415    /// of the target kicks in. This triggers a queue of initialization requests
416    /// of the `Target`, once those are all processed and the `url` fo the
417    /// `CreateTargetParams` has finished loading (The `Target`'s `Page` is
418    /// ready and idle), the `Target` sends its newly created `Page` as response
419    /// to the initiator (`tx`) of the `CreateTargetParams` request.
420    fn create_page(&mut self, params: CreateTargetParams, tx: OneshotSender<Result<Page>>) {
421        let about_blank = params.url == "about:blank";
422        let http_check =
423            !about_blank && params.url.starts_with("http") || params.url.starts_with("file://");
424
425        if about_blank || http_check {
426            let method = params.identifier();
427
428            let Some(conn) = self.conn.as_mut() else {
429                let _ = tx.send(Err(CdpError::msg("connection consumed"))).ok();
430                return;
431            };
432            match serde_json::to_value(params) {
433                Ok(params) => match conn.submit_command(method.clone(), None, params) {
434                    Ok(call_id) => {
435                        self.pending_commands.insert(
436                            call_id,
437                            (PendingRequest::CreateTarget(tx), method, Instant::now()),
438                        );
439                    }
440                    Err(err) => {
441                        let _ = tx.send(Err(err.into())).ok();
442                    }
443                },
444                Err(err) => {
445                    let _ = tx.send(Err(err.into())).ok();
446                }
447            }
448        } else {
449            let _ = tx.send(Err(CdpError::NotFound)).ok();
450        }
451    }
452
453    /// Process an incoming event read from the websocket
454    fn on_event(&mut self, event: CdpEventMessage) {
455        if let Some(session_id) = &event.session_id {
456            if let Some(session) = self.sessions.get(session_id.as_str()) {
457                if let Some(target) = self.targets.get_mut(session.target_id()) {
458                    return target.on_event(event);
459                }
460            }
461        }
462        let CdpEventMessage { params, method, .. } = event;
463
464        match params {
465            CdpEvent::TargetTargetCreated(ref ev) => self.on_target_created((**ev).clone()),
466            CdpEvent::TargetAttachedToTarget(ref ev) => self.on_attached_to_target(ev.clone()),
467            CdpEvent::TargetTargetDestroyed(ref ev) => self.on_target_destroyed(ev.clone()),
468            CdpEvent::TargetDetachedFromTarget(ref ev) => self.on_detached_from_target(ev.clone()),
469            _ => {}
470        }
471
472        chromiumoxide_cdp::consume_event!(match params {
473            |ev| self.event_listeners.start_send(ev),
474            |json| { let _ = self.event_listeners.try_send_custom(&method, json);}
475        });
476    }
477
478    /// Fired when a new target was created on the chromium instance
479    ///
480    /// Creates a new `Target` instance and keeps track of it
481    fn on_target_created(&mut self, event: EventTargetCreated) {
482        if !self.browser_contexts.is_empty() {
483            if let Some(ref context_id) = event.target_info.browser_context_id {
484                let bc = BrowserContext {
485                    id: Some(context_id.clone()),
486                };
487                if !self.browser_contexts.contains(&bc) {
488                    return;
489                }
490            }
491        }
492        let browser_ctx = event
493            .target_info
494            .browser_context_id
495            .clone()
496            .map(BrowserContext::from)
497            .unwrap_or_else(|| self.default_browser_context.clone());
498        let target = Target::new(
499            event.target_info,
500            TargetConfig {
501                ignore_https_errors: self.config.ignore_https_errors,
502                request_timeout: self.config.request_timeout,
503                viewport: self.config.viewport.clone(),
504                request_intercept: self.config.request_intercept,
505                cache_enabled: self.config.cache_enabled,
506                service_worker_enabled: self.config.service_worker_enabled,
507                ignore_visuals: self.config.ignore_visuals,
508                ignore_stylesheets: self.config.ignore_stylesheets,
509                ignore_javascript: self.config.ignore_javascript,
510                ignore_analytics: self.config.ignore_analytics,
511                ignore_prefetch: self.config.ignore_prefetch,
512                extra_headers: self.config.extra_headers.clone(),
513                only_html: self.config.only_html && self.config.created_first_target,
514                intercept_manager: self.config.intercept_manager,
515                max_bytes_allowed: self.config.max_bytes_allowed,
516                whitelist_patterns: self.config.whitelist_patterns.clone(),
517                blacklist_patterns: self.config.blacklist_patterns.clone(),
518                #[cfg(feature = "adblock")]
519                adblock_filter_rules: self.config.adblock_filter_rules.clone(),
520                page_wake: self.page_wake.clone(),
521            },
522            browser_ctx,
523        );
524
525        self.target_ids.push(target.target_id().clone());
526        self.targets.insert(target.target_id().clone(), target);
527    }
528
529    /// A new session is attached to a target
530    fn on_attached_to_target(&mut self, event: Box<EventAttachedToTarget>) {
531        let session = Session::new(event.session_id.clone(), event.target_info.target_id);
532        if let Some(target) = self.targets.get_mut(session.target_id()) {
533            target.set_session_id(session.session_id().clone())
534        }
535        self.sessions.insert(event.session_id, session);
536    }
537
538    /// The session was detached from target.
539    /// Can be issued multiple times per target if multiple session have been
540    /// attached to it.
541    fn on_detached_from_target(&mut self, event: EventDetachedFromTarget) {
542        // remove the session
543        if let Some(session) = self.sessions.remove(&event.session_id) {
544            if let Some(target) = self.targets.get_mut(session.target_id()) {
545                target.session_id_mut().take();
546            }
547        }
548    }
549
550    /// Fired when the target was destroyed in the browser
551    fn on_target_destroyed(&mut self, event: EventTargetDestroyed) {
552        self.attached_targets.remove(&event.target_id);
553
554        if let Some(target) = self.targets.remove(&event.target_id) {
555            // TODO shutdown?
556            if let Some(session) = target.session_id() {
557                self.sessions.remove(session);
558            }
559        }
560    }
561
562    /// House keeping of commands
563    ///
564    /// Remove all commands where `now` > `timestamp of command starting point +
565    /// request timeout` and notify the senders that their request timed out.
566    fn evict_timed_out_commands(&mut self, now: Instant) {
567        let deadline = match now.checked_sub(self.config.request_timeout) {
568            Some(d) => d,
569            None => return,
570        };
571
572        let timed_out: Vec<_> = self
573            .pending_commands
574            .iter()
575            .filter(|(_, (_, _, timestamp))| *timestamp < deadline)
576            .map(|(k, _)| *k)
577            .collect();
578
579        for call in timed_out {
580            if let Some((req, _, _)) = self.pending_commands.remove(&call) {
581                match req {
582                    PendingRequest::CreateTarget(tx) => {
583                        let _ = tx.send(Err(CdpError::Timeout));
584                    }
585                    PendingRequest::GetTargets(tx) => {
586                        let _ = tx.send(Err(CdpError::Timeout));
587                    }
588                    PendingRequest::Navigate(nav) => {
589                        if let Some(nav) = self.navigations.remove(&nav) {
590                            match nav {
591                                NavigationRequest::Navigate(nav) => {
592                                    let _ = nav.tx.send(Err(CdpError::Timeout));
593                                }
594                            }
595                        }
596                    }
597                    PendingRequest::ExternalCommand(tx) => {
598                        let _ = tx.send(Err(CdpError::Timeout));
599                    }
600                    PendingRequest::InternalCommand(_) => {}
601                    PendingRequest::CloseBrowser(tx) => {
602                        let _ = tx.send(Err(CdpError::Timeout));
603                    }
604                }
605            }
606        }
607    }
608
609    pub fn event_listeners_mut(&mut self) -> &mut EventListeners {
610        &mut self.event_listeners
611    }
612
613    // ------------------------------------------------------------------
614    //  Tokio-native async entry point
615    // ------------------------------------------------------------------
616
617    /// Run the handler as a fully async tokio task.
618    ///
619    /// This is the high-performance alternative to polling `Handler` as a
620    /// `Stream`.  Internally it:
621    ///
622    /// * Splits the WebSocket into independent read/write halves — the
623    ///   writer runs in its own tokio task with natural batching.
624    /// * Uses `tokio::select!` to multiplex the browser channel, page
625    ///   notifications, WebSocket reads, the eviction timer, and writer
626    ///   health.
627    /// * Drains every target's page channel via `try_recv()` (non-blocking)
628    ///   after each event, with an `Arc<Notify>` ensuring the select loop
629    ///   wakes up whenever a page sends a message.
630    ///
631    /// # Usage
632    ///
633    /// ```rust,no_run
634    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
635    /// use chromiumoxide::Browser;
636    /// let (browser, handler) = Browser::launch(Default::default()).await?;
637    /// let handler_task = tokio::spawn(handler.run());
638    /// // … use browser …
639    /// # Ok(())
640    /// # }
641    /// ```
642    pub async fn run(mut self) -> Result<()> {
643        use chromiumoxide_types::Message;
644        use tokio::time::MissedTickBehavior;
645        use tokio_tungstenite::tungstenite::{self, error::ProtocolError};
646
647        // --- set up page notification ---
648        let page_wake = Arc::new(Notify::new());
649        self.page_wake = Some(page_wake.clone());
650
651        // --- split WebSocket ---
652        let conn = self
653            .conn
654            .take()
655            .ok_or_else(|| CdpError::msg("Handler::run() called with no connection"))?;
656        let async_conn = conn.into_async();
657        let mut ws_reader = async_conn.reader;
658        let ws_tx = async_conn.cmd_tx;
659        let mut writer_handle = async_conn.writer_handle;
660        let mut next_call_id = async_conn.next_id;
661
662        // Helper to mint call-ids without &mut self.conn.
663        let mut alloc_call_id = || {
664            let id = chromiumoxide_types::CallId::new(next_call_id);
665            next_call_id = next_call_id.wrapping_add(1);
666            id
667        };
668
669        // --- eviction timer ---
670        let mut evict_timer = tokio::time::interval_at(
671            tokio::time::Instant::now() + self.config.request_timeout,
672            self.config.request_timeout,
673        );
674        evict_timer.set_missed_tick_behavior(MissedTickBehavior::Delay);
675
676        // Helper closure: submit a MethodCall through the WS writer.
677        macro_rules! ws_submit {
678            ($method:expr, $session_id:expr, $params:expr) => {{
679                let id = alloc_call_id();
680                let call = chromiumoxide_types::MethodCall {
681                    id,
682                    method: $method,
683                    session_id: $session_id,
684                    params: $params,
685                };
686                match ws_tx.try_send(call) {
687                    Ok(()) => Ok::<_, CdpError>(id),
688                    Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
689                        tracing::warn!("WS command channel full — dropping command");
690                        Err(CdpError::msg("WS command channel full"))
691                    }
692                    Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
693                        Err(CdpError::msg("WS writer closed"))
694                    }
695                }
696            }};
697        }
698
699        // ---- main event loop ----
700        loop {
701            let now = std::time::Instant::now();
702
703            // 1. Drain all target page channels (non-blocking) & advance
704            //    state machines.
705            for n in (0..self.target_ids.len()).rev() {
706                let target_id = self.target_ids.swap_remove(n);
707
708                if let Some((id, mut target)) = self.targets.remove_entry(&target_id) {
709                    // Drain page channel (non-blocking — waker is the Notify).
710                    {
711                        let mut msgs = Vec::new();
712                        if let Some(handle) = target.page_mut() {
713                            while let Ok(msg) = handle.rx.try_recv() {
714                                msgs.push(msg);
715                            }
716                        }
717                        for msg in msgs {
718                            target.on_page_message(msg);
719                        }
720                    }
721
722                    // Advance target state machine & process events.
723                    while let Some(event) = target.advance(now) {
724                        match event {
725                            TargetEvent::Request(req) => {
726                                if let Ok(call_id) = ws_submit!(
727                                    req.method.clone(),
728                                    req.session_id.map(Into::into),
729                                    req.params
730                                ) {
731                                    self.pending_commands.insert(
732                                        call_id,
733                                        (
734                                            PendingRequest::InternalCommand(
735                                                target.target_id().clone(),
736                                            ),
737                                            req.method,
738                                            now,
739                                        ),
740                                    );
741                                }
742                            }
743                            TargetEvent::Command(msg) => {
744                                if msg.is_navigation() {
745                                    let (req, tx) = msg.split();
746                                    let nav_id = self.next_navigation_id();
747                                    target.goto(FrameRequestedNavigation::new(
748                                        nav_id,
749                                        req.clone(),
750                                        self.config.request_timeout,
751                                    ));
752                                    if let Ok(call_id) = ws_submit!(
753                                        req.method.clone(),
754                                        req.session_id.map(Into::into),
755                                        req.params
756                                    ) {
757                                        self.pending_commands.insert(
758                                            call_id,
759                                            (PendingRequest::Navigate(nav_id), req.method, now),
760                                        );
761                                    }
762                                    self.navigations.insert(
763                                        nav_id,
764                                        NavigationRequest::Navigate(NavigationInProgress::new(tx)),
765                                    );
766                                } else {
767                                    if let Ok(call_id) = ws_submit!(
768                                        msg.method.clone(),
769                                        msg.session_id.map(Into::into),
770                                        msg.params
771                                    ) {
772                                        self.pending_commands.insert(
773                                            call_id,
774                                            (
775                                                PendingRequest::ExternalCommand(msg.sender),
776                                                msg.method,
777                                                now,
778                                            ),
779                                        );
780                                    }
781                                }
782                            }
783                            TargetEvent::NavigationRequest(nav_id, req) => {
784                                if let Ok(call_id) = ws_submit!(
785                                    req.method.clone(),
786                                    req.session_id.map(Into::into),
787                                    req.params
788                                ) {
789                                    self.pending_commands.insert(
790                                        call_id,
791                                        (PendingRequest::Navigate(nav_id), req.method, now),
792                                    );
793                                }
794                            }
795                            TargetEvent::NavigationResult(res) => {
796                                self.on_navigation_lifecycle_completed(res);
797                            }
798                            TargetEvent::BytesConsumed(n) => {
799                                if let Some(rem) = self.remaining_bytes.as_mut() {
800                                    *rem = rem.saturating_sub(n);
801                                    if *rem == 0 {
802                                        self.budget_exhausted = true;
803                                    }
804                                }
805                            }
806                        }
807                    }
808
809                    // Flush event listeners (no Context needed).
810                    target.event_listeners_mut().flush();
811
812                    self.targets.insert(id, target);
813                    self.target_ids.push(target_id);
814                }
815            }
816
817            // Flush handler-level event listeners.
818            self.event_listeners.flush();
819
820            if self.budget_exhausted {
821                for t in self.targets.values_mut() {
822                    t.network_manager.set_block_all(true);
823                }
824            }
825
826            if self.closing {
827                break;
828            }
829
830            // 2. Multiplex all event sources via tokio::select!
831            tokio::select! {
832                msg = self.from_browser.recv() => {
833                    match msg {
834                        Some(msg) => {
835                            match msg {
836                                HandlerMessage::Command(cmd) => {
837                                    if let Ok(call_id) = ws_submit!(
838                                        cmd.method.clone(),
839                                        cmd.session_id.map(Into::into),
840                                        cmd.params
841                                    ) {
842                                        self.pending_commands.insert(
843                                            call_id,
844                                            (PendingRequest::ExternalCommand(cmd.sender), cmd.method, now),
845                                        );
846                                    }
847                                }
848                                HandlerMessage::FetchTargets(tx) => {
849                                    let msg = TARGET_PARAMS_ID.clone();
850                                    if let Ok(call_id) = ws_submit!(msg.0.clone(), None, msg.1) {
851                                        self.pending_commands.insert(
852                                            call_id,
853                                            (PendingRequest::GetTargets(tx), msg.0, now),
854                                        );
855                                    }
856                                }
857                                HandlerMessage::CloseBrowser(tx) => {
858                                    let close_msg = CLOSE_PARAMS_ID.clone();
859                                    if let Ok(call_id) = ws_submit!(close_msg.0.clone(), None, close_msg.1) {
860                                        self.pending_commands.insert(
861                                            call_id,
862                                            (PendingRequest::CloseBrowser(tx), close_msg.0, now),
863                                        );
864                                    }
865                                }
866                                HandlerMessage::CreatePage(params, tx) => {
867                                    if let Some(ref id) = params.browser_context_id {
868                                        self.browser_contexts.insert(BrowserContext::from(id.clone()));
869                                    }
870                                    self.create_page_async(params, tx, &mut alloc_call_id, &ws_tx, now);
871                                }
872                                HandlerMessage::GetPages(tx) => {
873                                    let pages: Vec<_> = self.targets.values_mut()
874                                        .filter(|p| p.is_page())
875                                        .filter_map(|target| target.get_or_create_page())
876                                        .map(|page| Page::from(page.clone()))
877                                        .collect();
878                                    let _ = tx.send(pages);
879                                }
880                                HandlerMessage::InsertContext(ctx) => {
881                                    if self.default_browser_context.id().is_none() {
882                                        self.default_browser_context = ctx.clone();
883                                    }
884                                    self.browser_contexts.insert(ctx);
885                                }
886                                HandlerMessage::DisposeContext(ctx) => {
887                                    self.browser_contexts.remove(&ctx);
888                                    self.attached_targets.retain(|tid| {
889                                        self.targets.get(tid)
890                                            .and_then(|t| t.browser_context_id())
891                                            .map(|id| Some(id) != ctx.id())
892                                            .unwrap_or(true)
893                                    });
894                                    self.closing = true;
895                                }
896                                HandlerMessage::GetPage(target_id, tx) => {
897                                    let page = self.targets.get_mut(&target_id)
898                                        .and_then(|target| target.get_or_create_page())
899                                        .map(|page| Page::from(page.clone()));
900                                    let _ = tx.send(page);
901                                }
902                                HandlerMessage::AddEventListener(req) => {
903                                    self.event_listeners.add_listener(req);
904                                }
905                            }
906                        }
907                        None => break, // browser handle dropped
908                    }
909                }
910
911                frame = ws_reader.next_message() => {
912                    match frame {
913                        Some(Ok(boxed_msg)) => match *boxed_msg {
914                            Message::Response(resp) => {
915                                self.on_response(resp);
916                            }
917                            Message::Event(ev) => {
918                                self.on_event(ev);
919                            }
920                        },
921                        Some(Err(err)) => {
922                            tracing::error!("WS Connection error: {:?}", err);
923                            if let CdpError::Ws(ref ws_error) = err {
924                                match ws_error {
925                                    tungstenite::Error::AlreadyClosed => break,
926                                    tungstenite::Error::Protocol(detail)
927                                        if detail == &ProtocolError::ResetWithoutClosingHandshake =>
928                                    {
929                                        break;
930                                    }
931                                    _ => return Err(err),
932                                }
933                            } else {
934                                return Err(err);
935                            }
936                        }
937                        None => break, // WS closed
938                    }
939                }
940
941                _ = page_wake.notified() => {
942                    // A page sent a message — loop back to drain targets.
943                }
944
945                _ = evict_timer.tick() => {
946                    self.evict_timed_out_commands(now);
947                    for t in self.targets.values_mut() {
948                        t.network_manager.evict_stale_entries(now);
949                        t.frame_manager_mut().evict_stale_context_ids();
950                    }
951                }
952
953                result = &mut writer_handle => {
954                    // WS writer exited — propagate error or break.
955                    match result {
956                        Ok(Ok(())) => break,
957                        Ok(Err(e)) => return Err(e),
958                        Err(e) => return Err(CdpError::msg(format!("WS writer panicked: {e}"))),
959                    }
960                }
961            }
962        }
963
964        writer_handle.abort();
965        Ok(())
966    }
967
968    /// `create_page` variant for the `run()` path that submits via `ws_tx`.
969    fn create_page_async(
970        &mut self,
971        params: CreateTargetParams,
972        tx: OneshotSender<Result<Page>>,
973        alloc_call_id: &mut impl FnMut() -> chromiumoxide_types::CallId,
974        ws_tx: &tokio::sync::mpsc::Sender<chromiumoxide_types::MethodCall>,
975        now: std::time::Instant,
976    ) {
977        let about_blank = params.url == "about:blank";
978        let http_check =
979            !about_blank && params.url.starts_with("http") || params.url.starts_with("file://");
980
981        if about_blank || http_check {
982            let method = params.identifier();
983            match serde_json::to_value(params) {
984                Ok(params) => {
985                    let id = alloc_call_id();
986                    let call = chromiumoxide_types::MethodCall {
987                        id,
988                        method: method.clone(),
989                        session_id: None,
990                        params,
991                    };
992                    match ws_tx.try_send(call) {
993                        Ok(()) => {
994                            self.pending_commands
995                                .insert(id, (PendingRequest::CreateTarget(tx), method, now));
996                        }
997                        Err(_) => {
998                            let _ = tx
999                                .send(Err(CdpError::msg("WS command channel full or closed")))
1000                                .ok();
1001                        }
1002                    }
1003                }
1004                Err(err) => {
1005                    let _ = tx.send(Err(err.into())).ok();
1006                }
1007            }
1008        } else {
1009            let _ = tx.send(Err(CdpError::NotFound)).ok();
1010        }
1011    }
1012}
1013
1014impl Stream for Handler {
1015    type Item = Result<()>;
1016
1017    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
1018        let pin = self.get_mut();
1019
1020        let mut dispose = false;
1021
1022        let now = Instant::now();
1023
1024        loop {
1025            // temporary pinning of the browser receiver should be safe as we are pinning
1026            // through the already pinned self. with the receivers we can also
1027            // safely ignore exhaustion as those are fused.
1028            while let Poll::Ready(Some(msg)) = pin.from_browser.poll_recv(cx) {
1029                match msg {
1030                    HandlerMessage::Command(cmd) => {
1031                        pin.submit_external_command(cmd, now)?;
1032                    }
1033                    HandlerMessage::FetchTargets(tx) => {
1034                        pin.submit_fetch_targets(tx, now);
1035                    }
1036                    HandlerMessage::CloseBrowser(tx) => {
1037                        pin.submit_close(tx, now);
1038                    }
1039                    HandlerMessage::CreatePage(params, tx) => {
1040                        if let Some(ref id) = params.browser_context_id {
1041                            pin.browser_contexts
1042                                .insert(BrowserContext::from(id.clone()));
1043                        }
1044                        pin.create_page(params, tx);
1045                    }
1046                    HandlerMessage::GetPages(tx) => {
1047                        let pages: Vec<_> = pin
1048                            .targets
1049                            .values_mut()
1050                            .filter(|p: &&mut Target| p.is_page())
1051                            .filter_map(|target| target.get_or_create_page())
1052                            .map(|page| Page::from(page.clone()))
1053                            .collect();
1054                        let _ = tx.send(pages);
1055                    }
1056                    HandlerMessage::InsertContext(ctx) => {
1057                        if pin.default_browser_context.id().is_none() {
1058                            pin.default_browser_context = ctx.clone();
1059                        }
1060                        pin.browser_contexts.insert(ctx);
1061                    }
1062                    HandlerMessage::DisposeContext(ctx) => {
1063                        pin.browser_contexts.remove(&ctx);
1064                        pin.attached_targets.retain(|tid| {
1065                            pin.targets
1066                                .get(tid)
1067                                .and_then(|t| t.browser_context_id()) // however you expose it
1068                                .map(|id| Some(id) != ctx.id())
1069                                .unwrap_or(true)
1070                        });
1071                        pin.closing = true;
1072                        dispose = true;
1073                    }
1074                    HandlerMessage::GetPage(target_id, tx) => {
1075                        let page = pin
1076                            .targets
1077                            .get_mut(&target_id)
1078                            .and_then(|target| target.get_or_create_page())
1079                            .map(|page| Page::from(page.clone()));
1080                        let _ = tx.send(page);
1081                    }
1082                    HandlerMessage::AddEventListener(req) => {
1083                        pin.event_listeners.add_listener(req);
1084                    }
1085                }
1086            }
1087
1088            for n in (0..pin.target_ids.len()).rev() {
1089                let target_id = pin.target_ids.swap_remove(n);
1090
1091                if let Some((id, mut target)) = pin.targets.remove_entry(&target_id) {
1092                    while let Some(event) = target.poll(cx, now) {
1093                        match event {
1094                            TargetEvent::Request(req) => {
1095                                let _ = pin.submit_internal_command(
1096                                    target.target_id().clone(),
1097                                    req,
1098                                    now,
1099                                );
1100                            }
1101                            TargetEvent::Command(msg) => {
1102                                pin.on_target_message(&mut target, msg, now);
1103                            }
1104                            TargetEvent::NavigationRequest(id, req) => {
1105                                pin.submit_navigation(id, req, now);
1106                            }
1107                            TargetEvent::NavigationResult(res) => {
1108                                pin.on_navigation_lifecycle_completed(res)
1109                            }
1110                            TargetEvent::BytesConsumed(n) => {
1111                                if let Some(rem) = pin.remaining_bytes.as_mut() {
1112                                    *rem = rem.saturating_sub(n);
1113                                    if *rem == 0 {
1114                                        pin.budget_exhausted = true;
1115                                    }
1116                                }
1117                            }
1118                        }
1119                    }
1120
1121                    // poll the target's event listeners
1122                    target.event_listeners_mut().poll(cx);
1123
1124                    pin.targets.insert(id, target);
1125                    pin.target_ids.push(target_id);
1126                }
1127            }
1128
1129            // poll the handler-level event listeners once per iteration,
1130            // not once per target.
1131            pin.event_listeners_mut().poll(cx);
1132
1133            let mut done = true;
1134
1135            // Read WS messages into a temporary buffer so the conn borrow
1136            // is released before we process them (which needs &mut pin).
1137            let mut ws_msgs = Vec::new();
1138            let mut ws_err = None;
1139            {
1140                let Some(conn) = pin.conn.as_mut() else {
1141                    return Poll::Ready(Some(Err(CdpError::msg(
1142                        "connection consumed by Handler::run()",
1143                    ))));
1144                };
1145                while let Poll::Ready(Some(ev)) = Pin::new(&mut *conn).poll_next(cx) {
1146                    match ev {
1147                        Ok(msg) => ws_msgs.push(msg),
1148                        Err(err) => {
1149                            ws_err = Some(err);
1150                            break;
1151                        }
1152                    }
1153                }
1154            }
1155
1156            for boxed_msg in ws_msgs {
1157                match *boxed_msg {
1158                    Message::Response(resp) => {
1159                        pin.on_response(resp);
1160                        if pin.closing {
1161                            return Poll::Ready(None);
1162                        }
1163                    }
1164                    Message::Event(ev) => {
1165                        pin.on_event(ev);
1166                    }
1167                }
1168                done = false;
1169            }
1170
1171            if let Some(err) = ws_err {
1172                tracing::error!("WS Connection error: {:?}", err);
1173                if let CdpError::Ws(ref ws_error) = err {
1174                    match ws_error {
1175                        Error::AlreadyClosed => {
1176                            pin.closing = true;
1177                            dispose = true;
1178                        }
1179                        Error::Protocol(detail)
1180                            if detail == &ProtocolError::ResetWithoutClosingHandshake =>
1181                        {
1182                            pin.closing = true;
1183                            dispose = true;
1184                        }
1185                        _ => return Poll::Ready(Some(Err(err))),
1186                    }
1187                } else {
1188                    return Poll::Ready(Some(Err(err)));
1189                }
1190            }
1191
1192            if pin.evict_command_timeout.poll_ready(cx) {
1193                // evict all commands that timed out
1194                pin.evict_timed_out_commands(now);
1195                // evict stale network race-condition buffers and
1196                // orphaned context_ids / frame entries
1197                for t in pin.targets.values_mut() {
1198                    t.network_manager.evict_stale_entries(now);
1199                    t.frame_manager_mut().evict_stale_context_ids();
1200                }
1201            }
1202
1203            if pin.budget_exhausted {
1204                for t in pin.targets.values_mut() {
1205                    t.network_manager.set_block_all(true);
1206                }
1207            }
1208
1209            if dispose {
1210                return Poll::Ready(None);
1211            }
1212
1213            if done {
1214                // no events/responses were read from the websocket
1215                return Poll::Pending;
1216            }
1217        }
1218    }
1219}
1220
1221/// How to configure the handler
1222#[derive(Debug, Clone)]
1223pub struct HandlerConfig {
1224    /// Whether the `NetworkManager`s should ignore https errors
1225    pub ignore_https_errors: bool,
1226    /// Window and device settings
1227    pub viewport: Option<Viewport>,
1228    /// Context ids to set from the get go
1229    pub context_ids: Vec<BrowserContextId>,
1230    /// default request timeout to use
1231    pub request_timeout: Duration,
1232    /// Whether to enable request interception
1233    pub request_intercept: bool,
1234    /// Whether to enable cache
1235    pub cache_enabled: bool,
1236    /// Whether to enable Service Workers
1237    pub service_worker_enabled: bool,
1238    /// Whether to ignore visuals.
1239    pub ignore_visuals: bool,
1240    /// Whether to ignore stylesheets.
1241    pub ignore_stylesheets: bool,
1242    /// Whether to ignore Javascript only allowing critical framework or lib based rendering.
1243    pub ignore_javascript: bool,
1244    /// Whether to ignore analytics.
1245    pub ignore_analytics: bool,
1246    /// Ignore prefetch request. Defaults to true.
1247    pub ignore_prefetch: bool,
1248    /// Whether to ignore ads.
1249    pub ignore_ads: bool,
1250    /// Extra headers.
1251    pub extra_headers: Option<std::collections::HashMap<String, String>>,
1252    /// Only Html.
1253    pub only_html: bool,
1254    /// Created the first target.
1255    pub created_first_target: bool,
1256    /// The network intercept manager.
1257    pub intercept_manager: NetworkInterceptManager,
1258    /// The max bytes to receive.
1259    pub max_bytes_allowed: Option<u64>,
1260    /// Optional per-run/per-site whitelist of URL substrings (scripts/resources).
1261    pub whitelist_patterns: Option<Vec<String>>,
1262    /// Optional per-run/per-site blacklist of URL substrings (scripts/resources).
1263    pub blacklist_patterns: Option<Vec<String>>,
1264    /// Extra ABP/uBO filter rules for the adblock engine.
1265    #[cfg(feature = "adblock")]
1266    pub adblock_filter_rules: Option<Vec<String>>,
1267    /// Capacity of the channel between browser handle and handler.
1268    /// Defaults to 1000.
1269    pub channel_capacity: usize,
1270    /// Number of WebSocket connection retry attempts with exponential backoff.
1271    /// Defaults to 4.
1272    pub connection_retries: u32,
1273}
1274
1275impl Default for HandlerConfig {
1276    fn default() -> Self {
1277        Self {
1278            ignore_https_errors: true,
1279            viewport: Default::default(),
1280            context_ids: Vec::new(),
1281            request_timeout: Duration::from_millis(REQUEST_TIMEOUT),
1282            request_intercept: false,
1283            cache_enabled: true,
1284            service_worker_enabled: true,
1285            ignore_visuals: false,
1286            ignore_stylesheets: false,
1287            ignore_ads: false,
1288            ignore_javascript: false,
1289            ignore_analytics: true,
1290            ignore_prefetch: true,
1291            only_html: false,
1292            extra_headers: Default::default(),
1293            created_first_target: false,
1294            intercept_manager: NetworkInterceptManager::Unknown,
1295            max_bytes_allowed: None,
1296            whitelist_patterns: None,
1297            blacklist_patterns: None,
1298            #[cfg(feature = "adblock")]
1299            adblock_filter_rules: None,
1300            channel_capacity: 4096,
1301            connection_retries: crate::conn::DEFAULT_CONNECTION_RETRIES,
1302        }
1303    }
1304}
1305
1306/// Wraps the sender half of the channel who requested a navigation
1307#[derive(Debug)]
1308pub struct NavigationInProgress<T> {
1309    /// Marker to indicate whether a navigation lifecycle has completed
1310    navigated: bool,
1311    /// The response of the issued navigation request
1312    response: Option<Response>,
1313    /// Sender who initiated the navigation request
1314    tx: OneshotSender<T>,
1315}
1316
1317impl<T> NavigationInProgress<T> {
1318    fn new(tx: OneshotSender<T>) -> Self {
1319        Self {
1320            navigated: false,
1321            response: None,
1322            tx,
1323        }
1324    }
1325
1326    /// The response to the cdp request has arrived
1327    fn set_response(&mut self, resp: Response) {
1328        self.response = Some(resp);
1329    }
1330
1331    /// The navigation process has finished, the page finished loading.
1332    fn set_navigated(&mut self) {
1333        self.navigated = true;
1334    }
1335}
1336
1337/// Request type for navigation
1338#[derive(Debug)]
1339enum NavigationRequest {
1340    /// Represents a simple `NavigateParams` ("Page.navigate")
1341    Navigate(NavigationInProgress<Result<Response>>),
1342    // TODO are there more?
1343}
1344
1345/// Different kind of submitted request submitted from the  `Handler` to the
1346/// `Connection` and being waited on for the response.
1347#[derive(Debug)]
1348enum PendingRequest {
1349    /// A Request to create a new `Target` that results in the creation of a
1350    /// `Page` that represents a browser page.
1351    CreateTarget(OneshotSender<Result<Page>>),
1352    /// A Request to fetch old `Target`s created before connection
1353    GetTargets(OneshotSender<Result<Vec<TargetInfo>>>),
1354    /// A Request to navigate a specific `Target`.
1355    ///
1356    /// Navigation requests are not automatically completed once the response to
1357    /// the raw cdp navigation request (like `NavigateParams`) arrives, but only
1358    /// after the `Target` notifies the `Handler` that the `Page` has finished
1359    /// loading, which comes after the response.
1360    Navigate(NavigationId),
1361    /// A common request received via a channel (`Page`).
1362    ExternalCommand(OneshotSender<Result<Response>>),
1363    /// Requests that are initiated directly from a `Target` (all the
1364    /// initialization commands).
1365    InternalCommand(TargetId),
1366    // A Request to close the browser.
1367    CloseBrowser(OneshotSender<Result<CloseReturns>>),
1368}
1369
1370/// Events used internally to communicate with the handler, which are executed
1371/// in the background
1372// TODO rename to BrowserMessage
1373#[derive(Debug)]
1374pub(crate) enum HandlerMessage {
1375    CreatePage(CreateTargetParams, OneshotSender<Result<Page>>),
1376    FetchTargets(OneshotSender<Result<Vec<TargetInfo>>>),
1377    InsertContext(BrowserContext),
1378    DisposeContext(BrowserContext),
1379    GetPages(OneshotSender<Vec<Page>>),
1380    Command(CommandMessage),
1381    GetPage(TargetId, OneshotSender<Option<Page>>),
1382    AddEventListener(EventListenerRequest),
1383    CloseBrowser(OneshotSender<Result<CloseReturns>>),
1384}
1385
1386#[cfg(test)]
1387mod tests {
1388    use super::*;
1389    use chromiumoxide_cdp::cdp::browser_protocol::target::{AttachToTargetReturns, TargetInfo};
1390
1391    #[test]
1392    fn attach_to_target_response_sets_session_id_before_event_arrives() {
1393        let info = TargetInfo::builder()
1394            .target_id("target-1".to_string())
1395            .r#type("page")
1396            .title("")
1397            .url("about:blank")
1398            .attached(false)
1399            .can_access_opener(false)
1400            .build()
1401            .expect("target info");
1402        let mut target = Target::new(info, TargetConfig::default(), BrowserContext::default());
1403        let method: MethodId = AttachToTargetParams::IDENTIFIER.into();
1404        let result = serde_json::to_value(AttachToTargetReturns::new("session-1".to_string()))
1405            .expect("attach result");
1406        let resp = Response {
1407            id: CallId::new(1),
1408            result: Some(result),
1409            error: None,
1410        };
1411
1412        maybe_store_attach_session_id(&mut target, &method, &resp);
1413
1414        assert_eq!(
1415            target.session_id().map(AsRef::as_ref),
1416            Some("session-1"),
1417            "attach response should seed the flat session id even before Target.attachedToTarget"
1418        );
1419    }
1420}