Skip to main content

glass/browser/session/
targets.rs

1//! Multi-target (parallel page) operations.
2//!
3//! Provides [`BrowserSession::with_targets`] for opening multiple
4//! page targets, executing a closure with concurrent access, and
5//! automatically cleaning up. The active target before the call is
6//! restored afterward.
7
8use super::*;
9
10/// Maximum number of concurrent targets for parallel operations.
11const MAX_CONCURRENT_TARGETS: usize = 4;
12
13impl BrowserSession {
14    /// Open `n` page targets, execute an async closure with concurrent
15    /// access, then close all opened targets.
16    ///
17    /// The closure receives the list of opened target infos. Targets
18    /// are closed automatically when the closure returns (or panics).
19    /// Bounded to 4 concurrent targets.
20    ///
21    /// The active target before calling this method is restored after
22    /// all opened targets are closed.
23    pub async fn with_targets<F, Fut>(&self, n: usize, f: F) -> BrowserResult<Fut::Output>
24    where
25        F: FnOnce(Vec<PageTargetInfo>) -> Fut,
26        Fut: std::future::Future,
27    {
28        if n == 0 || n > MAX_CONCURRENT_TARGETS {
29            return Err(
30                format!("with_targets: n must be 1..={MAX_CONCURRENT_TARGETS}, got {n}").into(),
31            );
32        }
33
34        // Save current active target to restore later
35        let previous_active = {
36            let topology = self.topology.lock().await;
37            topology.active_target_id.clone()
38        };
39
40        // Phase 1: open all targets
41        let mut targets = Vec::with_capacity(n);
42        for i in 0..n {
43            let target = match self.create_target("about:blank").await {
44                Ok(target) => target,
45                Err(e) => {
46                    let _ = cleanup_targets(self, &targets).await;
47                    return Err(
48                        format!("with_targets: failed to create target {i}/{n}: {e}").into(),
49                    );
50                }
51            };
52            targets.push(target);
53        }
54
55        // Phase 2: execute the closure
56        let result = f(targets.clone()).await;
57
58        // Phase 3: close all opened targets
59        if let Err(e) = cleanup_targets(self, &targets).await {
60            tracing::warn!("with_targets: cleanup failed: {e}");
61        }
62
63        // Restore previous active if it still exists
64        if let Some(ref prev_id) = previous_active {
65            let exists = self
66                .list_targets()
67                .await
68                .map(|targets| targets.iter().any(|t| &t.id == prev_id))
69                .unwrap_or(false);
70            if exists {
71                let _ = self.select_target(prev_id).await;
72            }
73        }
74
75        Ok(result)
76    }
77}
78
79async fn cleanup_targets(
80    session: &BrowserSession,
81    targets: &[PageTargetInfo],
82) -> BrowserResult<()> {
83    for target in targets {
84        // Don't fail if a target is already gone
85        let _ = session.close_target(&target.id).await;
86    }
87    Ok(())
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn max_concurrent_targets_is_4() {
96        assert_eq!(MAX_CONCURRENT_TARGETS, 4);
97    }
98
99    #[test]
100    fn max_concurrent_targets_is_reasonable() {
101        // Must be at least 1 to allow single-target parallelism
102        const { assert!(MAX_CONCURRENT_TARGETS >= 1) };
103        // Must be at most 16 to avoid resource exhaustion
104        const { assert!(MAX_CONCURRENT_TARGETS <= 16) };
105    }
106
107    #[test]
108    fn max_concurrent_targets_is_power_of_two() {
109        // Power-of-two bounds align with typical pool sizing
110        assert_eq!(MAX_CONCURRENT_TARGETS.count_ones(), 1);
111    }
112}