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::channel::mpsc::Receiver;
10use futures::channel::oneshot::Sender as OneshotSender;
11use futures::stream::{Fuse, Stream, StreamExt};
12use futures::task::{Context, Poll};
13use hashbrown::{HashMap, HashSet};
14pub(crate) use page::PageInner;
15use spider_network_blocker::intercept_manager::NetworkInterceptManager;
16use std::pin::Pin;
17use std::time::{Duration, Instant};
18use tokio_tungstenite::tungstenite::error::ProtocolError;
19use tokio_tungstenite::tungstenite::Error;
20
21use crate::cmd::{to_command_response, CommandMessage};
22use crate::conn::Connection;
23use crate::error::{CdpError, Result};
24use crate::handler::browser::BrowserContext;
25use crate::handler::frame::FrameRequestedNavigation;
26use crate::handler::frame::{NavigationError, NavigationId, NavigationOk};
27use crate::handler::job::PeriodicJob;
28use crate::handler::session::Session;
29use crate::handler::target::TargetEvent;
30use crate::handler::target::{Target, TargetConfig};
31use crate::handler::viewport::Viewport;
32use crate::page::Page;
33
34pub const REQUEST_TIMEOUT: u64 = 30_000;
36
37pub mod blockers;
38pub mod browser;
39pub mod commandfuture;
40pub mod domworld;
41pub mod emulation;
42pub mod frame;
43pub mod http;
44pub mod httpfuture;
45mod job;
46pub mod network;
47mod page;
48mod session;
49pub mod target;
50pub mod target_message_future;
51pub mod viewport;
52
53#[must_use = "streams do nothing unless polled"]
56#[derive(Debug)]
57pub struct Handler {
58 pub default_browser_context: BrowserContext,
59 pub browser_contexts: HashSet<BrowserContext>,
60 pending_commands: FnvHashMap<CallId, (PendingRequest, MethodId, Instant)>,
64 from_browser: Fuse<Receiver<HandlerMessage>>,
66 target_ids: Vec<TargetId>,
68 targets: HashMap<TargetId, Target>,
70 navigations: FnvHashMap<NavigationId, NavigationRequest>,
72 sessions: HashMap<SessionId, Session>,
76 conn: Connection<CdpEventMessage>,
78 evict_command_timeout: PeriodicJob,
80 next_navigation_id: usize,
82 config: HandlerConfig,
84 event_listeners: EventListeners,
86 closing: bool,
88 remaining_bytes: Option<u64>,
90 budget_exhausted: bool,
92}
93
94lazy_static::lazy_static! {
95 static ref DISCOVER_ID: (std::borrow::Cow<'static, str>, serde_json::Value) = {
97 let discover = SetDiscoverTargetsParams::new(true);
98 (discover.identifier(), serde_json::to_value(discover).expect("valid discover target params"))
99 };
100 static ref TARGET_PARAMS_ID: (std::borrow::Cow<'static, str>, serde_json::Value) = {
102 let msg = GetTargetsParams { filter: None };
103 (msg.identifier(), serde_json::to_value(msg).expect("valid paramtarget"))
104 };
105 static ref CLOSE_PARAMS_ID: (std::borrow::Cow<'static, str>, serde_json::Value) = {
107 let close_msg = CloseParams::default();
108 (close_msg.identifier(), serde_json::to_value(close_msg).expect("valid close params"))
109 };
110}
111
112impl Handler {
113 pub(crate) fn new(
116 mut conn: Connection<CdpEventMessage>,
117 rx: Receiver<HandlerMessage>,
118 config: HandlerConfig,
119 ) -> Self {
120 let discover = DISCOVER_ID.clone();
121 let _ = conn.submit_command(discover.0, None, discover.1);
122
123 let browser_contexts = config
124 .context_ids
125 .iter()
126 .map(|id| BrowserContext::from(id.clone()))
127 .collect();
128
129 Self {
130 pending_commands: Default::default(),
131 from_browser: rx.fuse(),
132 default_browser_context: Default::default(),
133 browser_contexts,
134 target_ids: Default::default(),
135 targets: Default::default(),
136 navigations: Default::default(),
137 sessions: Default::default(),
138 conn,
139 evict_command_timeout: PeriodicJob::new(config.request_timeout),
140 next_navigation_id: 0,
141 config,
142 event_listeners: Default::default(),
143 closing: false,
144 remaining_bytes: None,
145 budget_exhausted: false,
146 }
147 }
148
149 pub fn get_target(&self, target_id: &TargetId) -> Option<&Target> {
151 self.targets.get(target_id)
152 }
153
154 pub fn targets(&self) -> impl Iterator<Item = &Target> + '_ {
156 self.targets.values()
157 }
158
159 pub fn default_browser_context(&self) -> &BrowserContext {
161 &self.default_browser_context
162 }
163
164 pub fn browser_contexts(&self) -> impl Iterator<Item = &BrowserContext> + '_ {
166 self.browser_contexts.iter()
167 }
168
169 fn on_navigation_response(&mut self, id: NavigationId, resp: Response) {
171 if let Some(nav) = self.navigations.remove(&id) {
172 match nav {
173 NavigationRequest::Navigate(mut nav) => {
174 if nav.navigated {
175 let _ = nav.tx.send(Ok(resp));
176 } else {
177 nav.set_response(resp);
178 self.navigations
179 .insert(id, NavigationRequest::Navigate(nav));
180 }
181 }
182 }
183 }
184 }
185
186 fn on_navigation_lifecycle_completed(&mut self, res: Result<NavigationOk, NavigationError>) {
188 match res {
189 Ok(ok) => {
190 let id = *ok.navigation_id();
191 if let Some(nav) = self.navigations.remove(&id) {
192 match nav {
193 NavigationRequest::Navigate(mut nav) => {
194 if let Some(resp) = nav.response.take() {
195 let _ = nav.tx.send(Ok(resp));
196 } else {
197 nav.set_navigated();
198 self.navigations
199 .insert(id, NavigationRequest::Navigate(nav));
200 }
201 }
202 }
203 }
204 }
205 Err(err) => {
206 if let Some(nav) = self.navigations.remove(err.navigation_id()) {
207 match nav {
208 NavigationRequest::Navigate(nav) => {
209 let _ = nav.tx.send(Err(err.into()));
210 }
211 }
212 }
213 }
214 }
215 }
216
217 fn on_response(&mut self, resp: Response) {
219 if let Some((req, method, _)) = self.pending_commands.remove(&resp.id) {
220 match req {
221 PendingRequest::CreateTarget(tx) => {
222 match to_command_response::<CreateTargetParams>(resp, method) {
223 Ok(resp) => {
224 if let Some(target) = self.targets.get_mut(&resp.target_id) {
225 target.set_initiator(tx);
228 }
229 }
230 Err(err) => {
231 let _ = tx.send(Err(err)).ok();
232 }
233 }
234 }
235 PendingRequest::GetTargets(tx) => {
236 match to_command_response::<GetTargetsParams>(resp, method) {
237 Ok(resp) => {
238 let targets: Vec<TargetInfo> = resp.result.target_infos;
239 let results = targets.clone();
240 for target_info in targets {
241 let target_id = target_info.target_id.clone();
242 let event: EventTargetCreated = EventTargetCreated { target_info };
243 self.on_target_created(event);
244 let attach = AttachToTargetParams::new(target_id);
245 let method = attach.identifier();
246
247 if let Ok(params) = serde_json::to_value(attach) {
248 let _ = self.conn.submit_command(method, None, params);
249 }
250 }
251
252 let _ = tx.send(Ok(results)).ok();
253 }
254 Err(err) => {
255 let _ = tx.send(Err(err)).ok();
256 }
257 }
258 }
259 PendingRequest::Navigate(id) => {
260 self.on_navigation_response(id, resp);
261 if self.config.only_html && !self.config.created_first_target {
262 self.config.created_first_target = true;
263 }
264 }
265 PendingRequest::ExternalCommand(tx) => {
266 let _ = tx.send(Ok(resp)).ok();
267 }
268 PendingRequest::InternalCommand(target_id) => {
269 if let Some(target) = self.targets.get_mut(&target_id) {
270 target.on_response(resp, method.as_ref());
271 }
272 }
273 PendingRequest::CloseBrowser(tx) => {
274 self.closing = true;
275 let _ = tx.send(Ok(CloseReturns {})).ok();
276 }
277 }
278 }
279 }
280
281 pub(crate) fn submit_external_command(
283 &mut self,
284 msg: CommandMessage,
285 now: Instant,
286 ) -> Result<()> {
287 let call_id = self
288 .conn
289 .submit_command(msg.method.clone(), msg.session_id, msg.params)?;
290 self.pending_commands.insert(
291 call_id,
292 (PendingRequest::ExternalCommand(msg.sender), msg.method, now),
293 );
294 Ok(())
295 }
296
297 pub(crate) fn submit_internal_command(
298 &mut self,
299 target_id: TargetId,
300 req: CdpRequest,
301 now: Instant,
302 ) -> Result<()> {
303 let call_id = self.conn.submit_command(
304 req.method.clone(),
305 req.session_id.map(Into::into),
306 req.params,
307 )?;
308 self.pending_commands.insert(
309 call_id,
310 (PendingRequest::InternalCommand(target_id), req.method, now),
311 );
312 Ok(())
313 }
314
315 fn submit_fetch_targets(&mut self, tx: OneshotSender<Result<Vec<TargetInfo>>>, now: Instant) {
316 let msg = TARGET_PARAMS_ID.clone();
317
318 if let Ok(call_id) = self.conn.submit_command(msg.0.clone(), None, msg.1) {
319 self.pending_commands
320 .insert(call_id, (PendingRequest::GetTargets(tx), msg.0, now));
321 }
322 }
323
324 fn submit_navigation(&mut self, id: NavigationId, req: CdpRequest, now: Instant) {
327 if let Ok(call_id) = self.conn.submit_command(
328 req.method.clone(),
329 req.session_id.map(Into::into),
330 req.params,
331 ) {
332 self.pending_commands
333 .insert(call_id, (PendingRequest::Navigate(id), req.method, now));
334 }
335 }
336
337 fn submit_close(&mut self, tx: OneshotSender<Result<CloseReturns>>, now: Instant) {
338 let close_msg = CLOSE_PARAMS_ID.clone();
339
340 if let Ok(call_id) = self
341 .conn
342 .submit_command(close_msg.0.clone(), None, close_msg.1)
343 {
344 self.pending_commands.insert(
345 call_id,
346 (PendingRequest::CloseBrowser(tx), close_msg.0, now),
347 );
348 }
349 }
350
351 fn on_target_message(&mut self, target: &mut Target, msg: CommandMessage, now: Instant) {
353 if msg.is_navigation() {
354 let (req, tx) = msg.split();
355 let id = self.next_navigation_id();
356
357 target.goto(FrameRequestedNavigation::new(id, req));
358
359 self.navigations.insert(
360 id,
361 NavigationRequest::Navigate(NavigationInProgress::new(tx)),
362 );
363 } else {
364 let _ = self.submit_external_command(msg, now);
365 }
366 }
367
368 fn next_navigation_id(&mut self) -> NavigationId {
370 let id = NavigationId(self.next_navigation_id);
371 self.next_navigation_id = self.next_navigation_id.wrapping_add(1);
372 id
373 }
374
375 fn create_page(&mut self, params: CreateTargetParams, tx: OneshotSender<Result<Page>>) {
386 let about_blank = params.url == "about:blank";
387 let http_check =
388 !about_blank && params.url.starts_with("http") || params.url.starts_with("file://");
389
390 if about_blank || http_check {
391 let method = params.identifier();
392
393 match serde_json::to_value(params) {
394 Ok(params) => match self.conn.submit_command(method.clone(), None, params) {
395 Ok(call_id) => {
396 self.pending_commands.insert(
397 call_id,
398 (PendingRequest::CreateTarget(tx), method, Instant::now()),
399 );
400 }
401 Err(err) => {
402 let _ = tx.send(Err(err.into())).ok();
403 }
404 },
405 Err(err) => {
406 let _ = tx.send(Err(err.into())).ok();
407 }
408 }
409 } else {
410 let _ = tx.send(Err(CdpError::NotFound)).ok();
411 }
412 }
413
414 fn on_event(&mut self, event: CdpEventMessage) {
416 if let Some(ref session_id) = event.session_id {
417 if let Some(session) = self.sessions.get(session_id.as_str()) {
418 if let Some(target) = self.targets.get_mut(session.target_id()) {
419 return target.on_event(event);
420 }
421 }
422 }
423 let CdpEventMessage { params, method, .. } = event;
424
425 match params {
426 CdpEvent::TargetTargetCreated(ref ev) => self.on_target_created(ev.clone()),
427 CdpEvent::TargetAttachedToTarget(ref ev) => self.on_attached_to_target(ev.clone()),
428 CdpEvent::TargetTargetDestroyed(ref ev) => self.on_target_destroyed(ev.clone()),
429 CdpEvent::TargetDetachedFromTarget(ref ev) => self.on_detached_from_target(ev.clone()),
430 _ => {}
431 }
432
433 chromiumoxide_cdp::consume_event!(match params {
434 |ev| self.event_listeners.start_send(ev),
435 |json| { let _ = self.event_listeners.try_send_custom(&method, json);}
436 });
437 }
438
439 fn on_target_created(&mut self, event: EventTargetCreated) {
443 let browser_ctx = match event.target_info.browser_context_id {
444 Some(ref context_id) => {
445 let browser_context = BrowserContext {
446 id: Some(context_id.clone()),
447 };
448 if self.default_browser_context.id.is_none() {
449 self.default_browser_context = browser_context.clone();
450 };
451 self.browser_contexts.insert(browser_context.clone());
452
453 browser_context
454 }
455 _ => event
456 .target_info
457 .browser_context_id
458 .clone()
459 .map(BrowserContext::from)
460 .filter(|id| self.browser_contexts.contains(id))
461 .unwrap_or_else(|| self.default_browser_context.clone()),
462 };
463
464 let target = Target::new(
465 event.target_info,
466 TargetConfig {
467 ignore_https_errors: self.config.ignore_https_errors,
468 request_timeout: self.config.request_timeout,
469 viewport: self.config.viewport.clone(),
470 request_intercept: self.config.request_intercept,
471 cache_enabled: self.config.cache_enabled,
472 service_worker_enabled: self.config.service_worker_enabled,
473 ignore_visuals: self.config.ignore_visuals,
474 ignore_stylesheets: self.config.ignore_stylesheets,
475 ignore_javascript: self.config.ignore_javascript,
476 ignore_analytics: self.config.ignore_analytics,
477 extra_headers: self.config.extra_headers.clone(),
478 only_html: self.config.only_html && self.config.created_first_target,
479 intercept_manager: self.config.intercept_manager,
480 max_bytes_allowed: self.config.max_bytes_allowed,
481 },
482 browser_ctx,
483 );
484
485 self.target_ids.push(target.target_id().clone());
486 self.targets.insert(target.target_id().clone(), target);
487 }
488
489 fn on_attached_to_target(&mut self, event: Box<EventAttachedToTarget>) {
491 let session = Session::new(event.session_id.clone(), event.target_info.target_id);
492 if let Some(target) = self.targets.get_mut(session.target_id()) {
493 target.set_session_id(session.session_id().clone())
494 }
495 self.sessions.insert(event.session_id, session);
496 }
497
498 fn on_detached_from_target(&mut self, event: EventDetachedFromTarget) {
502 if let Some(session) = self.sessions.remove(&event.session_id) {
504 if let Some(target) = self.targets.get_mut(session.target_id()) {
505 target.session_id().take();
506 }
507 }
508 }
509
510 fn on_target_destroyed(&mut self, event: EventTargetDestroyed) {
512 if let Some(target) = self.targets.remove(&event.target_id) {
513 if let Some(session) = target.session_id() {
515 self.sessions.remove(session);
516 }
517 }
518 }
519
520 fn evict_timed_out_commands(&mut self, now: Instant) {
525 let timed_out = self
526 .pending_commands
527 .iter()
528 .filter(|(_, (_, _, timestamp))| now > (*timestamp + self.config.request_timeout))
529 .map(|(k, _)| *k)
530 .collect::<Vec<_>>();
531
532 for call in timed_out {
533 if let Some((req, _, _)) = self.pending_commands.remove(&call) {
534 match req {
535 PendingRequest::CreateTarget(tx) => {
536 let _ = tx.send(Err(CdpError::Timeout));
537 }
538 PendingRequest::GetTargets(tx) => {
539 let _ = tx.send(Err(CdpError::Timeout));
540 }
541 PendingRequest::Navigate(nav) => {
542 if let Some(nav) = self.navigations.remove(&nav) {
543 match nav {
544 NavigationRequest::Navigate(nav) => {
545 let _ = nav.tx.send(Err(CdpError::Timeout));
546 }
547 }
548 }
549 }
550 PendingRequest::ExternalCommand(tx) => {
551 let _ = tx.send(Err(CdpError::Timeout));
552 }
553 PendingRequest::InternalCommand(_) => {}
554 PendingRequest::CloseBrowser(tx) => {
555 let _ = tx.send(Err(CdpError::Timeout));
556 }
557 }
558 }
559 }
560 }
561
562 pub fn event_listeners_mut(&mut self) -> &mut EventListeners {
563 &mut self.event_listeners
564 }
565}
566
567impl Stream for Handler {
568 type Item = Result<()>;
569
570 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
571 let pin = self.get_mut();
572
573 let mut dispose = false;
574
575 loop {
576 let now = Instant::now();
577 while let Poll::Ready(Some(msg)) = Pin::new(&mut pin.from_browser).poll_next(cx) {
581 match msg {
582 HandlerMessage::Command(cmd) => {
583 pin.submit_external_command(cmd, now)?;
584 }
585 HandlerMessage::FetchTargets(tx) => {
586 pin.submit_fetch_targets(tx, now);
587 }
588 HandlerMessage::CloseBrowser(tx) => {
589 pin.submit_close(tx, now);
590 }
591 HandlerMessage::CreatePage(params, tx) => {
592 pin.create_page(params, tx);
593 }
594 HandlerMessage::GetPages(tx) => {
595 let pages: Vec<_> = pin
596 .targets
597 .values_mut()
598 .filter(|p: &&mut Target| p.is_page())
599 .filter_map(|target| target.get_or_create_page())
600 .map(|page| Page::from(page.clone()))
601 .collect();
602 let _ = tx.send(pages);
603 }
604 HandlerMessage::InsertContext(ctx) => {
605 pin.default_browser_context = ctx.clone();
606 pin.browser_contexts.insert(ctx);
607 }
608 HandlerMessage::DisposeContext(ctx) => {
609 pin.browser_contexts.remove(&ctx);
610 pin.closing = true;
611 dispose = true;
612 }
613 HandlerMessage::GetPage(target_id, tx) => {
614 let page = pin
615 .targets
616 .get_mut(&target_id)
617 .and_then(|target| target.get_or_create_page())
618 .map(|page| Page::from(page.clone()));
619 let _ = tx.send(page);
620 }
621 HandlerMessage::AddEventListener(req) => {
622 pin.event_listeners.add_listener(req);
623 }
624 }
625 }
626
627 for n in (0..pin.target_ids.len()).rev() {
628 let target_id = pin.target_ids.swap_remove(n);
629
630 if let Some((id, mut target)) = pin.targets.remove_entry(&target_id) {
631 while let Some(event) = target.poll(cx, now) {
632 match event {
633 TargetEvent::Request(req) => {
634 let _ = pin.submit_internal_command(
635 target.target_id().clone(),
636 req,
637 now,
638 );
639 }
640 TargetEvent::Command(msg) => {
641 pin.on_target_message(&mut target, msg, now);
642 }
643 TargetEvent::NavigationRequest(id, req) => {
644 pin.submit_navigation(id, req, now);
645 }
646 TargetEvent::NavigationResult(res) => {
647 pin.on_navigation_lifecycle_completed(res)
648 }
649 TargetEvent::BytesConsumed(n) => {
650 if let Some(rem) = pin.remaining_bytes.as_mut() {
651 *rem = rem.saturating_sub(n);
652 if *rem == 0 {
653 pin.budget_exhausted = true;
654 }
655 }
656 }
657 }
658 }
659
660 target.event_listeners_mut().poll(cx);
662 pin.event_listeners_mut().poll(cx);
664
665 pin.targets.insert(id, target);
666 pin.target_ids.push(target_id);
667 }
668 }
669
670 let mut done = true;
671
672 while let Poll::Ready(Some(ev)) = Pin::new(&mut pin.conn).poll_next(cx) {
673 match ev {
674 Ok(boxed_msg) => match *boxed_msg {
675 Message::Response(resp) => {
676 pin.on_response(resp);
677 if pin.closing {
678 return Poll::Ready(None);
679 }
680 }
681 Message::Event(ev) => {
682 pin.on_event(ev);
683 }
684 },
685 Err(err) => {
686 tracing::error!("WS Connection error: {:?}", err);
687 match err {
688 CdpError::Ws(ref ws_error) => match ws_error {
689 Error::AlreadyClosed => {
690 pin.closing = true;
691 dispose = true;
692 break;
693 }
694 Error::Protocol(detail)
695 if detail == &ProtocolError::ResetWithoutClosingHandshake =>
696 {
697 pin.closing = true;
698 dispose = true;
699 break;
700 }
701 _ => {}
702 },
703 _ => {}
704 };
705 return Poll::Ready(Some(Err(err)));
706 }
707 }
708 done = false;
709 }
710
711 if pin.evict_command_timeout.poll_ready(cx) {
712 pin.evict_timed_out_commands(now);
714 }
715
716 if pin.budget_exhausted {
717 for t in pin.targets.values_mut() {
718 t.network_manager.set_block_all(true);
719 }
720 }
721
722 if dispose {
723 return Poll::Ready(None);
724 }
725
726 if done {
727 return Poll::Pending;
729 }
730 }
731 }
732}
733
734#[derive(Debug, Clone)]
736pub struct HandlerConfig {
737 pub ignore_https_errors: bool,
739 pub viewport: Option<Viewport>,
741 pub context_ids: Vec<BrowserContextId>,
743 pub request_timeout: Duration,
745 pub request_intercept: bool,
747 pub cache_enabled: bool,
749 pub service_worker_enabled: bool,
751 pub ignore_visuals: bool,
753 pub ignore_stylesheets: bool,
755 pub ignore_javascript: bool,
757 pub ignore_analytics: bool,
759 pub ignore_ads: bool,
761 pub extra_headers: Option<std::collections::HashMap<String, String>>,
763 pub only_html: bool,
765 pub created_first_target: bool,
767 pub intercept_manager: NetworkInterceptManager,
769 pub max_bytes_allowed: Option<u64>,
771}
772
773impl Default for HandlerConfig {
774 fn default() -> Self {
775 Self {
776 ignore_https_errors: true,
777 viewport: Default::default(),
778 context_ids: Vec::new(),
779 request_timeout: Duration::from_millis(REQUEST_TIMEOUT),
780 request_intercept: false,
781 cache_enabled: true,
782 service_worker_enabled: true,
783 ignore_visuals: false,
784 ignore_stylesheets: false,
785 ignore_ads: false,
786 ignore_javascript: false,
787 ignore_analytics: true,
788 only_html: false,
789 extra_headers: Default::default(),
790 created_first_target: false,
791 intercept_manager: NetworkInterceptManager::Unknown,
792 max_bytes_allowed: None,
793 }
794 }
795}
796
797#[derive(Debug)]
799pub struct NavigationInProgress<T> {
800 navigated: bool,
802 response: Option<Response>,
804 tx: OneshotSender<T>,
806}
807
808impl<T> NavigationInProgress<T> {
809 fn new(tx: OneshotSender<T>) -> Self {
810 Self {
811 navigated: false,
812 response: None,
813 tx,
814 }
815 }
816
817 fn set_response(&mut self, resp: Response) {
819 self.response = Some(resp);
820 }
821
822 fn set_navigated(&mut self) {
824 self.navigated = true;
825 }
826}
827
828#[derive(Debug)]
830enum NavigationRequest {
831 Navigate(NavigationInProgress<Result<Response>>),
833 }
835
836#[derive(Debug)]
839enum PendingRequest {
840 CreateTarget(OneshotSender<Result<Page>>),
843 GetTargets(OneshotSender<Result<Vec<TargetInfo>>>),
845 Navigate(NavigationId),
852 ExternalCommand(OneshotSender<Result<Response>>),
854 InternalCommand(TargetId),
857 CloseBrowser(OneshotSender<Result<CloseReturns>>),
859}
860
861#[derive(Debug)]
865pub(crate) enum HandlerMessage {
866 CreatePage(CreateTargetParams, OneshotSender<Result<Page>>),
867 FetchTargets(OneshotSender<Result<Vec<TargetInfo>>>),
868 InsertContext(BrowserContext),
869 DisposeContext(BrowserContext),
870 GetPages(OneshotSender<Vec<Page>>),
871 Command(CommandMessage),
872 GetPage(TargetId, OneshotSender<Option<Page>>),
873 AddEventListener(EventListenerRequest),
874 CloseBrowser(OneshotSender<Result<CloseReturns>>),
875}