Skip to main content

gthings_cdp/
session.rs

1use crate::connection::{CdpEvent, Connection};
2use crate::error::{CdpError, Result};
3use crate::tab::Tab;
4use serde_json::Value;
5use std::time::Duration;
6use tokio::sync::broadcast;
7use tracing;
8
9/// High-level CDP session. Manages connection + tabs with event-driven lifecycle.
10pub struct Session {
11    conn: Connection,
12}
13
14impl std::fmt::Debug for Session {
15    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16        f.debug_struct("Session").finish_non_exhaustive()
17    }
18}
19
20/// Wait for a specific CDP event on a pre-subscribed receiver.
21/// Subscribe BEFORE the triggering action to avoid missing the event.
22async fn wait_for_event(
23    rx: &mut broadcast::Receiver<CdpEvent>,
24    method: &str,
25    predicate: impl Fn(&CdpEvent) -> bool + Send,
26    timeout: Duration,
27) -> Result<CdpEvent> {
28    tokio::time::timeout(timeout, async move {
29        loop {
30            match rx.recv().await {
31                Ok(event) if event.method.as_str() == method && predicate(&event) => {
32                    return Ok(event);
33                }
34                Ok(_) => continue,
35                Err(broadcast::error::RecvError::Closed) => {
36                    return Err(CdpError::ConnectionFailed {
37                        detail: "event channel closed while waiting".into(),
38                    });
39                }
40                Err(broadcast::error::RecvError::Lagged(n)) => {
41                    tracing::warn!("Event receiver lagged by {n} messages");
42                    continue;
43                }
44            }
45        }
46    })
47    .await
48    .map_err(|_| CdpError::NavigationTimeout {
49        url: "unknown".into(),
50        timeout: timeout.as_secs(),
51    })?
52}
53
54impl Session {
55    /// Connect to browser via WebSocket URL
56    pub async fn connect(ws_url: &str) -> Result<Self> {
57        let conn = Connection::connect(ws_url).await?;
58        Ok(Session { conn })
59    }
60
61    /// Create a new tab/page
62    pub async fn create_tab(&self, url: &str) -> Result<Tab> {
63        Tab::create(self, url).await
64    }
65
66    /// Evaluate JavaScript in a tab, return JSON result
67    pub async fn evaluate(&self, tab: &Tab, js: &str) -> Result<Value> {
68        tab.evaluate(self, js).await
69    }
70
71    /// Navigate to URL and wait for networkIdle lifecycle event
72    pub async fn navigate(&self, tab: &Tab, url: &str) -> Result<()> {
73        let conn = &self.conn;
74        let sid = tab.session_id.as_deref();
75
76        // 1. Enable Page events so we receive lifecycle events
77        conn.call("Page.enable", serde_json::json!({}), sid).await?;
78
79        // 1a. Enable lifecycle events (required for Chrome 144+)
80        conn.call(
81            "Page.setLifecycleEventsEnabled",
82            serde_json::json!({"enabled": true}),
83            sid,
84        )
85        .await?;
86
87        // 2. Subscribe BEFORE navigation — don't miss the networkIdle event
88        let mut rx = conn.event_rx();
89
90        // 3. Start navigation
91        conn.call("Page.navigate", serde_json::json!({"url": url}), sid)
92            .await?;
93
94        // 4. Wait for networkIdle using the pre-subscribed receiver
95        let result = wait_for_event(
96            &mut rx,
97            "Page.lifecycleEvent",
98            |evt| evt.params.get("name").and_then(|v| v.as_str()) == Some("networkIdle"),
99            Duration::from_secs(10),
100        )
101        .await;
102
103        // Fallback: if lifecycle event timed out, poll document.readyState
104        match result {
105            Ok(_) => {}
106            Err(CdpError::NavigationTimeout { .. }) => {
107                tracing::warn!("Lifecycle event timeout, falling back to readyState polling");
108                // Poll document.readyState up to 5 more seconds
109                for _ in 0..10 {
110                    if let Ok(val) = conn
111                        .call(
112                            "Runtime.evaluate",
113                            serde_json::json!({
114                                "expression": "document.readyState",
115                                "returnByValue": true
116                            }),
117                            sid,
118                        )
119                        .await
120                    {
121                        if val
122                            .get("result")
123                            .and_then(|r| r.get("value"))
124                            .and_then(|v| v.as_str())
125                            == Some("complete")
126                        {
127                            return Ok(());
128                        }
129                    }
130                    tokio::time::sleep(Duration::from_millis(500)).await;
131                }
132                return Err(CdpError::NavigationTimeout {
133                    url: url.to_string(),
134                    timeout: 15,
135                });
136            }
137            Err(e) => return Err(e),
138        }
139
140        Ok(())
141    }
142
143    /// Wait for a CDP event matching method + predicate.
144    ///
145    /// Warning: Creates a new event subscription. Subscribe BEFORE the action that
146    /// triggers the event to avoid race conditions. For navigation, use
147    /// [`navigate()`](Session::navigate) instead.
148    pub async fn wait_for<F>(
149        &self,
150        method: &str,
151        predicate: F,
152        timeout: Duration,
153    ) -> Result<CdpEvent>
154    where
155        F: Fn(&CdpEvent) -> bool + Send + 'static,
156    {
157        let mut rx = self.conn.event_rx();
158
159        tokio::time::timeout(timeout, async move {
160            loop {
161                match rx.recv().await {
162                    Ok(event) if event.method.as_str() == method && predicate(&event) => {
163                        return Ok(event);
164                    }
165                    Ok(_) => continue,
166                    Err(broadcast::error::RecvError::Closed) => {
167                        return Err(CdpError::ConnectionFailed {
168                            detail: "event channel closed while waiting".into(),
169                        });
170                    }
171                    Err(broadcast::error::RecvError::Lagged(n)) => {
172                        tracing::warn!("Event receiver lagged by {n} messages");
173                        continue;
174                    }
175                }
176            }
177        })
178        .await
179        .map_err(|_| CdpError::CdpCallFailed {
180            method: format!("wait_for({method})"),
181            detail: format!("timeout after {timeout:?}"),
182        })?
183    }
184
185    /// Close a tab
186    pub async fn close_tab(&self, tab: Tab) -> Result<()> {
187        tab.close(self).await
188    }
189
190    /// Disconnect from browser
191    pub async fn disconnect(self) -> Result<()> {
192        self.conn.close().await;
193        Ok(())
194    }
195
196    /// Access the underlying Connection (for direct CDP calls)
197    pub fn connection(&self) -> &Connection {
198        &self.conn
199    }
200}