phantom_frame/
lib.rs

1pub mod cache;
2pub mod config;
3pub mod control;
4pub mod path_matcher;
5pub mod proxy;
6
7use axum::{extract::Extension, Router};
8use cache::{CacheStore, RefreshTrigger};
9use proxy::ProxyState;
10use std::sync::Arc;
11
12/// Information about an incoming request for cache key generation
13#[derive(Clone, Debug)]
14pub struct RequestInfo<'a> {
15    /// HTTP method (e.g., "GET", "POST", "PUT", "DELETE")
16    pub method: &'a str,
17    /// Request path (e.g., "/api/users")
18    pub path: &'a str,
19    /// Query string (e.g., "id=123&sort=asc")
20    pub query: &'a str,
21    /// Request headers (for custom cache key logic based on headers)
22    pub headers: &'a axum::http::HeaderMap,
23}
24
25/// Configuration for creating a proxy
26#[derive(Clone)]
27pub struct CreateProxyConfig {
28    /// The backend URL to proxy requests to
29    pub proxy_url: String,
30    
31    /// Paths to include in caching (empty means include all)
32    /// Supports wildcards and method prefixes: "/api/*", "POST /api/*", "GET /*/users", etc.
33    pub include_paths: Vec<String>,
34    
35    /// Paths to exclude from caching (empty means exclude none)
36    /// Supports wildcards and method prefixes: "/admin/*", "POST *", "PUT /api/*", etc.
37    /// Exclude overrides include
38    pub exclude_paths: Vec<String>,
39    
40    /// Enable WebSocket and protocol upgrade support (default: true)
41    /// When enabled, requests with Connection: Upgrade headers will bypass
42    /// the cache and establish a direct bidirectional TCP tunnel
43    pub enable_websocket: bool,
44    
45    /// Only allow GET requests, reject all others (default: false)
46    /// When true, only GET requests are processed; POST, PUT, DELETE, etc. return 405 Method Not Allowed
47    /// Useful for static site prerendering where mutations shouldn't be allowed
48    pub forward_get_only: bool,
49    
50    /// Custom cache key generator
51    /// Takes request info and returns a cache key
52    /// Default: method + path + query string
53    pub cache_key_fn: Arc<dyn Fn(&RequestInfo) -> String + Send + Sync>,
54}
55
56impl CreateProxyConfig {
57    /// Create a new config with default settings
58    pub fn new(proxy_url: String) -> Self {
59        Self {
60            proxy_url,
61            include_paths: vec![],
62            exclude_paths: vec![],
63            enable_websocket: true,
64            forward_get_only: false,
65            cache_key_fn: Arc::new(|req_info| {
66                if req_info.query.is_empty() {
67                    format!("{}:{}", req_info.method, req_info.path)
68                } else {
69                    format!("{}:{}?{}", req_info.method, req_info.path, req_info.query)
70                }
71            }),
72        }
73    }
74    
75    /// Set include paths
76    pub fn with_include_paths(mut self, paths: Vec<String>) -> Self {
77        self.include_paths = paths;
78        self
79    }
80    
81    /// Set exclude paths
82    pub fn with_exclude_paths(mut self, paths: Vec<String>) -> Self {
83        self.exclude_paths = paths;
84        self
85    }
86    
87    /// Enable or disable WebSocket and protocol upgrade support
88    pub fn with_websocket_enabled(mut self, enabled: bool) -> Self {
89        self.enable_websocket = enabled;
90        self
91    }
92    
93    /// Only allow GET requests, reject all others
94    pub fn with_forward_get_only(mut self, enabled: bool) -> Self {
95        self.forward_get_only = enabled;
96        self
97    }
98    
99    /// Set custom cache key function
100    pub fn with_cache_key_fn<F>(mut self, f: F) -> Self
101    where
102        F: Fn(&RequestInfo) -> String + Send + Sync + 'static,
103    {
104        self.cache_key_fn = Arc::new(f);
105        self
106    }
107}
108
109/// The main library interface for using phantom-frame as a library
110/// Returns a proxy handler function and a refresh trigger
111pub fn create_proxy(config: CreateProxyConfig) -> (Router, RefreshTrigger) {
112    let refresh_trigger = RefreshTrigger::new();
113    let cache = CacheStore::new(refresh_trigger.clone());
114
115    // Spawn background task to listen for refresh events
116    spawn_refresh_listener(cache.clone());
117
118    let proxy_state = Arc::new(ProxyState::new(cache, config));
119
120    let app = Router::new()
121        .fallback(proxy::proxy_handler)
122        .layer(Extension(proxy_state));
123
124    (app, refresh_trigger)
125}
126
127/// Create a proxy handler with an existing refresh trigger
128pub fn create_proxy_with_trigger(config: CreateProxyConfig, refresh_trigger: RefreshTrigger) -> Router {
129    let cache = CacheStore::new(refresh_trigger);
130    
131    // Spawn background task to listen for refresh events
132    spawn_refresh_listener(cache.clone());
133
134    let proxy_state = Arc::new(ProxyState::new(cache, config));
135
136    Router::new()
137        .fallback(proxy::proxy_handler)
138        .layer(Extension(proxy_state))
139}
140
141/// Spawn a background task to listen for refresh events
142fn spawn_refresh_listener(cache: CacheStore) {
143    let mut receiver = cache.refresh_trigger().subscribe();
144    
145    tokio::spawn(async move {
146        loop {
147            match receiver.recv().await {
148                Ok(cache::RefreshMessage::All) => {
149                    tracing::debug!("Cache refresh triggered: clearing all entries");
150                    cache.clear().await;
151                }
152                Ok(cache::RefreshMessage::Pattern(pattern)) => {
153                    tracing::debug!("Cache refresh triggered: clearing entries matching pattern '{}'", pattern);
154                    cache.clear_by_pattern(&pattern).await;
155                }
156                Err(e) => {
157                    tracing::error!("Refresh trigger channel error: {}", e);
158                    break;
159                }
160            }
161        }
162    });
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[tokio::test]
170    async fn test_create_proxy() {
171        let config = CreateProxyConfig::new("http://localhost:8080".to_string());
172        let (_app, trigger) = create_proxy(config);
173        trigger.trigger();
174        trigger.trigger_by_key_match("GET:/api/*");
175        // Just ensure it compiles and runs without panic
176    }
177}