Skip to main content

cloudflare_dns/api/
cache.rs

1#![allow(dead_code)]
2
3//! Caching layer for DNS records.
4//!
5//! This module provides a simple in-memory cache with TTL (time-to-live) support
6//! to reduce redundant API calls when refreshing the UI.
7
8use std::time::{Duration, Instant};
9
10use crate::api::DnsRecord;
11
12/// Cache for DNS records with automatic expiration.
13///
14/// The cache stores records along with their fetch timestamp and expires
15/// after a configurable TTL to ensure data freshness.
16pub struct DnsCache {
17    /// Cached DNS records (None means not yet loaded or expired)
18    records: Option<Vec<DnsRecord>>,
19    /// When the cache was last populated
20    last_updated: Option<Instant>,
21    /// How long the cache is valid
22    ttl: Duration,
23}
24
25impl DnsCache {
26    /// Create a new DNS record cache.
27    ///
28    /// # Arguments
29    /// * `ttl` - How long to keep records cached before requiring a refresh
30    pub fn new(ttl: Duration) -> Self {
31        Self {
32            records: None,
33            last_updated: None,
34            ttl,
35        }
36    }
37
38    /// Create a cache with default 60-second TTL.
39    pub fn with_default_ttl() -> Self {
40        Self::new(Duration::from_secs(60))
41    }
42
43    /// Check if the cache has valid (non-expired) records.
44    pub fn is_valid(&self) -> bool {
45        match self.last_updated {
46            Some(updated) => updated.elapsed() < self.ttl,
47            None => false,
48        }
49    }
50
51    /// Get cached records if valid, otherwise returns None.
52    pub fn get(&self) -> Option<&Vec<DnsRecord>> {
53        if self.is_valid() {
54            self.records.as_ref()
55        } else {
56            None
57        }
58    }
59
60    /// Update the cache with fresh records.
61    pub fn set(&mut self, records: Vec<DnsRecord>) {
62        self.records = Some(records);
63        self.last_updated = Some(Instant::now());
64    }
65
66    /// Force invalidate the cache.
67    pub fn invalidate(&mut self) {
68        self.records = None;
69        self.last_updated = None;
70    }
71
72    /// Get the age of the cached records.
73    pub fn age(&self) -> Option<Duration> {
74        self.last_updated.map(|t| t.elapsed())
75    }
76
77    /// Get remaining TTL before cache expires.
78    pub fn remaining_ttl(&self) -> Option<Duration> {
79        self.last_updated.map(|updated| {
80            let elapsed = updated.elapsed();
81            if elapsed < self.ttl {
82                self.ttl - elapsed
83            } else {
84                Duration::ZERO
85            }
86        })
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use std::thread;
94
95    fn make_record(id: &str) -> DnsRecord {
96        DnsRecord {
97            id: Some(id.to_string()),
98            record_type: "A".to_string(),
99            name: "example.com".to_string(),
100            content: "127.0.0.1".to_string(),
101            ttl: Some(300),
102            proxied: Some(false),
103            comment: None,
104        }
105    }
106
107    #[test]
108    fn test_cache_initially_invalid() {
109        let cache = DnsCache::with_default_ttl();
110        assert!(!cache.is_valid());
111        assert!(cache.get().is_none());
112    }
113
114    #[test]
115    fn test_cache_set_and_get() {
116        let mut cache = DnsCache::with_default_ttl();
117        let records = vec![make_record("1"), make_record("2")];
118        cache.set(records);
119
120        assert!(cache.is_valid());
121        let cached = cache.get().unwrap();
122        assert_eq!(cached.len(), 2);
123        assert_eq!(cached[0].id, Some("1".to_string()));
124    }
125
126    #[test]
127    fn test_cache_expires() {
128        let mut cache = DnsCache::new(Duration::from_millis(100));
129        let records = vec![make_record("1")];
130        cache.set(records);
131
132        assert!(cache.is_valid());
133
134        // Wait for cache to expire
135        thread::sleep(Duration::from_millis(150));
136
137        assert!(!cache.is_valid());
138        assert!(cache.get().is_none());
139    }
140
141    #[test]
142    fn test_cache_invalidate() {
143        let mut cache = DnsCache::with_default_ttl();
144        cache.set(vec![make_record("1")]);
145        assert!(cache.is_valid());
146
147        cache.invalidate();
148        assert!(!cache.is_valid());
149        assert!(cache.get().is_none());
150    }
151
152    #[test]
153    fn test_cache_age() {
154        let mut cache = DnsCache::with_default_ttl();
155        assert!(cache.age().is_none());
156
157        cache.set(vec![make_record("1")]);
158        assert!(cache.age().is_some());
159        assert!(cache.age().unwrap() < Duration::from_secs(1));
160    }
161
162    #[test]
163    fn test_cache_remaining_ttl() {
164        let mut cache = DnsCache::new(Duration::from_secs(10));
165        assert!(cache.remaining_ttl().is_none());
166
167        cache.set(vec![make_record("1")]);
168        let remaining = cache.remaining_ttl().unwrap();
169        assert!(remaining <= Duration::from_secs(10));
170        assert!(remaining > Duration::from_secs(9));
171    }
172
173    #[test]
174    fn test_cache_remaining_ttl_zero_when_expired() {
175        let mut cache = DnsCache::new(Duration::from_millis(50));
176        cache.set(vec![make_record("1")]);
177
178        thread::sleep(Duration::from_millis(100));
179        assert_eq!(cache.remaining_ttl().unwrap(), Duration::ZERO);
180    }
181}