Skip to main content

playwright_cdp/
worker.rs

1//! `Worker` — web/service/shared-worker capture via the CDP `Target` domain.
2//!
3//! [`Page::on_worker`](crate::page::Page::on_worker) enables flattened auto-attach
4//! on the page session (`Target.setAutoAttach { flatten: true }`) and dispatches a
5//! [`Worker`] for every child target whose type is `worker`, `service_worker`, or
6//! `shared_worker` (surfaced via `Target.attachedToTarget`). Each worker gets its
7//! own sub-session on the same flattened connection; evaluation runs there via
8//! `Runtime.evaluate`.
9
10use crate::cdp::session::CdpSession;
11use crate::error::{Error, Result};
12use serde::de::DeserializeOwned;
13use serde_json::{json, Value};
14use std::sync::atomic::{AtomicBool, Ordering};
15use std::sync::Arc;
16
17/// A web/service/shared worker owned by a page.
18///
19/// Produced by [`Page::on_worker`](crate::page::Page::on_worker). The worker is
20/// driven through its own CDP sub-session; call [`evaluate`](Worker::evaluate)
21/// to run JS in the worker's execution context.
22#[derive(Clone)]
23pub struct Worker {
24    inner: Arc<WorkerInner>,
25}
26
27struct WorkerInner {
28    url: String,
29    session: CdpSession,
30}
31
32impl Worker {
33    pub(crate) fn new(connection: Arc<crate::cdp::connection::CdpConnection>, session_id: String, url: String) -> Self {
34        let session = CdpSession::target(connection, session_id);
35        // Best-effort: enable the Runtime domain on the worker session so
36        // `Runtime.evaluate` works. We don't await this here (we're in a sync
37        // ctor); the listener task fires it after construction.
38        Self {
39            inner: Arc::new(WorkerInner { url, session }),
40        }
41    }
42
43    /// The worker's script URL (e.g. `blob:...`, `https://.../sw.js`).
44    pub fn url(&self) -> &str {
45        &self.inner.url
46    }
47
48    /// (Internal) the worker's CDP sub-session.
49    #[allow(dead_code)]
50    pub(crate) fn session(&self) -> &CdpSession {
51        &self.inner.session
52    }
53
54    /// Best-effort `Runtime.enable` on the worker session so evaluate works.
55    pub(crate) async fn enable_runtime(&self) {
56        let _ = self.inner.session.send("Runtime.enable", json!({})).await;
57    }
58
59    /// Register a handler invoked once when the worker is destroyed.
60    ///
61    /// A worker is reported as destroyed when the browser detaches its target
62    /// (`Target.detachedFromTarget` whose `sessionId` matches this worker's
63    /// sub-session). That event is delivered on the parent session, so we
64    /// subscribe at the connection (browser/root) level and filter by session
65    /// id. We also watch the worker's own session for
66    /// `Runtime.executionContextDestroyed` as a fallback. The handler fires at
67    /// most once: the first signal wins, the other is suppressed.
68    pub fn on_close<F, Fut>(&self, handler: F)
69    where
70        F: Fn() -> Fut + Send + Sync + 'static,
71        Fut: std::future::Future<Output = ()> + Send + 'static,
72    {
73        let session_id = match self.inner.session.session_id() {
74            Some(s) => s.to_string(),
75            // No session id to track — nothing to observe; drop the handler.
76            None => return,
77        };
78        let conn = self.inner.session.connection().clone();
79
80        // Box the handler so both watcher tasks can share a single, one-shot
81        // invocation slot.
82        let boxed: Arc<
83            dyn Fn() -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>
84                + Send
85                + Sync,
86        > = Arc::new(move || Box::pin(handler()));
87        let fired = Arc::new(AtomicBool::new(false));
88
89        // Subscribe at the browser/root level: `Target.detachedFromTarget`
90        // for the worker arrives on the parent session, not the worker's own.
91        let mut root_rx = conn.subscribe(None);
92        let boxed_root = Arc::clone(&boxed);
93        let fired_root = Arc::clone(&fired);
94        tokio::spawn(async move {
95            loop {
96                let ev = match root_rx.recv().await {
97                    Ok(ev) => ev,
98                    Err(_) => break,
99                };
100                if ev.method != "Target.detachedFromTarget" {
101                    continue;
102                }
103                let detached = ev
104                    .params
105                    .get("sessionId")
106                    .or_else(|| ev.params.get("session_id"))
107                    .and_then(|v| v.as_str());
108                if detached == Some(session_id.as_str()) {
109                    if !fired_root.swap(true, Ordering::SeqCst) {
110                        (boxed_root)().await;
111                    }
112                    break;
113                }
114            }
115        });
116
117        // Fallback: watch the worker's own session for context teardown.
118        let mut self_rx = self.inner.session.subscribe();
119        let boxed_ctx = Arc::clone(&boxed);
120        let fired_ctx = Arc::clone(&fired);
121        tokio::spawn(async move {
122            loop {
123                let ev = match self_rx.recv().await {
124                    Ok(ev) => ev,
125                    Err(_) => break,
126                };
127                if ev.method == "Runtime.executionContextDestroyed"
128                    && !fired_ctx.swap(true, Ordering::SeqCst)
129                {
130                    (boxed_ctx)().await;
131                    break;
132                }
133            }
134        });
135    }
136
137    /// Evaluate a JS expression in the worker's execution context, returning a
138    /// typed result.
139    ///
140    /// `expression` is wrapped as `(() => { return (<expression>); })()` and
141    /// the result's `value` is parsed with serde.
142    pub async fn evaluate<R: DeserializeOwned>(&self, expression: &str) -> Result<R> {
143        let wrapped = format!("(() => {{ return ({expression}); }})()");
144        let resp = self
145            .inner
146            .session
147            .send(
148                "Runtime.evaluate",
149                json!({
150                    "expression": wrapped,
151                    "returnByValue": true,
152                    "awaitPromise": true,
153                }),
154            )
155            .await?;
156        if let Some(exc) = resp.get("exceptionDetails") {
157            let msg = exc
158                .get("exception")
159                .and_then(|e| e.get("description"))
160                .and_then(|v| v.as_str())
161                .unwrap_or("evaluation threw");
162            return Err(Error::ProtocolError(format!("eval error: {msg}")));
163        }
164        let value = resp
165            .get("result")
166            .and_then(|r| r.get("value"))
167            .cloned()
168            .unwrap_or(Value::Null);
169        serde_json::from_value::<R>(value).map_err(Error::from)
170    }
171}