use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tokio::sync::mpsc;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StoredEvent {
pub id: String,
pub event_type: String,
pub data: String,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct StreamConfig {
pub max_events_per_stream: usize,
pub max_age: Duration,
pub channel_capacity: usize,
}
impl StreamConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn max_events_per_stream(mut self, max: usize) -> Self {
self.max_events_per_stream = max;
self
}
#[must_use]
pub const fn max_age(mut self, max_age: Duration) -> Self {
self.max_age = max_age;
self
}
#[must_use]
pub const fn channel_capacity(mut self, capacity: usize) -> Self {
self.channel_capacity = capacity;
self
}
}
impl Default for StreamConfig {
fn default() -> Self {
Self {
max_events_per_stream: 1000,
max_age: Duration::from_secs(300),
channel_capacity: 100,
}
}
}
#[derive(Debug)]
struct StreamSlot {
id: u64,
seq: u64,
buffer: VecDeque<(Instant, StoredEvent)>,
sender: Option<mpsc::Sender<StoredEvent>>,
opened: Instant,
died: Option<Instant>,
}
impl StreamSlot {
fn store(&mut self, event_type: &str, data: String, config: &StreamConfig) -> StoredEvent {
let event = StoredEvent {
id: format!("{}-{}", self.id, self.seq),
event_type: event_type.to_string(),
data,
};
self.seq += 1;
self.buffer.push_back((Instant::now(), event.clone()));
while self.buffer.len() > config.max_events_per_stream {
self.buffer.pop_front();
}
while self
.buffer
.front()
.is_some_and(|(at, _)| at.elapsed() > config.max_age)
{
self.buffer.pop_front();
}
event
}
}
#[derive(Debug)]
pub struct StreamRegistry {
inner: Mutex<Inner>,
config: StreamConfig,
}
#[derive(Debug)]
struct Inner {
streams: Vec<StreamSlot>,
next_stream_id: u64,
}
#[derive(Debug)]
pub struct StreamHandle {
stream_id: u64,
rx: mpsc::Receiver<StoredEvent>,
registry: Arc<StreamRegistry>,
}
impl StreamHandle {
#[must_use]
pub const fn stream_id(&self) -> u64 {
self.stream_id
}
pub async fn recv(&mut self) -> Option<StoredEvent> {
self.rx.recv().await
}
}
impl Drop for StreamHandle {
fn drop(&mut self) {
self.registry.mark_dead(self.stream_id);
}
}
impl StreamRegistry {
#[must_use]
pub fn new(config: StreamConfig) -> Self {
Self {
inner: Mutex::new(Inner {
streams: Vec::new(),
next_stream_id: 1,
}),
config,
}
}
pub fn open(
self: &Arc<Self>,
prime_event_type: &str,
prime_data: String,
) -> (StreamHandle, StoredEvent) {
let (tx, rx) = mpsc::channel(self.config.channel_capacity);
let mut inner = self.inner.lock().expect("stream registry lock");
Self::reap(&mut inner, &self.config);
let id = inner.next_stream_id;
inner.next_stream_id += 1;
let mut slot = StreamSlot {
id,
seq: 0,
buffer: VecDeque::new(),
sender: Some(tx),
opened: Instant::now(),
died: None,
};
let prime = slot.store(prime_event_type, prime_data, &self.config);
inner.streams.push(slot);
drop(inner);
(
StreamHandle {
stream_id: id,
rx,
registry: Arc::clone(self),
},
prime,
)
}
pub fn resume(
self: &Arc<Self>,
last_event_id: &str,
) -> Option<(StreamHandle, Vec<StoredEvent>)> {
let (stream_id, seq) = parse_event_id(last_event_id)?;
let (tx, rx) = mpsc::channel(self.config.channel_capacity);
let mut inner = self.inner.lock().expect("stream registry lock");
Self::reap(&mut inner, &self.config);
let slot = inner.streams.iter_mut().find(|s| s.id == stream_id)?;
slot.sender = Some(tx);
slot.died = None;
let replay = slot
.buffer
.iter()
.filter(|(_, e)| parse_event_id(&e.id).is_some_and(|(_, s)| s > seq))
.map(|(_, e)| e.clone())
.collect();
drop(inner);
Some((
StreamHandle {
stream_id,
rx,
registry: Arc::clone(self),
},
replay,
))
}
#[must_use]
pub fn send(&self, event_type: &str, data: String) -> Option<String> {
let mut inner = self.inner.lock().expect("stream registry lock");
Self::reap(&mut inner, &self.config);
let config = &self.config;
let slot = inner
.streams
.iter_mut()
.filter(|s| s.sender.is_some())
.min_by_key(|s| s.opened)?;
let event = slot.store(event_type, data, config);
if let Some(sender) = &slot.sender {
match sender.try_send(event.clone()) {
Ok(()) => {}
Err(mpsc::error::TrySendError::Full(_) | mpsc::error::TrySendError::Closed(_)) => {
slot.sender = None;
slot.died = Some(Instant::now());
}
}
}
Some(event.id)
}
#[must_use]
pub fn has_live_stream(&self) -> bool {
self.inner
.lock()
.expect("stream registry lock")
.streams
.iter()
.any(|s| s.sender.is_some())
}
fn mark_dead(&self, stream_id: u64) {
if let Ok(mut inner) = self.inner.lock() {
if let Some(slot) = inner.streams.iter_mut().find(|s| s.id == stream_id) {
slot.sender = None;
slot.died = Some(Instant::now());
}
}
}
fn reap(inner: &mut Inner, config: &StreamConfig) {
inner.streams.retain(|s| {
s.sender.is_some() || s.died.is_none_or(|at| at.elapsed() < config.max_age)
});
}
}
fn parse_event_id(id: &str) -> Option<(u64, u64)> {
let (stream, seq) = id.split_once('-')?;
Some((stream.parse().ok()?, seq.parse().ok()?))
}
#[cfg(test)]
mod tests {
use super::*;
fn registry() -> Arc<StreamRegistry> {
Arc::new(StreamRegistry::new(StreamConfig::default()))
}
#[tokio::test]
async fn send_delivers_to_exactly_one_stream() {
let reg = registry();
let (mut a, _) = reg.open("connected", "sid".into());
let (mut b, _) = reg.open("connected", "sid".into());
let id = reg.send("message", "hello".into()).expect("live stream");
let got = a.recv().await.expect("delivered");
assert_eq!(got.id, id);
assert_eq!(got.data, "hello");
assert!(
tokio::time::timeout(Duration::from_millis(50), b.recv())
.await
.is_err(),
"second stream must not receive the message"
);
}
#[tokio::test]
async fn event_ids_encode_stream_and_sequence() {
let reg = registry();
let (_a, prime) = reg.open("connected", "sid".into());
assert_eq!(prime.id, "1-0");
let id1 = reg.send("message", "x".into()).unwrap();
let id2 = reg.send("message", "y".into()).unwrap();
assert_eq!(id1, "1-1");
assert_eq!(id2, "1-2");
}
#[tokio::test]
async fn resume_replays_only_same_stream_events_after_cursor() {
let reg = registry();
let (a, _) = reg.open("connected", "sid".into());
let id1 = reg.send("message", "one".into()).unwrap();
let _id2 = reg.send("message", "two".into()).unwrap();
drop(a);
let (_b, _) = reg.open("connected", "sid".into());
let _ = reg.send("message", "other-stream".into()).unwrap();
let (_a2, replay) = reg.resume(&id1).expect("resumable");
assert_eq!(replay.len(), 1, "only events after the cursor: {replay:?}");
assert_eq!(replay[0].data, "two");
}
#[tokio::test]
async fn resumed_stream_keeps_designation() {
let reg = registry();
let (a, prime) = reg.open("connected", "sid".into());
let (_b, _) = reg.open("connected", "sid".into());
drop(a);
let (mut a2, _) = reg.resume(&prime.id).expect("resumable");
let id = reg.send("message", "after-resume".into()).unwrap();
assert!(
id.starts_with("1-"),
"designated must still be stream 1: {id}"
);
assert_eq!(a2.recv().await.unwrap().data, "after-resume");
}
#[tokio::test]
async fn overflow_kills_stream_and_replay_recovers() {
let reg = Arc::new(StreamRegistry::new(StreamConfig {
channel_capacity: 2,
..StreamConfig::default()
}));
let (a, prime) = reg.open("connected", "sid".into());
let _ = reg.send("message", "m1".into()).unwrap();
let _ = reg.send("message", "m2".into()).unwrap();
let id3 = reg.send("message", "m3".into()).unwrap();
assert!(!reg.has_live_stream(), "overflow must kill the stream");
drop(a);
let (_a2, replay) = reg.resume(&prime.id).expect("resumable");
assert_eq!(replay.last().map(|e| e.id.as_str()), Some(id3.as_str()));
assert_eq!(replay.len(), 3);
}
#[tokio::test]
async fn no_live_stream_returns_none() {
let reg = registry();
assert!(reg.send("message", "x".into()).is_none());
let (a, _) = reg.open("connected", "sid".into());
drop(a);
assert!(
reg.send("message", "x".into()).is_none(),
"a dead stream is not a delivery target"
);
}
#[tokio::test]
async fn dead_stream_buffer_is_reaped_after_max_age() {
let reg = Arc::new(StreamRegistry::new(StreamConfig {
max_age: Duration::from_millis(10),
..StreamConfig::default()
}));
let (a, prime) = reg.open("connected", "sid".into());
drop(a);
tokio::time::sleep(Duration::from_millis(30)).await;
let _ = reg.send("message", "x".into());
assert!(
reg.resume(&prime.id).is_none(),
"expired dead stream must not be resumable"
);
}
#[tokio::test]
async fn handle_drop_marks_stream_dead() {
let reg = registry();
let (a, _) = reg.open("connected", "sid".into());
assert!(reg.has_live_stream());
drop(a);
assert!(!reg.has_live_stream());
}
}