Skip to main content

oxihuman_core/
key_cache.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Key cache: string-keyed LRU-style cache with TTL (time-to-live) in frames.
6
7use std::collections::HashMap;
8
9/// A single cache entry with value and expiry frame.
10#[derive(Debug, Clone)]
11#[allow(dead_code)]
12pub struct KeyCacheEntry<V> {
13    pub value: V,
14    pub expiry: u64,
15    pub hits: u32,
16}
17
18/// String-keyed cache.
19#[derive(Debug)]
20#[allow(dead_code)]
21pub struct KeyCache<V> {
22    entries: HashMap<String, KeyCacheEntry<V>>,
23    frame: u64,
24    capacity: usize,
25}
26
27/// Create a new KeyCache with given capacity.
28#[allow(dead_code)]
29pub fn new_key_cache<V>(capacity: usize) -> KeyCache<V> {
30    KeyCache {
31        entries: HashMap::new(),
32        frame: 0,
33        capacity,
34    }
35}
36
37/// Insert a key with TTL in frames.
38#[allow(dead_code)]
39pub fn kc_insert<V>(cache: &mut KeyCache<V>, key: &str, value: V, ttl: u64) {
40    if cache.entries.len() >= cache.capacity && !cache.entries.contains_key(key) {
41        // evict oldest expiry
42        if let Some(oldest) = cache
43            .entries
44            .iter()
45            .min_by_key(|(_, e)| e.expiry)
46            .map(|(k, _)| k.clone())
47        {
48            cache.entries.remove(&oldest);
49        }
50    }
51    cache.entries.insert(
52        key.to_string(),
53        KeyCacheEntry {
54            value,
55            expiry: cache.frame + ttl,
56            hits: 0,
57        },
58    );
59}
60
61/// Get a reference; returns None if expired or missing.
62#[allow(dead_code)]
63pub fn kc_get<'a, V>(cache: &'a mut KeyCache<V>, key: &str) -> Option<&'a V> {
64    let frame = cache.frame;
65    if let Some(e) = cache.entries.get_mut(key) {
66        if e.expiry >= frame {
67            e.hits += 1;
68            return Some(&e.value);
69        }
70    }
71    None
72}
73
74/// Advance frame counter; evicts expired entries.
75#[allow(dead_code)]
76pub fn kc_advance(cache: &mut KeyCache<impl std::fmt::Debug>, frames: u64) {
77    cache.frame += frames;
78    let frame = cache.frame;
79    cache.entries.retain(|_, e| e.expiry >= frame);
80}
81
82/// Remove a key explicitly.
83#[allow(dead_code)]
84pub fn kc_remove<V>(cache: &mut KeyCache<V>, key: &str) -> bool {
85    cache.entries.remove(key).is_some()
86}
87
88/// Whether key is present and not expired.
89#[allow(dead_code)]
90pub fn kc_contains<V>(cache: &mut KeyCache<V>, key: &str) -> bool {
91    kc_get(cache, key).is_some()
92}
93
94/// Number of cached entries.
95#[allow(dead_code)]
96pub fn kc_len<V>(cache: &KeyCache<V>) -> usize {
97    cache.entries.len()
98}
99
100/// Current frame.
101#[allow(dead_code)]
102pub fn kc_frame<V>(cache: &KeyCache<V>) -> u64 {
103    cache.frame
104}
105
106/// Hit count for a key.
107#[allow(dead_code)]
108pub fn kc_hits<V>(cache: &KeyCache<V>, key: &str) -> u32 {
109    cache.entries.get(key).map(|e| e.hits).unwrap_or(0)
110}
111
112/// Clear all entries.
113#[allow(dead_code)]
114pub fn kc_clear<V>(cache: &mut KeyCache<V>) {
115    cache.entries.clear();
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_insert_get() {
124        let mut c: KeyCache<u32> = new_key_cache(10);
125        kc_insert(&mut c, "a", 42, 5);
126        assert_eq!(kc_get(&mut c, "a"), Some(&42));
127    }
128
129    #[test]
130    fn test_expired() {
131        let mut c: KeyCache<u32> = new_key_cache(10);
132        kc_insert(&mut c, "x", 1, 2);
133        kc_advance(&mut c, 3);
134        assert_eq!(kc_get(&mut c, "x"), None);
135    }
136
137    #[test]
138    fn test_remove() {
139        let mut c: KeyCache<u32> = new_key_cache(10);
140        kc_insert(&mut c, "k", 5, 100);
141        assert!(kc_remove(&mut c, "k"));
142        assert_eq!(kc_get(&mut c, "k"), None);
143    }
144
145    #[test]
146    fn test_contains() {
147        let mut c: KeyCache<i32> = new_key_cache(10);
148        kc_insert(&mut c, "z", -1, 10);
149        assert!(kc_contains(&mut c, "z"));
150    }
151
152    #[test]
153    fn test_capacity_eviction() {
154        let mut c: KeyCache<u32> = new_key_cache(2);
155        kc_insert(&mut c, "a", 1, 100);
156        kc_insert(&mut c, "b", 2, 50);
157        kc_insert(&mut c, "c", 3, 200);
158        assert_eq!(kc_len(&c), 2);
159    }
160
161    #[test]
162    fn test_advance_evicts() {
163        let mut c: KeyCache<u32> = new_key_cache(10);
164        kc_insert(&mut c, "a", 1, 1);
165        kc_advance(&mut c, 2);
166        assert_eq!(kc_len(&c), 0);
167    }
168
169    #[test]
170    fn test_hits() {
171        let mut c: KeyCache<u32> = new_key_cache(10);
172        kc_insert(&mut c, "h", 7, 10);
173        kc_get(&mut c, "h");
174        kc_get(&mut c, "h");
175        assert_eq!(kc_hits(&c, "h"), 2);
176    }
177
178    #[test]
179    fn test_clear() {
180        let mut c: KeyCache<u32> = new_key_cache(10);
181        kc_insert(&mut c, "a", 1, 10);
182        kc_clear(&mut c);
183        assert_eq!(kc_len(&c), 0);
184    }
185
186    #[test]
187    fn test_frame_advances() {
188        let mut c: KeyCache<u32> = new_key_cache(10);
189        kc_advance(&mut c, 5);
190        assert_eq!(kc_frame(&c), 5);
191    }
192}