viewpoint_core/context/routing/
mod.rs

1//! Context-level network routing.
2//!
3//! This module provides route handlers that apply to all pages in a browser context.
4//! Context routes are applied before page routes, allowing context-wide mocking
5//! and interception of network requests.
6
7// Allow dead code for context routing scaffolding (spec: network-routing)
8
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::{Arc, Weak};
12
13use tokio::sync::{RwLock, broadcast};
14use tracing::debug;
15
16use viewpoint_cdp::CdpConnection;
17
18use crate::error::NetworkError;
19use crate::network::{Route, RouteHandlerRegistry, UrlMatcher, UrlPattern};
20
21/// Notification sent when context routes change.
22#[derive(Debug, Clone)]
23pub enum RouteChangeNotification {
24    /// A new route was added.
25    RouteAdded,
26}
27
28/// A registered context-level route handler.
29struct ContextRouteHandler {
30    /// Pattern to match URLs.
31    pattern: Box<dyn UrlMatcher>,
32    /// The handler function.
33    handler: Arc<
34        dyn Fn(Route) -> Pin<Box<dyn Future<Output = Result<(), NetworkError>> + Send>>
35            + Send
36            + Sync,
37    >,
38}
39
40impl std::fmt::Debug for ContextRouteHandler {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        f.debug_struct("ContextRouteHandler")
43            .field("pattern", &"<pattern>")
44            .field("handler", &"<fn>")
45            .finish()
46    }
47}
48
49/// Context-level route handler registry.
50///
51/// Routes registered here apply to all pages in the context.
52/// New pages automatically inherit these routes.
53#[derive(Debug)]
54pub struct ContextRouteRegistry {
55    /// Registered handlers (in reverse order - last registered is first tried).
56    handlers: RwLock<Vec<ContextRouteHandler>>,
57    /// CDP connection.
58    connection: Arc<CdpConnection>,
59    /// Context ID.
60    context_id: String,
61    /// Broadcast channel to notify pages when routes change.
62    route_change_tx: broadcast::Sender<RouteChangeNotification>,
63    /// Weak references to page route registries.
64    /// Used to synchronously enable Fetch when a new context route is added.
65    page_registries: RwLock<Vec<Weak<RouteHandlerRegistry>>>,
66}
67
68impl ContextRouteRegistry {
69    /// Create a new context route registry.
70    pub fn new(connection: Arc<CdpConnection>, context_id: String) -> Self {
71        // Create broadcast channel with capacity for a few notifications
72        let (route_change_tx, _) = broadcast::channel(16);
73        Self {
74            handlers: RwLock::new(Vec::new()),
75            connection,
76            context_id,
77            route_change_tx,
78            page_registries: RwLock::new(Vec::new()),
79        }
80    }
81
82    /// Register a page's route registry with this context.
83    ///
84    /// When a new route is added to the context, Fetch will be enabled on all
85    /// registered page registries before the `route()` call returns.
86    pub async fn register_page_registry(&self, registry: &Arc<RouteHandlerRegistry>) {
87        let mut registries = self.page_registries.write().await;
88        // Clean up any stale weak references while we're at it
89        registries.retain(|weak| weak.strong_count() > 0);
90        registries.push(Arc::downgrade(registry));
91    }
92
93    /// Enable Fetch domain on all registered pages.
94    ///
95    /// This is called when a new route is added to ensure all pages can intercept requests.
96    async fn enable_fetch_on_all_pages(&self) -> Result<(), NetworkError> {
97        let registries = self.page_registries.read().await;
98        for weak in registries.iter() {
99            if let Some(registry) = weak.upgrade() {
100                registry.ensure_fetch_enabled_public().await?;
101            }
102        }
103        Ok(())
104    }
105
106    /// Subscribe to route change notifications.
107    ///
108    /// Pages use this to know when they need to enable Fetch domain
109    /// for newly added context routes.
110    pub fn subscribe_route_changes(&self) -> broadcast::Receiver<RouteChangeNotification> {
111        self.route_change_tx.subscribe()
112    }
113
114    /// Register a route handler for the given pattern.
115    ///
116    /// The handler will be applied to all pages in the context.
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if registering the route handler fails.
121    pub async fn route<M, H, Fut>(&self, pattern: M, handler: H) -> Result<(), NetworkError>
122    where
123        M: Into<UrlPattern>,
124        H: Fn(Route) -> Fut + Send + Sync + 'static,
125        Fut: Future<Output = Result<(), NetworkError>> + Send + 'static,
126    {
127        let pattern = pattern.into();
128
129        debug!(context_id = %self.context_id, "Registering context route");
130
131        // Wrap the handler
132        let handler: Arc<
133            dyn Fn(Route) -> Pin<Box<dyn Future<Output = Result<(), NetworkError>> + Send>>
134                + Send
135                + Sync,
136        > = Arc::new(move |route| Box::pin(handler(route)));
137
138        // Add to handlers (will be matched in reverse order)
139        let mut handlers = self.handlers.write().await;
140        handlers.push(ContextRouteHandler {
141            pattern: Box::new(pattern),
142            handler,
143        });
144        drop(handlers); // Release write lock before enabling Fetch
145
146        // Synchronously enable Fetch on all existing pages before returning.
147        // This ensures that navigation that happens immediately after route()
148        // will be intercepted by this route.
149        self.enable_fetch_on_all_pages().await?;
150
151        // Notify subscribers that a route was added (for future pages and as backup)
152        // Ignore send errors (no subscribers is fine)
153        let _ = self
154            .route_change_tx
155            .send(RouteChangeNotification::RouteAdded);
156
157        Ok(())
158    }
159
160    /// Register a route handler with a predicate function.
161    ///
162    /// # Errors
163    ///
164    /// Returns an error if registering the route handler fails.
165    pub async fn route_predicate<P, H, Fut>(
166        &self,
167        predicate: P,
168        handler: H,
169    ) -> Result<(), NetworkError>
170    where
171        P: Fn(&str) -> bool + Send + Sync + 'static,
172        H: Fn(Route) -> Fut + Send + Sync + 'static,
173        Fut: Future<Output = Result<(), NetworkError>> + Send + 'static,
174    {
175        // Create a matcher from the predicate
176        struct PredicateMatcher<F>(F);
177        impl<F: Fn(&str) -> bool + Send + Sync> UrlMatcher for PredicateMatcher<F> {
178            fn matches(&self, url: &str) -> bool {
179                (self.0)(url)
180            }
181        }
182
183        // Wrap the handler
184        let handler: Arc<
185            dyn Fn(Route) -> Pin<Box<dyn Future<Output = Result<(), NetworkError>> + Send>>
186                + Send
187                + Sync,
188        > = Arc::new(move |route| Box::pin(handler(route)));
189
190        // Add to handlers
191        let mut handlers = self.handlers.write().await;
192        handlers.push(ContextRouteHandler {
193            pattern: Box::new(PredicateMatcher(predicate)),
194            handler,
195        });
196        drop(handlers); // Release write lock before enabling Fetch
197
198        // Synchronously enable Fetch on all existing pages
199        self.enable_fetch_on_all_pages().await?;
200
201        // Notify subscribers that a route was added
202        let _ = self
203            .route_change_tx
204            .send(RouteChangeNotification::RouteAdded);
205
206        Ok(())
207    }
208
209    /// Unregister handlers matching the given pattern.
210    pub async fn unroute(&self, pattern: &str) {
211        let mut handlers = self.handlers.write().await;
212        handlers.retain(|h| !h.pattern.matches(pattern));
213    }
214
215    /// Unregister all handlers.
216    pub async fn unroute_all(&self) {
217        let mut handlers = self.handlers.write().await;
218        handlers.clear();
219    }
220
221    /// Check if there are any registered routes.
222    pub async fn has_routes(&self) -> bool {
223        let handlers = self.handlers.read().await;
224        !handlers.is_empty()
225    }
226
227    /// Get the number of registered routes.
228    pub async fn route_count(&self) -> usize {
229        let handlers = self.handlers.read().await;
230        handlers.len()
231    }
232
233    /// Apply context routes to a page's route registry.
234    ///
235    /// This should be called when a new page is created to copy
236    /// context routes to the page.
237    ///
238    /// # Errors
239    ///
240    /// Returns an error if applying routes to the page fails.
241    #[deprecated(note = "Use set_context_routes on RouteHandlerRegistry instead")]
242    pub async fn apply_to_page(
243        &self,
244        page_registry: &RouteHandlerRegistry,
245    ) -> Result<(), NetworkError> {
246        let handlers = self.handlers.read().await;
247
248        for handler in handlers.iter() {
249            // Clone the handler Arc for the page
250            let handler_clone = handler.handler.clone();
251
252            // We need to create a pattern that can be cloned
253            // For now, we'll use a catch-all pattern and filter in the handler
254            // This is a simplification - a full implementation would need
255            // to serialize/deserialize patterns
256
257            // Note: This is a simplified approach. In practice, we would need
258            // to properly copy the pattern logic to the page registry.
259            page_registry
260                .route("*", move |route| {
261                    let handler = handler_clone.clone();
262                    async move { handler(route).await }
263                })
264                .await?;
265        }
266
267        Ok(())
268    }
269
270    /// Try to handle a request with context routes.
271    ///
272    /// Returns `Some(handler)` if a matching handler is found, `None` otherwise.
273    /// This is called by page route registries as a fallback.
274    pub async fn find_handler(
275        &self,
276        url: &str,
277    ) -> Option<
278        Arc<
279            dyn Fn(Route) -> Pin<Box<dyn Future<Output = Result<(), NetworkError>> + Send>>
280                + Send
281                + Sync,
282        >,
283    > {
284        let handlers = self.handlers.read().await;
285
286        // Find matching handlers (in reverse order - LIFO)
287        for handler in handlers.iter().rev() {
288            if handler.pattern.matches(url) {
289                return Some(handler.handler.clone());
290            }
291        }
292
293        None
294    }
295
296    /// Find all matching handlers for a URL.
297    ///
298    /// Returns all handlers that match the URL in reverse order (LIFO).
299    /// This is used for fallback chaining - handlers are tried in order
300    /// until one handles the request.
301    pub async fn find_all_handlers(
302        &self,
303        url: &str,
304    ) -> Vec<
305        Arc<
306            dyn Fn(Route) -> Pin<Box<dyn Future<Output = Result<(), NetworkError>> + Send>>
307                + Send
308                + Sync,
309        >,
310    > {
311        let handlers = self.handlers.read().await;
312
313        // Collect all matching handlers (in reverse order - LIFO)
314        handlers
315            .iter()
316            .rev()
317            .filter(|h| h.pattern.matches(url))
318            .map(|h| h.handler.clone())
319            .collect()
320    }
321}
322
323#[cfg(test)]
324mod tests;