Skip to main content

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;