tirea_state/
delta_tracked.rs

1/// A collection with cursor-based delta tracking.
2///
3/// `DeltaTracked<T>` wraps a `Vec<T>` and tracks which items have been
4/// consumed via `take_delta()`. This provides an efficient way to accumulate
5/// items over time and periodically extract only the new ones.
6///
7/// The cursor marks the position up to which items have been taken.
8/// `take_delta()` returns `items[cursor..]` and advances the cursor.
9#[derive(Debug, Clone)]
10pub struct DeltaTracked<T> {
11    items: Vec<T>,
12    cursor: usize,
13    /// Snapshot of items.len() at construction time. Unlike `cursor`, this
14    /// value is never mutated by `take_delta()`.
15    initial_len: usize,
16}
17
18impl<T> DeltaTracked<T> {
19    /// Create from existing items with cursor at the end (no pending delta).
20    pub fn new(items: Vec<T>) -> Self {
21        let len = items.len();
22        Self {
23            items,
24            cursor: len,
25            initial_len: len,
26        }
27    }
28
29    /// Create an empty tracker with cursor at 0.
30    pub fn empty() -> Self {
31        Self {
32            items: Vec::new(),
33            cursor: 0,
34            initial_len: 0,
35        }
36    }
37
38    /// Append a single item.
39    pub fn push(&mut self, item: T) {
40        self.items.push(item);
41    }
42
43    /// Append multiple items.
44    pub fn extend(&mut self, iter: impl IntoIterator<Item = T>) {
45        self.items.extend(iter);
46    }
47
48    /// View all items (including already-consumed ones).
49    pub fn as_slice(&self) -> &[T] {
50        &self.items
51    }
52
53    /// Total number of items.
54    pub fn len(&self) -> usize {
55        self.items.len()
56    }
57
58    /// Whether the collection is empty.
59    pub fn is_empty(&self) -> bool {
60        self.items.is_empty()
61    }
62
63    /// Consume and return the inner `Vec<T>`.
64    pub fn into_items(self) -> Vec<T> {
65        self.items
66    }
67
68    /// Number of items that existed before any push/extend (the initial set).
69    /// Stable across `take_delta()` calls.
70    pub fn initial_count(&self) -> usize {
71        self.initial_len
72    }
73
74    /// Whether there are items after the cursor.
75    pub fn has_delta(&self) -> bool {
76        self.cursor < self.items.len()
77    }
78}
79
80impl<T: Clone> DeltaTracked<T> {
81    /// Clone and return items added since the last `take_delta()`, then advance the cursor.
82    pub fn take_delta(&mut self) -> Vec<T> {
83        let delta = self.items[self.cursor..].to_vec();
84        self.cursor = self.items.len();
85        delta
86    }
87}
88
89impl<T> Default for DeltaTracked<T> {
90    fn default() -> Self {
91        Self::empty()
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn new_has_no_delta() {
101        let mut dt = DeltaTracked::new(vec![1, 2, 3]);
102        assert!(!dt.has_delta());
103        assert_eq!(dt.take_delta(), Vec::<i32>::new());
104        assert_eq!(dt.as_slice(), &[1, 2, 3]);
105    }
106
107    #[test]
108    fn empty_has_no_delta() {
109        let dt = DeltaTracked::<i32>::empty();
110        assert!(!dt.has_delta());
111        assert!(dt.is_empty());
112    }
113
114    #[test]
115    fn push_creates_delta() {
116        let mut dt = DeltaTracked::new(vec![1, 2]);
117        dt.push(3);
118        dt.push(4);
119        assert!(dt.has_delta());
120        assert_eq!(dt.take_delta(), vec![3, 4]);
121        assert!(!dt.has_delta());
122        assert_eq!(dt.as_slice(), &[1, 2, 3, 4]);
123    }
124
125    #[test]
126    fn extend_creates_delta() {
127        let mut dt = DeltaTracked::new(vec![1]);
128        dt.extend(vec![2, 3]);
129        assert_eq!(dt.take_delta(), vec![2, 3]);
130    }
131
132    #[test]
133    fn empty_then_push() {
134        let mut dt = DeltaTracked::empty();
135        dt.push(1);
136        dt.push(2);
137        assert!(dt.has_delta());
138        assert_eq!(dt.take_delta(), vec![1, 2]);
139        assert!(!dt.has_delta());
140    }
141
142    #[test]
143    fn multiple_takes() {
144        let mut dt = DeltaTracked::empty();
145        dt.push(1);
146        assert_eq!(dt.take_delta(), vec![1]);
147        dt.push(2);
148        dt.push(3);
149        assert_eq!(dt.take_delta(), vec![2, 3]);
150        assert_eq!(dt.take_delta(), Vec::<i32>::new());
151    }
152
153    #[test]
154    fn into_items() {
155        let mut dt = DeltaTracked::new(vec![1, 2]);
156        dt.push(3);
157        assert_eq!(dt.into_items(), vec![1, 2, 3]);
158    }
159
160    #[test]
161    fn len_and_is_empty() {
162        let mut dt = DeltaTracked::empty();
163        assert!(dt.is_empty());
164        assert_eq!(dt.len(), 0);
165        dt.push(1);
166        assert!(!dt.is_empty());
167        assert_eq!(dt.len(), 1);
168    }
169
170    #[test]
171    fn default_is_empty() {
172        let dt = DeltaTracked::<String>::default();
173        assert!(dt.is_empty());
174        assert!(!dt.has_delta());
175    }
176
177    #[test]
178    fn initial_count_stable_after_take_delta() {
179        let mut dt = DeltaTracked::new(vec![1, 2, 3]);
180        assert_eq!(dt.initial_count(), 3);
181
182        dt.push(4);
183        dt.push(5);
184        assert_eq!(dt.initial_count(), 3, "push must not change initial_count");
185
186        dt.take_delta();
187        assert_eq!(
188            dt.initial_count(),
189            3,
190            "take_delta must not change initial_count"
191        );
192
193        dt.push(6);
194        dt.take_delta();
195        assert_eq!(
196            dt.initial_count(),
197            3,
198            "multiple take_delta cycles must not change initial_count"
199        );
200    }
201
202    #[test]
203    fn initial_count_zero_for_empty() {
204        let mut dt = DeltaTracked::empty();
205        assert_eq!(dt.initial_count(), 0);
206        dt.push(1);
207        assert_eq!(dt.initial_count(), 0);
208        dt.take_delta();
209        assert_eq!(dt.initial_count(), 0);
210    }
211}