1use std::collections::VecDeque;
17
18use serde_json::Value;
19
20use super::ObservedEvent;
21
22#[derive(Debug)]
26pub struct TopicRing {
27 inner: VecDeque<ObservedEvent>,
28 capacity: usize,
29 next_cursor: u64,
30 head_cursor: u64,
31 reset_epoch: u64,
32 topic: String,
33}
34
35#[derive(Debug, Clone)]
37pub enum ReadResult {
38 Ok {
41 events: Vec<ObservedEvent>,
44 next_cursor: u64,
46 },
47 CursorExpired {
50 head_cursor: u64,
53 reset_epoch: u64,
55 },
56}
57
58impl TopicRing {
59 #[must_use]
62 pub fn new(topic: impl Into<String>, capacity: usize, reset_epoch: u64) -> Self {
63 Self {
64 inner: VecDeque::with_capacity(capacity.max(1)),
65 capacity: capacity.max(1),
66 next_cursor: 1,
67 head_cursor: 1,
68 reset_epoch,
69 topic: topic.into(),
70 }
71 }
72
73 #[must_use]
75 pub fn topic(&self) -> &str {
76 &self.topic
77 }
78
79 #[must_use]
81 pub fn reset_epoch(&self) -> u64 {
82 self.reset_epoch
83 }
84
85 #[must_use]
87 pub fn len(&self) -> usize {
88 self.inner.len()
89 }
90
91 #[must_use]
93 pub fn is_empty(&self) -> bool {
94 self.inner.is_empty()
95 }
96
97 #[must_use]
99 pub fn head_cursor(&self) -> u64 {
100 self.head_cursor
101 }
102
103 #[must_use]
105 pub fn next_cursor(&self) -> u64 {
106 self.next_cursor
107 }
108
109 pub fn push(&mut self, payload: Value, received_at: String) -> u64 {
113 let cursor = self.next_cursor;
114 self.next_cursor = self.next_cursor.saturating_add(1);
115
116 let event = ObservedEvent {
117 cursor,
118 topic: self.topic.clone(),
119 received_at,
120 reset_epoch: self.reset_epoch,
121 payload,
122 };
123
124 if self.inner.len() == self.capacity {
125 let evicted = self.inner.pop_front();
128 if evicted.is_some() {
129 self.head_cursor = self.inner.front().map(|e| e.cursor).unwrap_or(cursor);
132 }
133 }
134
135 self.inner.push_back(event);
136 if self.inner.len() == 1 {
138 self.head_cursor = cursor;
139 }
140
141 cursor
142 }
143
144 #[must_use]
151 pub fn read_since(&self, since: u64, limit: usize) -> ReadResult {
152 if since >= self.next_cursor.saturating_sub(1) {
154 return ReadResult::Ok {
155 events: Vec::new(),
156 next_cursor: self.next_cursor.saturating_sub(1),
157 };
158 }
159
160 if since + 1 < self.head_cursor {
163 return ReadResult::CursorExpired {
164 head_cursor: self.head_cursor,
165 reset_epoch: self.reset_epoch,
166 };
167 }
168
169 let events: Vec<ObservedEvent> = self
170 .inner
171 .iter()
172 .filter(|e| e.cursor > since)
173 .take(limit.max(1))
174 .cloned()
175 .collect();
176
177 let next_cursor = events
178 .last()
179 .map(|e| e.cursor)
180 .unwrap_or_else(|| self.next_cursor.saturating_sub(1));
181
182 ReadResult::Ok {
183 events,
184 next_cursor,
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use serde_json::json;
193
194 fn ts() -> String {
195 "2026-05-06T00:00:00Z".to_string()
196 }
197
198 #[test]
199 fn push_assigns_monotonic_cursors() {
200 let mut ring = TopicRing::new("orders", 10, 0);
201 let c1 = ring.push(json!({"n": 1}), ts());
202 let c2 = ring.push(json!({"n": 2}), ts());
203 let c3 = ring.push(json!({"n": 3}), ts());
204 assert_eq!(c1, 1);
205 assert_eq!(c2, 2);
206 assert_eq!(c3, 3);
207 }
208
209 #[test]
210 fn read_since_zero_returns_all_buffered() {
211 let mut ring = TopicRing::new("orders", 10, 0);
212 ring.push(json!({"n": 1}), ts());
213 ring.push(json!({"n": 2}), ts());
214 match ring.read_since(0, 100) {
215 ReadResult::Ok {
216 events,
217 next_cursor,
218 } => {
219 assert_eq!(events.len(), 2);
220 assert_eq!(next_cursor, 2);
221 assert_eq!(events[0].payload, json!({"n": 1}));
222 assert_eq!(events[1].payload, json!({"n": 2}));
223 }
224 other => panic!("expected Ok, got {other:?}"),
225 }
226 }
227
228 #[test]
229 fn read_since_returns_only_new_events() {
230 let mut ring = TopicRing::new("orders", 10, 0);
231 ring.push(json!({"n": 1}), ts());
232 ring.push(json!({"n": 2}), ts());
233 ring.push(json!({"n": 3}), ts());
234 match ring.read_since(1, 100) {
235 ReadResult::Ok {
236 events,
237 next_cursor,
238 } => {
239 assert_eq!(events.len(), 2);
240 assert_eq!(events[0].cursor, 2);
241 assert_eq!(events[1].cursor, 3);
242 assert_eq!(next_cursor, 3);
243 }
244 other => panic!("expected Ok, got {other:?}"),
245 }
246 }
247
248 #[test]
249 fn read_since_caught_up_returns_empty() {
250 let mut ring = TopicRing::new("orders", 10, 0);
251 ring.push(json!({"n": 1}), ts());
252 ring.push(json!({"n": 2}), ts());
253 match ring.read_since(2, 100) {
254 ReadResult::Ok {
255 events,
256 next_cursor,
257 } => {
258 assert!(events.is_empty());
259 assert_eq!(next_cursor, 2);
260 }
261 other => panic!("expected empty Ok, got {other:?}"),
262 }
263 }
264
265 #[test]
266 fn read_since_respects_limit() {
267 let mut ring = TopicRing::new("orders", 100, 0);
268 for i in 0..50 {
269 ring.push(json!({ "n": i }), ts());
270 }
271 match ring.read_since(0, 10) {
272 ReadResult::Ok {
273 events,
274 next_cursor,
275 } => {
276 assert_eq!(events.len(), 10);
277 assert_eq!(next_cursor, 10);
278 }
279 other => panic!("expected Ok, got {other:?}"),
280 }
281 }
282
283 #[test]
284 fn capacity_evicts_oldest_first() {
285 let mut ring = TopicRing::new("orders", 3, 0);
286 for i in 1..=5 {
287 ring.push(json!({"n": i}), ts());
288 }
289 assert_eq!(ring.len(), 3);
291 assert_eq!(ring.head_cursor(), 3);
292 match ring.read_since(0, 100) {
293 ReadResult::CursorExpired { head_cursor, .. } => {
294 assert_eq!(head_cursor, 3);
295 }
296 other => panic!("expected CursorExpired, got {other:?}"),
297 }
298 }
299
300 #[test]
301 fn read_after_overflow_at_head_succeeds() {
302 let mut ring = TopicRing::new("orders", 3, 0);
303 for i in 1..=5 {
304 ring.push(json!({"n": i}), ts());
305 }
306 match ring.read_since(2, 100) {
308 ReadResult::Ok {
309 events,
310 next_cursor,
311 } => {
312 assert_eq!(events.len(), 3);
313 assert_eq!(events[0].cursor, 3);
314 assert_eq!(next_cursor, 5);
315 }
316 other => panic!("expected Ok, got {other:?}"),
317 }
318 }
319
320 #[test]
321 fn read_at_or_past_overflow_returns_expired() {
322 let mut ring = TopicRing::new("orders", 3, 0);
323 for i in 1..=5 {
324 ring.push(json!({"n": i}), ts());
325 }
326 assert!(matches!(
328 ring.read_since(0, 100),
329 ReadResult::CursorExpired { .. }
330 ));
331 assert!(matches!(
332 ring.read_since(1, 100),
333 ReadResult::CursorExpired { .. }
334 ));
335 }
336
337 #[test]
338 fn reset_epoch_propagates_to_read_result() {
339 let mut ring = TopicRing::new("orders", 2, 42);
340 for i in 1..=4 {
341 ring.push(json!({"n": i}), ts());
342 }
343 match ring.read_since(0, 100) {
344 ReadResult::CursorExpired { reset_epoch, .. } => {
345 assert_eq!(reset_epoch, 42);
346 }
347 other => panic!("expected CursorExpired, got {other:?}"),
348 }
349 }
350
351 #[test]
352 fn reset_epoch_propagates_to_pushed_events() {
353 let mut ring = TopicRing::new("orders", 4, 7);
354 ring.push(json!({"n": 1}), ts());
355 match ring.read_since(0, 10) {
356 ReadResult::Ok { events, .. } => {
357 assert_eq!(events[0].reset_epoch, 7);
358 }
359 other => panic!("expected Ok, got {other:?}"),
360 }
361 }
362
363 #[test]
364 fn empty_ring_read_since_zero_is_ok_empty() {
365 let ring = TopicRing::new("orders", 10, 0);
366 match ring.read_since(0, 100) {
367 ReadResult::Ok {
368 events,
369 next_cursor,
370 } => {
371 assert!(events.is_empty());
372 assert_eq!(next_cursor, 0);
373 }
374 other => panic!("expected Ok, got {other:?}"),
375 }
376 }
377
378 #[test]
379 fn topic_name_appears_on_pushed_events() {
380 let mut ring = TopicRing::new("marketdata:265598", 4, 0);
381 ring.push(json!({"31": "150.25"}), ts());
382 match ring.read_since(0, 10) {
383 ReadResult::Ok { events, .. } => {
384 assert_eq!(events[0].topic, "marketdata:265598");
385 }
386 other => panic!("expected Ok, got {other:?}"),
387 }
388 }
389}