night_fury_core/session/mod.rs
1use std::time::Duration;
2
3use tokio::sync::mpsc;
4
5use crate::action_chain::ActionChain;
6use crate::builder::SessionBuilder;
7use crate::cmd::BrowserCmd;
8use crate::error::NightFuryError;
9
10/// A live browser session backed by a stealth Chrome instance.
11///
12/// `BrowserSession` is `Send + 'static`. The underlying `ChaserPage` (`!Send`)
13/// lives on a dedicated `std::thread` / `tokio::task::LocalSet`. All operations
14/// cross the thread boundary via an `mpsc` channel.
15///
16/// **Drop = browser closes.** Dropping `BrowserSession` closes the channel,
17/// which causes the worker loop to exit and the browser process to terminate.
18#[derive(Clone, Debug)]
19pub struct BrowserSession {
20 tx: mpsc::Sender<BrowserCmd>,
21 auto_wait: Option<Duration>,
22}
23
24impl BrowserSession {
25 /// Create a `SessionBuilder` to configure and launch a new browser.
26 pub fn builder() -> SessionBuilder {
27 SessionBuilder::default()
28 }
29
30 /// Wrap an existing `mpsc::Sender<BrowserCmd>` into a `BrowserSession`.
31 ///
32 /// Use this when you need to manage the browser thread yourself — for
33 /// example, flock's `stealth_launch` spawns its own thread to perform
34 /// Cloudflare bypass logic, then wraps the resulting channel sender here.
35 ///
36 /// The caller must ensure a `run_worker(chaser, rx)` loop is running on a
37 /// `LocalSet` thread that consumes the matching `mpsc::Receiver<BrowserCmd>`.
38 pub fn from_sender(tx: mpsc::Sender<BrowserCmd>) -> Self {
39 Self {
40 tx,
41 auto_wait: None,
42 }
43 }
44
45 /// Return the configured auto-wait duration, if any.
46 pub fn auto_wait(&self) -> Option<Duration> {
47 self.auto_wait
48 }
49
50 /// Set the auto-wait duration for element operations.
51 ///
52 /// When set, element operations (`click`, `type_text`, `get_text`, etc.)
53 /// automatically retry on `ElementNotFound` errors, polling every 100 ms
54 /// until the element appears or the timeout expires.
55 ///
56 /// Pass `None` to disable auto-wait (the default).
57 pub fn set_auto_wait(&mut self, timeout: Option<Duration>) {
58 self.auto_wait = timeout;
59 }
60
61 /// Create a new session handle with the given auto-wait duration.
62 ///
63 /// This is a convenience builder-style method that returns a new
64 /// `BrowserSession` sharing the same underlying browser connection.
65 pub fn with_auto_wait(mut self, timeout: Duration) -> Self {
66 self.auto_wait = Some(timeout);
67 self
68 }
69
70 // -------------------------------------------------------------------------
71 // Internal
72 // -------------------------------------------------------------------------
73
74 /// Create an [`ActionChain`] builder to queue multiple browser actions.
75 ///
76 /// Actions are executed sequentially when [`ActionChain::execute`] is called,
77 /// stopping on the first error.
78 pub fn actions(&self) -> ActionChain<'_> {
79 ActionChain::new(self)
80 }
81
82 pub(crate) fn from_sender_with_auto_wait(
83 tx: mpsc::Sender<BrowserCmd>,
84 auto_wait: Option<Duration>,
85 ) -> Self {
86 Self { tx, auto_wait }
87 }
88
89 pub(crate) async fn send(&self, cmd: BrowserCmd) -> Result<(), NightFuryError> {
90 self.tx
91 .send(cmd)
92 .await
93 .map_err(|_| NightFuryError::WorkerDead)
94 }
95
96 /// Retry an async operation on `ElementNotFound` with 100ms poll interval.
97 ///
98 /// Returns the first successful result, or the last `ElementNotFound` error
99 /// if the timeout expires. Non-`ElementNotFound` errors are returned immediately.
100 pub(crate) async fn retry_on_not_found<T, F, Fut>(
101 &self,
102 timeout: Duration,
103 mut op: F,
104 ) -> Result<T, NightFuryError>
105 where
106 F: FnMut() -> Fut,
107 Fut: std::future::Future<Output = Result<T, NightFuryError>>,
108 {
109 let start = std::time::Instant::now();
110 loop {
111 match op().await {
112 Ok(v) => return Ok(v),
113 Err(NightFuryError::ElementNotFound(_)) if start.elapsed() < timeout => {
114 tokio::time::sleep(Duration::from_millis(100)).await;
115 }
116 Err(e) => return Err(e),
117 }
118 }
119 }
120}
121
122#[cfg(test)]
123mod tests;