halldyll_core/fetch/
conditional.rs

1//! Conditional - Conditional requests (ETag, If-Modified-Since)
2
3use std::collections::HashMap;
4use std::sync::RwLock;
5use url::Url;
6
7/// Metadata cache for conditional requests
8pub struct ConditionalCache {
9    /// Entry storage
10    entries: RwLock<HashMap<String, CacheEntry>>,
11    /// Max cache size
12    max_size: usize,
13}
14
15/// Cache entry
16#[derive(Debug, Clone)]
17pub struct CacheEntry {
18    /// ETag
19    pub etag: Option<String>,
20    /// Last-Modified
21    pub last_modified: Option<String>,
22    /// Caching timestamp
23    pub cached_at: std::time::Instant,
24}
25
26impl Default for ConditionalCache {
27    fn default() -> Self {
28        Self::new(10000)
29    }
30}
31
32impl ConditionalCache {
33    /// New cache
34    pub fn new(max_size: usize) -> Self {
35        Self {
36            entries: RwLock::new(HashMap::new()),
37            max_size,
38        }
39    }
40
41    /// Cache key for a URL
42    fn cache_key(url: &Url) -> String {
43        // Normalize: remove fragment, sort query params
44        let mut url = url.clone();
45        url.set_fragment(None);
46        url.to_string()
47    }
48
49    /// Retrieves an entry
50    pub fn get(&self, url: &Url) -> Option<CacheEntry> {
51        let key = Self::cache_key(url);
52        self.entries.read().ok()?.get(&key).cloned()
53    }
54
55    /// Caches an entry
56    pub fn set(&self, url: &Url, etag: Option<String>, last_modified: Option<String>) {
57        let key = Self::cache_key(url);
58        let entry = CacheEntry {
59            etag,
60            last_modified,
61            cached_at: std::time::Instant::now(),
62        };
63
64        if let Ok(mut entries) = self.entries.write() {
65            // Simple eviction if too large
66            if entries.len() >= self.max_size {
67                // Remove 10% of the oldest entries
68                let to_remove: Vec<_> = entries
69                    .iter()
70                    .take(self.max_size / 10)
71                    .map(|(k, _)| k.clone())
72                    .collect();
73                for k in to_remove {
74                    entries.remove(&k);
75                }
76            }
77            entries.insert(key, entry);
78        }
79    }
80
81    /// Removes an entry
82    pub fn remove(&self, url: &Url) {
83        let key = Self::cache_key(url);
84        if let Ok(mut entries) = self.entries.write() {
85            entries.remove(&key);
86        }
87    }
88
89    /// Clears the cache
90    pub fn clear(&self) {
91        if let Ok(mut entries) = self.entries.write() {
92            entries.clear();
93        }
94    }
95
96    /// Current size
97    pub fn len(&self) -> usize {
98        self.entries.read().map(|e| e.len()).unwrap_or(0)
99    }
100
101    /// Is empty?
102    pub fn is_empty(&self) -> bool {
103        self.len() == 0
104    }
105}
106
107/// Builder for conditional request
108pub struct ConditionalRequest {
109    /// ETag for If-None-Match
110    pub etag: Option<String>,
111    /// Date for If-Modified-Since
112    pub last_modified: Option<String>,
113}
114
115impl ConditionalRequest {
116    /// New empty conditional request
117    pub fn new() -> Self {
118        Self {
119            etag: None,
120            last_modified: None,
121        }
122    }
123
124    /// From a cache entry
125    pub fn from_cache(entry: &CacheEntry) -> Self {
126        Self {
127            etag: entry.etag.clone(),
128            last_modified: entry.last_modified.clone(),
129        }
130    }
131
132    /// Do we have conditions?
133    pub fn has_conditions(&self) -> bool {
134        self.etag.is_some() || self.last_modified.is_some()
135    }
136}
137
138impl Default for ConditionalRequest {
139    fn default() -> Self {
140        Self::new()
141    }
142}