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::{broadcast, RwLock};
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.route_change_tx.send(RouteChangeNotification::RouteAdded);
154
155        Ok(())
156    }
157
158    /// Register a route handler with a predicate function.
159    ///
160    /// # Errors
161    ///
162    /// Returns an error if registering the route handler fails.
163    pub async fn route_predicate<P, H, Fut>(&self, predicate: P, handler: H) -> Result<(), NetworkError>
164    where
165        P: Fn(&str) -> bool + Send + Sync + 'static,
166        H: Fn(Route) -> Fut + Send + Sync + 'static,
167        Fut: Future<Output = Result<(), NetworkError>> + Send + 'static,
168    {
169        // Create a matcher from the predicate
170        struct PredicateMatcher<F>(F);
171        impl<F: Fn(&str) -> bool + Send + Sync> UrlMatcher for PredicateMatcher<F> {
172            fn matches(&self, url: &str) -> bool {
173                (self.0)(url)
174            }
175        }
176
177        // Wrap the handler
178        let handler: Arc<
179            dyn Fn(Route) -> Pin<Box<dyn Future<Output = Result<(), NetworkError>> + Send>>
180                + Send
181                + Sync,
182        > = Arc::new(move |route| Box::pin(handler(route)));
183
184        // Add to handlers
185        let mut handlers = self.handlers.write().await;
186        handlers.push(ContextRouteHandler {
187            pattern: Box::new(PredicateMatcher(predicate)),
188            handler,
189        });
190        drop(handlers); // Release write lock before enabling Fetch
191        
192        // Synchronously enable Fetch on all existing pages
193        self.enable_fetch_on_all_pages().await?;
194        
195        // Notify subscribers that a route was added
196        let _ = self.route_change_tx.send(RouteChangeNotification::RouteAdded);
197
198        Ok(())
199    }
200
201    /// Unregister handlers matching the given pattern.
202    pub async fn unroute(&self, pattern: &str) {
203        let mut handlers = self.handlers.write().await;
204        handlers.retain(|h| !h.pattern.matches(pattern));
205    }
206
207    /// Unregister all handlers.
208    pub async fn unroute_all(&self) {
209        let mut handlers = self.handlers.write().await;
210        handlers.clear();
211    }
212
213    /// Check if there are any registered routes.
214    pub async fn has_routes(&self) -> bool {
215        let handlers = self.handlers.read().await;
216        !handlers.is_empty()
217    }
218
219    /// Get the number of registered routes.
220    pub async fn route_count(&self) -> usize {
221        let handlers = self.handlers.read().await;
222        handlers.len()
223    }
224
225    /// Apply context routes to a page's route registry.
226    ///
227    /// This should be called when a new page is created to copy
228    /// context routes to the page.
229    ///
230    /// # Errors
231    ///
232    /// Returns an error if applying routes to the page fails.
233    #[deprecated(note = "Use set_context_routes on RouteHandlerRegistry instead")]
234    pub async fn apply_to_page(&self, page_registry: &RouteHandlerRegistry) -> Result<(), NetworkError> {
235        let handlers = self.handlers.read().await;
236
237        for handler in handlers.iter() {
238            // Clone the handler Arc for the page
239            let handler_clone = handler.handler.clone();
240
241            // We need to create a pattern that can be cloned
242            // For now, we'll use a catch-all pattern and filter in the handler
243            // This is a simplification - a full implementation would need
244            // to serialize/deserialize patterns
245            
246            // Note: This is a simplified approach. In practice, we would need
247            // to properly copy the pattern logic to the page registry.
248            page_registry.route("*", move |route| {
249                let handler = handler_clone.clone();
250                async move {
251                    handler(route).await
252                }
253            }).await?;
254        }
255
256        Ok(())
257    }
258
259    /// Try to handle a request with context routes.
260    ///
261    /// Returns `Some(handler)` if a matching handler is found, `None` otherwise.
262    /// This is called by page route registries as a fallback.
263    pub async fn find_handler(&self, url: &str) -> Option<Arc<
264        dyn Fn(Route) -> Pin<Box<dyn Future<Output = Result<(), NetworkError>> + Send>>
265            + Send
266            + Sync,
267    >> {
268        let handlers = self.handlers.read().await;
269        
270        // Find matching handlers (in reverse order - LIFO)
271        for handler in handlers.iter().rev() {
272            if handler.pattern.matches(url) {
273                return Some(handler.handler.clone());
274            }
275        }
276        
277        None
278    }
279    
280    /// Find all matching handlers for a URL.
281    ///
282    /// Returns all handlers that match the URL in reverse order (LIFO).
283    /// This is used for fallback chaining - handlers are tried in order
284    /// until one handles the request.
285    pub async fn find_all_handlers(&self, url: &str) -> Vec<Arc<
286        dyn Fn(Route) -> Pin<Box<dyn Future<Output = Result<(), NetworkError>> + Send>>
287            + Send
288            + Sync,
289    >> {
290        let handlers = self.handlers.read().await;
291        
292        // Collect all matching handlers (in reverse order - LIFO)
293        handlers
294            .iter()
295            .rev()
296            .filter(|h| h.pattern.matches(url))
297            .map(|h| h.handler.clone())
298            .collect()
299    }
300}
301
302#[cfg(test)]
303mod tests;