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    /// Custom cache key generator
46    /// Takes request info and returns a cache key
47    /// Default: method + path + query string
48    pub cache_key_fn: Arc<dyn Fn(&RequestInfo) -> String + Send + Sync>,
49}
50
51impl CreateProxyConfig {
52    /// Create a new config with default settings
53    pub fn new(proxy_url: String) -> Self {
54        Self {
55            proxy_url,
56            include_paths: vec![],
57            exclude_paths: vec![],
58            enable_websocket: true,
59            cache_key_fn: Arc::new(|req_info| {
60                if req_info.query.is_empty() {
61                    format!("{}:{}", req_info.method, req_info.path)
62                } else {
63                    format!("{}:{}?{}", req_info.method, req_info.path, req_info.query)
64                }
65            }),
66        }
67    }
68    
69    /// Set include paths
70    pub fn with_include_paths(mut self, paths: Vec<String>) -> Self {
71        self.include_paths = paths;
72        self
73    }
74    
75    /// Set exclude paths
76    pub fn with_exclude_paths(mut self, paths: Vec<String>) -> Self {
77        self.exclude_paths = paths;
78        self
79    }
80    
81    /// Enable or disable WebSocket and protocol upgrade support
82    pub fn with_websocket_enabled(mut self, enabled: bool) -> Self {
83        self.enable_websocket = enabled;
84        self
85    }
86    
87    /// Set custom cache key function
88    pub fn with_cache_key_fn<F>(mut self, f: F) -> Self
89    where
90        F: Fn(&RequestInfo) -> String + Send + Sync + 'static,
91    {
92        self.cache_key_fn = Arc::new(f);
93        self
94    }
95}
96
97/// The main library interface for using phantom-frame as a library
98/// Returns a proxy handler function and a refresh trigger
99pub fn create_proxy(config: CreateProxyConfig) -> (Router, RefreshTrigger) {
100    let refresh_trigger = RefreshTrigger::new();
101    let cache = CacheStore::new(refresh_trigger.clone());
102
103    // Spawn background task to listen for refresh events
104    spawn_refresh_listener(cache.clone());
105
106    let proxy_state = Arc::new(ProxyState::new(cache, config));
107
108    let app = Router::new()
109        .fallback(proxy::proxy_handler)
110        .layer(Extension(proxy_state));
111
112    (app, refresh_trigger)
113}
114
115/// Create a proxy handler with an existing refresh trigger
116pub fn create_proxy_with_trigger(config: CreateProxyConfig, refresh_trigger: RefreshTrigger) -> Router {
117    let cache = CacheStore::new(refresh_trigger);
118    
119    // Spawn background task to listen for refresh events
120    spawn_refresh_listener(cache.clone());
121
122    let proxy_state = Arc::new(ProxyState::new(cache, config));
123
124    Router::new()
125        .fallback(proxy::proxy_handler)
126        .layer(Extension(proxy_state))
127}
128
129/// Spawn a background task to listen for refresh events
130fn spawn_refresh_listener(cache: CacheStore) {
131    let mut receiver = cache.refresh_trigger().subscribe();
132    
133    tokio::spawn(async move {
134        loop {
135            match receiver.recv().await {
136                Ok(cache::RefreshMessage::All) => {
137                    tracing::info!("Cache refresh triggered: clearing all entries");
138                    cache.clear().await;
139                }
140                Ok(cache::RefreshMessage::Pattern(pattern)) => {
141                    tracing::info!("Cache refresh triggered: clearing entries matching pattern '{}'", pattern);
142                    cache.clear_by_pattern(&pattern).await;
143                }
144                Err(e) => {
145                    tracing::error!("Refresh trigger channel error: {}", e);
146                    break;
147                }
148            }
149        }
150    });
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[tokio::test]
158    async fn test_create_proxy() {
159        let config = CreateProxyConfig::new("http://localhost:8080".to_string());
160        let (_app, trigger) = create_proxy(config);
161        trigger.trigger();
162        trigger.trigger_by_key_match("GET:/api/*");
163        // Just ensure it compiles and runs without panic
164    }
165}