Skip to main content

lds_core/
log_store.rs

1//! Generic in-memory log store with fixed capacity.
2//!
3//! Shared across modules (recipe, git, sandbox) for execution history.
4//! Backed by a `Mutex<VecDeque<T>>` ring buffer — oldest entries are
5//! evicted when capacity is reached.
6
7use std::collections::VecDeque;
8use std::sync::Mutex;
9
10pub trait HasId {
11    fn id(&self) -> &str;
12}
13
14#[derive(Debug)]
15pub struct LogStore<T> {
16    entries: Mutex<VecDeque<T>>,
17    capacity: usize,
18}
19
20impl<T: HasId + Clone> LogStore<T> {
21    pub fn new(capacity: usize) -> Self {
22        Self {
23            entries: Mutex::new(VecDeque::with_capacity(capacity)),
24            capacity,
25        }
26    }
27
28    pub fn push(&self, entry: T) {
29        let mut entries = self.entries.lock().unwrap();
30        if entries.len() >= self.capacity {
31            entries.pop_front();
32        }
33        entries.push_back(entry);
34    }
35
36    pub fn get(&self, id: &str) -> Option<T> {
37        let entries = self.entries.lock().unwrap();
38        entries.iter().find(|e| e.id() == id).cloned()
39    }
40
41    pub fn recent(&self, n: usize) -> Vec<T> {
42        let entries = self.entries.lock().unwrap();
43        entries.iter().rev().take(n).cloned().collect()
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    #[derive(Debug, Clone)]
52    struct Entry {
53        id: String,
54        value: i32,
55    }
56
57    impl HasId for Entry {
58        fn id(&self) -> &str {
59            &self.id
60        }
61    }
62
63    #[test]
64    fn push_and_get() {
65        let store = LogStore::new(10);
66        store.push(Entry {
67            id: "a".into(),
68            value: 1,
69        });
70        store.push(Entry {
71            id: "b".into(),
72            value: 2,
73        });
74        assert_eq!(store.get("a").unwrap().value, 1);
75        assert_eq!(store.get("b").unwrap().value, 2);
76    }
77
78    #[test]
79    fn eviction_at_capacity() {
80        let store = LogStore::new(2);
81        store.push(Entry {
82            id: "a".into(),
83            value: 1,
84        });
85        store.push(Entry {
86            id: "b".into(),
87            value: 2,
88        });
89        store.push(Entry {
90            id: "c".into(),
91            value: 3,
92        });
93        assert!(store.get("a").is_none());
94        assert_eq!(store.get("b").unwrap().value, 2);
95        assert_eq!(store.get("c").unwrap().value, 3);
96    }
97
98    #[test]
99    fn recent_newest_first() {
100        let store = LogStore::new(10);
101        store.push(Entry {
102            id: "a".into(),
103            value: 1,
104        });
105        store.push(Entry {
106            id: "b".into(),
107            value: 2,
108        });
109        store.push(Entry {
110            id: "c".into(),
111            value: 3,
112        });
113        let recent = store.recent(2);
114        assert_eq!(recent.len(), 2);
115        assert_eq!(recent[0].id, "c");
116        assert_eq!(recent[1].id, "b");
117    }
118
119    #[test]
120    fn get_missing_returns_none() {
121        let store: LogStore<Entry> = LogStore::new(10);
122        assert!(store.get("nonexistent").is_none());
123    }
124}