1use crate::route::Route;
7use crate::{trace_log, RouteParams};
8use lru::LruCache;
9use std::num::NonZeroUsize;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct RouteId {
17 pub path: String,
19}
20
21impl RouteId {
22 pub fn from_route(route: &Route) -> Self {
24 Self {
25 path: route.config.path.clone(),
26 }
27 }
28
29 pub fn from_path(path: impl Into<String>) -> Self {
31 Self { path: path.into() }
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37struct OutletCacheKey {
38 path: String,
39 outlet_name: Option<String>,
40}
41
42#[derive(Debug, Clone)]
44struct ParentRouteCacheEntry {
45 parent_route_id: RouteId,
46}
47
48#[derive(Debug, Clone, Default)]
50pub struct CacheStats {
51 pub parent_hits: usize,
52 pub parent_misses: usize,
53 pub child_hits: usize,
54 pub child_misses: usize,
55 pub invalidations: usize,
56}
57
58impl CacheStats {
59 pub fn parent_hit_rate(&self) -> f64 {
60 let total = self.parent_hits + self.parent_misses;
61 if total == 0 {
62 0.0
63 } else {
64 self.parent_hits as f64 / total as f64
65 }
66 }
67
68 pub fn child_hit_rate(&self) -> f64 {
69 let total = self.child_hits + self.child_misses;
70 if total == 0 {
71 0.0
72 } else {
73 self.child_hits as f64 / total as f64
74 }
75 }
76
77 pub fn overall_hit_rate(&self) -> f64 {
78 let total_hits = self.parent_hits + self.child_hits;
79 let total_misses = self.parent_misses + self.child_misses;
80 let total = total_hits + total_misses;
81 if total == 0 {
82 0.0
83 } else {
84 total_hits as f64 / total as f64
85 }
86 }
87}
88
89#[derive(Debug)]
93pub struct RouteCache {
94 parent_cache: LruCache<String, ParentRouteCacheEntry>,
95 child_cache: LruCache<OutletCacheKey, RouteParams>,
96 stats: CacheStats,
97}
98
99impl RouteCache {
100 const DEFAULT_CAPACITY: usize = 1000;
101
102 pub fn new() -> Self {
103 Self::with_capacity(Self::DEFAULT_CAPACITY)
104 }
105
106 pub fn with_capacity(capacity: usize) -> Self {
107 let cap = NonZeroUsize::new(capacity).expect("Cache capacity must be non-zero");
108 Self {
109 parent_cache: LruCache::new(cap),
110 child_cache: LruCache::new(cap),
111 stats: CacheStats::default(),
112 }
113 }
114
115 pub fn clear(&mut self) {
116 trace_log!("Clearing route cache");
117 self.parent_cache.clear();
118 self.child_cache.clear();
119 self.stats.invalidations += 1;
120 }
121
122 pub fn get_parent(&mut self, path: &str) -> Option<RouteId> {
123 if let Some(entry) = self.parent_cache.get(path) {
124 self.stats.parent_hits += 1;
125 trace_log!("Parent cache hit for path: '{}'", path);
126 Some(entry.parent_route_id.clone())
127 } else {
128 self.stats.parent_misses += 1;
129 trace_log!("Parent cache miss for path: '{}'", path);
130 None
131 }
132 }
133
134 pub fn set_parent(&mut self, path: String, parent_route_id: RouteId) {
135 trace_log!(
136 "Caching parent route '{}' for path '{}'",
137 parent_route_id.path,
138 path
139 );
140 self.parent_cache
141 .push(path, ParentRouteCacheEntry { parent_route_id });
142 }
143
144 pub fn stats(&self) -> &CacheStats {
145 &self.stats
146 }
147
148 pub fn reset_stats(&mut self) {
149 self.stats = CacheStats::default();
150 }
151
152 pub fn parent_cache_size(&self) -> usize {
153 self.parent_cache.len()
154 }
155
156 pub fn child_cache_size(&self) -> usize {
157 self.child_cache.len()
158 }
159
160 pub fn total_size(&self) -> usize {
161 self.parent_cache_size() + self.child_cache_size()
162 }
163}
164
165impl Default for RouteCache {
166 fn default() -> Self {
167 Self::new()
168 }
169}
170
171impl Clone for RouteCache {
172 fn clone(&self) -> Self {
173 let parent_cap = self.parent_cache.cap();
174 let child_cap = self.child_cache.cap();
175 Self {
176 parent_cache: LruCache::new(parent_cap),
177 child_cache: LruCache::new(child_cap),
178 stats: self.stats.clone(),
179 }
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_cache_creation() {
189 let cache = RouteCache::new();
190 assert_eq!(cache.parent_cache_size(), 0);
191 assert_eq!(cache.stats().parent_hits, 0);
192 }
193
194 #[test]
195 fn test_parent_cache_miss() {
196 let mut cache = RouteCache::new();
197 let result = cache.get_parent("/dashboard");
198 assert!(result.is_none());
199 assert_eq!(cache.stats().parent_misses, 1);
200 }
201
202 #[test]
203 fn test_parent_cache_hit() {
204 let mut cache = RouteCache::new();
205 let route_id = RouteId::from_path("/dashboard");
206 cache.set_parent("/dashboard/analytics".to_string(), route_id.clone());
207
208 let result = cache.get_parent("/dashboard/analytics");
209 assert!(result.is_some());
210 assert_eq!(result.unwrap().path, "/dashboard");
211 assert_eq!(cache.stats().parent_hits, 1);
212 }
213
214 #[test]
215 fn test_cache_clear() {
216 let mut cache = RouteCache::new();
217 cache.set_parent("/dashboard".to_string(), RouteId::from_path("/"));
218 assert_eq!(cache.parent_cache_size(), 1);
219
220 cache.clear();
221 assert_eq!(cache.parent_cache_size(), 0);
222 assert_eq!(cache.stats().invalidations, 1);
223 }
224
225 #[test]
226 fn test_hit_rate_calculation() {
227 let mut cache = RouteCache::new();
228 cache.get_parent("/a");
229 cache.get_parent("/b");
230 cache.get_parent("/c");
231
232 cache.set_parent("/a".to_string(), RouteId::from_path("/"));
233 cache.set_parent("/b".to_string(), RouteId::from_path("/"));
234
235 cache.get_parent("/a");
236 cache.get_parent("/b");
237
238 assert_eq!(cache.stats().parent_hits, 2);
239 assert_eq!(cache.stats().parent_misses, 3);
240 assert!((cache.stats().parent_hit_rate() - 0.4).abs() < 0.001);
241 }
242}