Skip to main content

oxihuman_core/
cache_entry.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5/// A single cache entry with key, value, TTL and access tracking.
6#[allow(dead_code)]
7#[derive(Debug, Clone)]
8pub struct CacheEntryItem {
9    pub key: String,
10    pub value: Vec<u8>,
11    pub ttl_ms: u64,
12    pub created_at: u64,
13    pub last_access: u64,
14    pub access_count: u64,
15    pub dirty: bool,
16}
17
18/// A cache store backed by a Vec of entries.
19#[allow(dead_code)]
20#[derive(Debug, Clone)]
21pub struct CacheStore {
22    entries: Vec<CacheEntryItem>,
23    capacity: usize,
24    current_time: u64,
25}
26
27#[allow(dead_code)]
28impl CacheStore {
29    pub fn new(capacity: usize) -> Self {
30        Self {
31            entries: Vec::new(),
32            capacity: capacity.max(1),
33            current_time: 0,
34        }
35    }
36
37    pub fn advance_time(&mut self, ms: u64) {
38        self.current_time += ms;
39    }
40
41    pub fn put(&mut self, key: &str, value: Vec<u8>, ttl_ms: u64) {
42        if let Some(entry) = self.entries.iter_mut().find(|e| e.key == key) {
43            entry.value = value;
44            entry.ttl_ms = ttl_ms;
45            entry.created_at = self.current_time;
46            entry.last_access = self.current_time;
47            entry.dirty = true;
48            return;
49        }
50        if self.entries.len() >= self.capacity {
51            self.evict_oldest();
52        }
53        self.entries.push(CacheEntryItem {
54            key: key.to_string(),
55            value,
56            ttl_ms,
57            created_at: self.current_time,
58            last_access: self.current_time,
59            access_count: 0,
60            dirty: false,
61        });
62    }
63
64    pub fn get(&mut self, key: &str) -> Option<&[u8]> {
65        let now = self.current_time;
66        if let Some(entry) = self.entries.iter_mut().find(|e| e.key == key) {
67            if entry.ttl_ms > 0 && now - entry.created_at > entry.ttl_ms {
68                return None;
69            }
70            entry.last_access = now;
71            entry.access_count += 1;
72            Some(entry.value.as_slice())
73        } else {
74            None
75        }
76    }
77
78    pub fn contains(&self, key: &str) -> bool {
79        self.entries.iter().any(|e| e.key == key)
80    }
81
82    pub fn remove(&mut self, key: &str) -> bool {
83        let before = self.entries.len();
84        self.entries.retain(|e| e.key != key);
85        self.entries.len() < before
86    }
87
88    fn evict_oldest(&mut self) {
89        if self.entries.is_empty() {
90            return;
91        }
92        let mut oldest_idx = 0;
93        let mut oldest_access = u64::MAX;
94        #[allow(clippy::needless_range_loop)]
95        for i in 0..self.entries.len() {
96            if self.entries[i].last_access < oldest_access {
97                oldest_access = self.entries[i].last_access;
98                oldest_idx = i;
99            }
100        }
101        self.entries.remove(oldest_idx);
102    }
103
104    pub fn evict_expired(&mut self) {
105        let now = self.current_time;
106        self.entries
107            .retain(|e| e.ttl_ms == 0 || now - e.created_at <= e.ttl_ms);
108    }
109
110    pub fn count(&self) -> usize {
111        self.entries.len()
112    }
113
114    pub fn capacity(&self) -> usize {
115        self.capacity
116    }
117
118    pub fn clear(&mut self) {
119        self.entries.clear();
120    }
121
122    pub fn total_bytes(&self) -> usize {
123        self.entries.iter().map(|e| e.value.len()).sum()
124    }
125
126    pub fn most_accessed(&self) -> Option<&str> {
127        self.entries
128            .iter()
129            .max_by_key(|e| e.access_count)
130            .map(|e| e.key.as_str())
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn test_new() {
140        let cs = CacheStore::new(10);
141        assert_eq!(cs.count(), 0);
142        assert_eq!(cs.capacity(), 10);
143    }
144
145    #[test]
146    fn test_put_and_get() {
147        let mut cs = CacheStore::new(10);
148        cs.put("k", vec![1, 2, 3], 0);
149        assert_eq!(cs.get("k"), Some([1u8, 2, 3].as_slice()));
150    }
151
152    #[test]
153    fn test_contains() {
154        let mut cs = CacheStore::new(10);
155        cs.put("a", vec![1], 0);
156        assert!(cs.contains("a"));
157        assert!(!cs.contains("b"));
158    }
159
160    #[test]
161    fn test_remove() {
162        let mut cs = CacheStore::new(10);
163        cs.put("a", vec![1], 0);
164        assert!(cs.remove("a"));
165        assert!(!cs.contains("a"));
166    }
167
168    #[test]
169    fn test_evict_on_capacity() {
170        let mut cs = CacheStore::new(2);
171        cs.put("a", vec![1], 0);
172        cs.put("b", vec![2], 0);
173        cs.put("c", vec![3], 0);
174        assert_eq!(cs.count(), 2);
175        assert!(cs.contains("c"));
176    }
177
178    #[test]
179    fn test_ttl_expiry() {
180        let mut cs = CacheStore::new(10);
181        cs.put("a", vec![1], 100);
182        cs.advance_time(200);
183        assert!(cs.get("a").is_none());
184    }
185
186    #[test]
187    fn test_evict_expired() {
188        let mut cs = CacheStore::new(10);
189        cs.put("a", vec![1], 50);
190        cs.put("b", vec![2], 0);
191        cs.advance_time(100);
192        cs.evict_expired();
193        assert_eq!(cs.count(), 1);
194    }
195
196    #[test]
197    fn test_total_bytes() {
198        let mut cs = CacheStore::new(10);
199        cs.put("a", vec![1, 2, 3], 0);
200        cs.put("b", vec![4, 5], 0);
201        assert_eq!(cs.total_bytes(), 5);
202    }
203
204    #[test]
205    fn test_most_accessed() {
206        let mut cs = CacheStore::new(10);
207        cs.put("a", vec![1], 0);
208        cs.put("b", vec![2], 0);
209        cs.get("b");
210        cs.get("b");
211        cs.get("a");
212        assert_eq!(cs.most_accessed(), Some("b"));
213    }
214
215    #[test]
216    fn test_clear() {
217        let mut cs = CacheStore::new(10);
218        cs.put("a", vec![1], 0);
219        cs.clear();
220        assert_eq!(cs.count(), 0);
221    }
222}