1use std::collections::VecDeque;
23
24use serde_json::Value;
25
26use super::ObservedEvent;
27
28#[derive(Debug)]
32pub struct TopicRing {
33 inner: VecDeque<ObservedEvent>,
34 capacity: usize,
35 base: u64,
36 next_cursor: u64,
37 head_cursor: u64,
38 reset_epoch: u64,
39 topic: String,
40}
41
42#[derive(Debug, Clone)]
44pub enum ReadResult {
45 Ok {
48 events: Vec<ObservedEvent>,
51 next_cursor: u64,
53 reset_epoch: u64,
56 },
57 CursorExpired {
62 head_cursor: u64,
65 reset_epoch: u64,
67 },
68}
69
70impl TopicRing {
71 #[must_use]
74 pub fn new(topic: impl Into<String>, capacity: usize, reset_epoch: u64) -> Self {
75 Self::with_base(topic, capacity, reset_epoch, 1)
76 }
77
78 #[must_use]
80 pub fn with_base(
81 topic: impl Into<String>,
82 capacity: usize,
83 reset_epoch: u64,
84 base: u64,
85 ) -> Self {
86 let base = base.max(1);
87 Self {
88 inner: VecDeque::with_capacity(capacity.max(1)),
89 capacity: capacity.max(1),
90 base,
91 next_cursor: base,
92 head_cursor: base,
93 reset_epoch,
94 topic: topic.into(),
95 }
96 }
97
98 #[must_use]
100 pub fn base(&self) -> u64 {
101 self.base
102 }
103
104 pub fn set_reset_epoch(&mut self, reset_epoch: u64) {
108 self.reset_epoch = reset_epoch;
109 }
110
111 #[must_use]
113 pub fn topic(&self) -> &str {
114 &self.topic
115 }
116
117 #[must_use]
119 pub fn reset_epoch(&self) -> u64 {
120 self.reset_epoch
121 }
122
123 #[must_use]
125 pub fn len(&self) -> usize {
126 self.inner.len()
127 }
128
129 #[must_use]
131 pub fn is_empty(&self) -> bool {
132 self.inner.is_empty()
133 }
134
135 #[must_use]
137 pub fn head_cursor(&self) -> u64 {
138 self.head_cursor
139 }
140
141 #[must_use]
143 pub fn next_cursor(&self) -> u64 {
144 self.next_cursor
145 }
146
147 pub fn push(&mut self, payload: Value, received_at: String) -> u64 {
151 let cursor = self.next_cursor;
152 self.next_cursor = self.next_cursor.saturating_add(1);
153
154 let event = ObservedEvent {
155 cursor,
156 topic: self.topic.clone(),
157 received_at,
158 reset_epoch: self.reset_epoch,
159 payload,
160 };
161
162 if self.inner.len() == self.capacity {
163 let evicted = self.inner.pop_front();
166 if evicted.is_some() {
167 self.head_cursor = self.inner.front().map(|e| e.cursor).unwrap_or(cursor);
170 }
171 }
172
173 self.inner.push_back(event);
174 if self.inner.len() == 1 {
176 self.head_cursor = cursor;
177 }
178
179 cursor
180 }
181
182 #[must_use]
192 pub fn read_since(&self, since: u64, limit: usize) -> ReadResult {
193 let last = self.next_cursor.saturating_sub(1);
194 if since > last {
195 return self.expired();
196 }
197 if since == last {
199 return ReadResult::Ok {
200 events: Vec::new(),
201 next_cursor: last,
202 reset_epoch: self.reset_epoch,
203 };
204 }
205 let since = since.max(self.base - 1);
206
207 if since + 1 < self.head_cursor {
210 return self.expired();
211 }
212
213 let events: Vec<ObservedEvent> = self
214 .inner
215 .iter()
216 .filter(|e| e.cursor > since)
217 .take(limit.max(1))
218 .cloned()
219 .collect();
220
221 let next_cursor = events.last().map_or(last, |e| e.cursor);
222
223 ReadResult::Ok {
224 events,
225 next_cursor,
226 reset_epoch: self.reset_epoch,
227 }
228 }
229
230 fn expired(&self) -> ReadResult {
231 ReadResult::CursorExpired {
232 head_cursor: self.head_cursor,
233 reset_epoch: self.reset_epoch,
234 }
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use serde_json::json;
242
243 fn ts() -> String {
244 "2026-05-06T00:00:00Z".to_string()
245 }
246
247 #[test]
248 fn push_assigns_monotonic_cursors() {
249 let mut ring = TopicRing::new("orders", 10, 0);
250 let c1 = ring.push(json!({"n": 1}), ts());
251 let c2 = ring.push(json!({"n": 2}), ts());
252 let c3 = ring.push(json!({"n": 3}), ts());
253 assert_eq!(c1, 1);
254 assert_eq!(c2, 2);
255 assert_eq!(c3, 3);
256 }
257
258 #[test]
259 fn read_since_zero_returns_all_buffered() {
260 let mut ring = TopicRing::new("orders", 10, 0);
261 ring.push(json!({"n": 1}), ts());
262 ring.push(json!({"n": 2}), ts());
263 match ring.read_since(0, 100) {
264 ReadResult::Ok {
265 events,
266 next_cursor,
267 ..
268 } => {
269 assert_eq!(events.len(), 2);
270 assert_eq!(next_cursor, 2);
271 assert_eq!(events[0].payload, json!({"n": 1}));
272 assert_eq!(events[1].payload, json!({"n": 2}));
273 }
274 other => panic!("expected Ok, got {other:?}"),
275 }
276 }
277
278 #[test]
279 fn read_since_returns_only_new_events() {
280 let mut ring = TopicRing::new("orders", 10, 0);
281 ring.push(json!({"n": 1}), ts());
282 ring.push(json!({"n": 2}), ts());
283 ring.push(json!({"n": 3}), ts());
284 match ring.read_since(1, 100) {
285 ReadResult::Ok {
286 events,
287 next_cursor,
288 ..
289 } => {
290 assert_eq!(events.len(), 2);
291 assert_eq!(events[0].cursor, 2);
292 assert_eq!(events[1].cursor, 3);
293 assert_eq!(next_cursor, 3);
294 }
295 other => panic!("expected Ok, got {other:?}"),
296 }
297 }
298
299 #[test]
300 fn read_since_caught_up_returns_empty() {
301 let mut ring = TopicRing::new("orders", 10, 0);
302 ring.push(json!({"n": 1}), ts());
303 ring.push(json!({"n": 2}), ts());
304 match ring.read_since(2, 100) {
305 ReadResult::Ok {
306 events,
307 next_cursor,
308 ..
309 } => {
310 assert!(events.is_empty());
311 assert_eq!(next_cursor, 2);
312 }
313 other => panic!("expected empty Ok, got {other:?}"),
314 }
315 }
316
317 #[test]
318 fn read_since_respects_limit() {
319 let mut ring = TopicRing::new("orders", 100, 0);
320 for i in 0..50 {
321 ring.push(json!({ "n": i }), ts());
322 }
323 match ring.read_since(0, 10) {
324 ReadResult::Ok {
325 events,
326 next_cursor,
327 ..
328 } => {
329 assert_eq!(events.len(), 10);
330 assert_eq!(next_cursor, 10);
331 }
332 other => panic!("expected Ok, got {other:?}"),
333 }
334 }
335
336 #[test]
337 fn capacity_evicts_oldest_first() {
338 let mut ring = TopicRing::new("orders", 3, 0);
339 for i in 1..=5 {
340 ring.push(json!({"n": i}), ts());
341 }
342 assert_eq!(ring.len(), 3);
344 assert_eq!(ring.head_cursor(), 3);
345 match ring.read_since(0, 100) {
346 ReadResult::CursorExpired { head_cursor, .. } => {
347 assert_eq!(head_cursor, 3);
348 }
349 other => panic!("expected CursorExpired, got {other:?}"),
350 }
351 }
352
353 #[test]
354 fn read_after_overflow_at_head_succeeds() {
355 let mut ring = TopicRing::new("orders", 3, 0);
356 for i in 1..=5 {
357 ring.push(json!({"n": i}), ts());
358 }
359 match ring.read_since(2, 100) {
361 ReadResult::Ok {
362 events,
363 next_cursor,
364 ..
365 } => {
366 assert_eq!(events.len(), 3);
367 assert_eq!(events[0].cursor, 3);
368 assert_eq!(next_cursor, 5);
369 }
370 other => panic!("expected Ok, got {other:?}"),
371 }
372 }
373
374 #[test]
375 fn read_at_or_past_overflow_returns_expired() {
376 let mut ring = TopicRing::new("orders", 3, 0);
377 for i in 1..=5 {
378 ring.push(json!({"n": i}), ts());
379 }
380 assert!(matches!(
382 ring.read_since(0, 100),
383 ReadResult::CursorExpired { .. }
384 ));
385 assert!(matches!(
386 ring.read_since(1, 100),
387 ReadResult::CursorExpired { .. }
388 ));
389 }
390
391 #[test]
392 fn reset_epoch_propagates_to_read_result() {
393 let mut ring = TopicRing::new("orders", 2, 42);
394 for i in 1..=4 {
395 ring.push(json!({"n": i}), ts());
396 }
397 match ring.read_since(0, 100) {
398 ReadResult::CursorExpired { reset_epoch, .. } => {
399 assert_eq!(reset_epoch, 42);
400 }
401 other => panic!("expected CursorExpired, got {other:?}"),
402 }
403 }
404
405 #[test]
406 fn reset_epoch_propagates_to_pushed_events() {
407 let mut ring = TopicRing::new("orders", 4, 7);
408 ring.push(json!({"n": 1}), ts());
409 match ring.read_since(0, 10) {
410 ReadResult::Ok { events, .. } => {
411 assert_eq!(events[0].reset_epoch, 7);
412 }
413 other => panic!("expected Ok, got {other:?}"),
414 }
415 }
416
417 #[test]
418 fn empty_ring_read_since_zero_is_ok_empty() {
419 let ring = TopicRing::new("orders", 10, 0);
420 match ring.read_since(0, 100) {
421 ReadResult::Ok {
422 events,
423 next_cursor,
424 ..
425 } => {
426 assert!(events.is_empty());
427 assert_eq!(next_cursor, 0);
428 }
429 other => panic!("expected Ok, got {other:?}"),
430 }
431 }
432
433 #[test]
434 fn a_cursor_from_the_future_is_expired_not_caught_up() {
435 let mut ring = TopicRing::new("orders", 10, 2);
439 for i in 1..=3 {
440 ring.push(json!({ "n": i }), ts());
441 }
442 match ring.read_since(500, 10) {
443 ReadResult::CursorExpired {
444 head_cursor,
445 reset_epoch,
446 } => {
447 assert_eq!(head_cursor, 1);
448 assert_eq!(reset_epoch, 2);
449 }
450 other => panic!("expected CursorExpired, got {other:?}"),
451 }
452 }
453
454 #[test]
455 fn cursors_start_at_the_base() {
456 let mut ring = TopicRing::with_base("orders", 10, 1, 1_000);
457 assert_eq!(ring.push(json!({}), ts()), 1_000);
458 assert_eq!(ring.push(json!({}), ts()), 1_001);
459 assert_eq!(ring.head_cursor(), 1_000);
460 }
461
462 #[test]
463 fn a_cursor_below_the_base_reads_from_the_start() {
464 let mut ring = TopicRing::with_base("orders", 10, 1, 1_000);
467 ring.push(json!({"n": 1}), ts());
468 ring.push(json!({"n": 2}), ts());
469 for since in [0, 1, 500, 999] {
470 match ring.read_since(since, 10) {
471 ReadResult::Ok {
472 events,
473 next_cursor,
474 ..
475 } => {
476 assert_eq!(events.len(), 2, "since={since}");
477 assert_eq!(next_cursor, 1_001);
478 }
479 other => panic!("since={since}: expected Ok, got {other:?}"),
480 }
481 }
482 }
483
484 #[test]
485 fn an_empty_ring_answers_with_its_own_cursor_not_the_callers() {
486 let ring = TopicRing::with_base("orders", 10, 4, 1_000);
487 match ring.read_since(17, 10) {
488 ReadResult::Ok {
489 events,
490 next_cursor,
491 reset_epoch,
492 } => {
493 assert!(events.is_empty());
494 assert_eq!(next_cursor, 999);
495 assert_eq!(reset_epoch, 4);
496 }
497 other => panic!("expected Ok, got {other:?}"),
498 }
499 assert!(matches!(
500 ring.read_since(5_000, 10),
501 ReadResult::CursorExpired { .. }
502 ));
503 }
504
505 #[test]
506 fn reads_carry_the_live_epoch_not_the_first_events() {
507 let mut ring = TopicRing::new("orders", 10, 1);
508 ring.push(json!({"n": 1}), ts());
509 ring.set_reset_epoch(2);
510 ring.push(json!({"n": 2}), ts());
511 match ring.read_since(0, 10) {
512 ReadResult::Ok {
513 events,
514 reset_epoch,
515 ..
516 } => {
517 assert_eq!(reset_epoch, 2, "the batch straddles a reconnect");
518 assert_eq!(events[0].reset_epoch, 1, "captured before it");
519 assert_eq!(events[1].reset_epoch, 2);
520 }
521 other => panic!("expected Ok, got {other:?}"),
522 }
523 match ring.read_since(2, 10) {
525 ReadResult::Ok { reset_epoch, .. } => assert_eq!(reset_epoch, 2),
526 other => panic!("expected Ok, got {other:?}"),
527 }
528 }
529
530 #[test]
531 fn topic_name_appears_on_pushed_events() {
532 let mut ring = TopicRing::new("marketdata:265598", 4, 0);
533 ring.push(json!({"31": "150.25"}), ts());
534 match ring.read_since(0, 10) {
535 ReadResult::Ok { events, .. } => {
536 assert_eq!(events[0].topic, "marketdata:265598");
537 }
538 other => panic!("expected Ok, got {other:?}"),
539 }
540 }
541}