Skip to main content

oxigdal_cloud/cache/
metadata.rs

1//! Cache metadata and entry types
2
3use bytes::Bytes;
4use std::time::{Duration, Instant};
5
6/// Cache key type
7pub type CacheKey = String;
8
9/// Cache entry with metadata
10#[derive(Debug, Clone)]
11pub struct CacheEntry {
12    /// Cached data
13    pub data: Bytes,
14    /// Entry size in bytes
15    pub size: usize,
16    /// Access count for LFU
17    pub access_count: u64,
18    /// Last access time
19    pub last_access: Instant,
20    /// Creation time
21    pub created_at: Instant,
22    /// TTL expiration time
23    pub expires_at: Option<Instant>,
24    /// Whether data is compressed
25    pub compressed: bool,
26    /// Spatial metadata for geospatial caching
27    pub spatial_info: Option<SpatialInfo>,
28}
29
30impl CacheEntry {
31    /// Creates a new cache entry
32    #[must_use]
33    pub fn new(data: Bytes, compressed: bool) -> Self {
34        let size = data.len();
35        let now = Instant::now();
36
37        Self {
38            data,
39            size,
40            access_count: 1,
41            last_access: now,
42            created_at: now,
43            expires_at: None,
44            compressed,
45            spatial_info: None,
46        }
47    }
48
49    /// Creates a new cache entry with TTL
50    #[must_use]
51    pub fn with_ttl(data: Bytes, compressed: bool, ttl: Duration) -> Self {
52        let mut entry = Self::new(data, compressed);
53        entry.expires_at = Some(Instant::now() + ttl);
54        entry
55    }
56
57    /// Creates a new cache entry with spatial info
58    #[must_use]
59    pub fn with_spatial_info(data: Bytes, compressed: bool, spatial_info: SpatialInfo) -> Self {
60        let mut entry = Self::new(data, compressed);
61        entry.spatial_info = Some(spatial_info);
62        entry
63    }
64
65    /// Records an access
66    pub fn record_access(&mut self) {
67        self.access_count += 1;
68        self.last_access = Instant::now();
69    }
70
71    /// Returns the age of the entry
72    #[must_use]
73    pub fn age(&self) -> Duration {
74        self.created_at.elapsed()
75    }
76
77    /// Checks if the entry is expired
78    #[must_use]
79    pub fn is_expired(&self) -> bool {
80        if let Some(expires_at) = self.expires_at {
81            Instant::now() >= expires_at
82        } else {
83            false
84        }
85    }
86
87    /// Returns remaining TTL if set
88    #[must_use]
89    pub fn remaining_ttl(&self) -> Option<Duration> {
90        self.expires_at.map(|expires_at| {
91            let now = Instant::now();
92            if now >= expires_at {
93                Duration::ZERO
94            } else {
95                expires_at - now
96            }
97        })
98    }
99}
100
101/// Spatial information for geospatial caching
102#[derive(Debug, Clone)]
103pub struct SpatialInfo {
104    /// Bounding box: (min_x, min_y, max_x, max_y)
105    pub bounds: (f64, f64, f64, f64),
106    /// Coordinate reference system (EPSG code)
107    pub crs: Option<u32>,
108    /// Resolution in CRS units
109    pub resolution: Option<(f64, f64)>,
110    /// Zoom level for tile caching
111    pub zoom_level: Option<u8>,
112}
113
114impl SpatialInfo {
115    /// Creates new spatial info with bounding box
116    #[must_use]
117    pub fn new(bounds: (f64, f64, f64, f64)) -> Self {
118        Self {
119            bounds,
120            crs: None,
121            resolution: None,
122            zoom_level: None,
123        }
124    }
125
126    /// Sets the CRS
127    #[must_use]
128    pub fn with_crs(mut self, crs: u32) -> Self {
129        self.crs = Some(crs);
130        self
131    }
132
133    /// Sets the resolution
134    #[must_use]
135    pub fn with_resolution(mut self, res_x: f64, res_y: f64) -> Self {
136        self.resolution = Some((res_x, res_y));
137        self
138    }
139
140    /// Sets the zoom level
141    #[must_use]
142    pub fn with_zoom_level(mut self, zoom: u8) -> Self {
143        self.zoom_level = Some(zoom);
144        self
145    }
146
147    /// Checks if this bounding box intersects with another
148    #[must_use]
149    pub fn intersects(&self, other: &SpatialInfo) -> bool {
150        let (min_x1, min_y1, max_x1, max_y1) = self.bounds;
151        let (min_x2, min_y2, max_x2, max_y2) = other.bounds;
152
153        min_x1 <= max_x2 && max_x1 >= min_x2 && min_y1 <= max_y2 && max_y1 >= min_y2
154    }
155
156    /// Checks if this bounding box contains a point
157    #[must_use]
158    pub fn contains_point(&self, x: f64, y: f64) -> bool {
159        let (min_x, min_y, max_x, max_y) = self.bounds;
160        x >= min_x && x <= max_x && y >= min_y && y <= max_y
161    }
162}
163
164/// Tile coordinates for tile-based caching
165#[derive(Debug, Clone, Hash, PartialEq, Eq)]
166pub struct TileCoord {
167    /// Zoom level
168    pub z: u8,
169    /// X coordinate
170    pub x: u32,
171    /// Y coordinate
172    pub y: u32,
173}
174
175impl TileCoord {
176    /// Creates new tile coordinates
177    #[must_use]
178    pub const fn new(z: u8, x: u32, y: u32) -> Self {
179        Self { z, x, y }
180    }
181
182    /// Generates cache key for this tile
183    #[must_use]
184    pub fn to_cache_key(&self, prefix: &str) -> String {
185        format!("{prefix}/tiles/{}/{}/{}", self.z, self.x, self.y)
186    }
187
188    /// Returns parent tile coordinates
189    #[must_use]
190    pub fn parent(&self) -> Option<Self> {
191        if self.z == 0 {
192            return None;
193        }
194        Some(Self {
195            z: self.z - 1,
196            x: self.x / 2,
197            y: self.y / 2,
198        })
199    }
200
201    /// Returns children tile coordinates
202    #[must_use]
203    pub fn children(&self) -> [Self; 4] {
204        let z = self.z + 1;
205        let x = self.x * 2;
206        let y = self.y * 2;
207        [
208            Self::new(z, x, y),
209            Self::new(z, x + 1, y),
210            Self::new(z, x, y + 1),
211            Self::new(z, x + 1, y + 1),
212        ]
213    }
214}
215
216/// Metadata for disk cache entries
217#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
218pub struct DiskCacheMetadata {
219    /// File path relative to cache dir
220    pub path: String,
221    /// Entry size
222    pub size: usize,
223    /// Creation timestamp
224    pub created_at_ms: u64,
225    /// Expiration timestamp
226    pub expires_at_ms: Option<u64>,
227    /// Access count
228    pub access_count: u64,
229    /// Whether compressed
230    pub compressed: bool,
231}
232
233/// Cache statistics
234#[derive(Debug, Default)]
235pub struct CacheStats {
236    /// Cache hits
237    pub hits: std::sync::atomic::AtomicU64,
238    /// Cache misses
239    pub misses: std::sync::atomic::AtomicU64,
240    /// Cache writes
241    pub writes: std::sync::atomic::AtomicU64,
242    /// Cache evictions
243    pub evictions: std::sync::atomic::AtomicU64,
244}
245
246impl CacheStats {
247    /// Returns hit ratio
248    #[must_use]
249    pub fn hit_ratio(&self) -> f64 {
250        use std::sync::atomic::Ordering;
251        let hits = self.hits.load(Ordering::Relaxed) as f64;
252        let misses = self.misses.load(Ordering::Relaxed) as f64;
253        if hits + misses == 0.0 {
254            0.0
255        } else {
256            hits / (hits + misses)
257        }
258    }
259
260    /// Resets statistics
261    pub fn reset(&self) {
262        use std::sync::atomic::Ordering;
263        self.hits.store(0, Ordering::Relaxed);
264        self.misses.store(0, Ordering::Relaxed);
265        self.writes.store(0, Ordering::Relaxed);
266        self.evictions.store(0, Ordering::Relaxed);
267    }
268}
269
270/// Statistics per zoom level
271#[derive(Debug, Default)]
272pub struct LevelStats {
273    /// Number of tiles at this level
274    pub tile_count: std::sync::atomic::AtomicUsize,
275    /// Total size at this level
276    pub total_size: std::sync::atomic::AtomicUsize,
277    /// Hit count at this level
278    pub hits: std::sync::atomic::AtomicU64,
279}