use std::collections::VecDeque;
use std::sync::Mutex;
use serde::Serialize;
use serde_json::{json, Value};
use tokio::sync::broadcast;
pub const BUFFER_CAPACITY: usize = 4096;
pub const DEFAULT_PAGE: usize = 200;
pub const MAX_PAGE: usize = 1000;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct DevEvent {
pub seq: u64,
#[serde(rename = "type")]
pub kind: String,
pub at: String,
pub data: Value,
}
impl DevEvent {
pub fn ephemeral(&self) -> bool {
self.seq == 0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cursor {
pub boot: Option<String>,
pub seq: u64,
}
impl Cursor {
pub fn parse(raw: &str) -> Option<Self> {
let raw = raw.trim();
match raw.rsplit_once(':') {
Some((boot, seq)) if !boot.is_empty() => Some(Self {
boot: Some(boot.to_string()),
seq: seq.parse().ok()?,
}),
Some(_) => None,
None => Some(Self {
boot: None,
seq: raw.parse().ok()?,
}),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Replay {
pub events: Vec<DevEvent>,
pub reset: bool,
pub more: bool,
pub head: u64,
}
struct Inner {
next: u64,
buffer: VecDeque<DevEvent>,
}
pub struct EventHub {
boot: String,
capacity: usize,
inner: Mutex<Inner>,
live: broadcast::Sender<DevEvent>,
}
impl EventHub {
pub fn new() -> Self {
Self::with_capacity(BUFFER_CAPACITY)
}
pub fn with_capacity(capacity: usize) -> Self {
let mut bytes = [0u8; 6];
let _ = getrandom::getrandom(&mut bytes);
let boot: String = bytes.iter().map(|byte| format!("{byte:02x}")).collect();
let (live, _) = broadcast::channel(1024);
Self {
boot,
capacity: capacity.max(1),
inner: Mutex::new(Inner {
next: 1,
buffer: VecDeque::new(),
}),
live,
}
}
pub fn boot(&self) -> &str {
&self.boot
}
pub fn event_id(&self, event: &DevEvent) -> String {
format!("{}:{}", self.boot, event.seq)
}
pub fn head(&self) -> String {
let inner = self.lock();
self.cursor_at(inner.next - 1)
}
pub fn cursor_at(&self, seq: u64) -> String {
format!("{}:{}", self.boot, seq)
}
fn lock(&self) -> std::sync::MutexGuard<'_, Inner> {
self.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub fn publish(&self, kind: &str, data: Value) -> DevEvent {
let mut inner = self.lock();
let event = DevEvent {
seq: inner.next,
kind: kind.to_string(),
at: now(),
data,
};
inner.next += 1;
inner.buffer.push_back(event.clone());
while inner.buffer.len() > self.capacity {
inner.buffer.pop_front();
}
let _ = self.live.send(event.clone());
event
}
pub fn publish_ephemeral(&self, kind: &str, data: Value) {
let _ = self.live.send(DevEvent {
seq: 0,
kind: kind.to_string(),
at: now(),
data,
});
}
pub fn since(&self, cursor: Option<&Cursor>, limit: usize) -> Replay {
let inner = self.lock();
self.since_locked(&inner, cursor, limit)
}
fn since_locked(&self, inner: &Inner, cursor: Option<&Cursor>, limit: usize) -> Replay {
let oldest = inner.buffer.front().map(|event| event.seq);
let (after, mut reset) = match cursor {
None => (0, false),
Some(cursor) if cursor.boot.as_deref().is_some_and(|boot| boot != self.boot) => {
(0, true)
}
Some(cursor) if cursor.seq >= inner.next => (0, true),
Some(cursor) => (cursor.seq, false),
};
if let Some(oldest) = oldest {
if after + 1 < oldest && cursor.is_some() {
reset = true;
}
}
let mut events: Vec<DevEvent> = inner
.buffer
.iter()
.filter(|event| event.seq > after)
.take(limit.saturating_add(1))
.cloned()
.collect();
let more = events.len() > limit;
events.truncate(limit);
Replay {
events,
reset,
more,
head: inner.next - 1,
}
}
pub fn subscribe(&self, cursor: Option<&Cursor>) -> (Replay, broadcast::Receiver<DevEvent>) {
let inner = self.lock();
let replay = self.since_locked(&inner, cursor, usize::MAX - 1);
(replay, self.live.subscribe())
}
pub fn listen(&self) -> broadcast::Receiver<DevEvent> {
self.live.subscribe()
}
}
impl Default for EventHub {
fn default() -> Self {
Self::new()
}
}
pub fn now() -> String {
chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
}
pub fn reset_event(hub: &EventHub) -> DevEvent {
DevEvent {
seq: 0,
kind: "stream.reset".into(),
at: now(),
data: json!({ "boot": hub.boot(), "reason": "The events after your cursor are no longer buffered; reload the protocol and instances." }),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn numbers_buffers_and_replays_after_a_cursor() {
let hub = EventHub::with_capacity(3);
for index in 0..5 {
hub.publish("step.prepared", json!({ "index": index }));
}
let all = hub.since(None, 100);
assert_eq!(
all.events.iter().map(|e| e.seq).collect::<Vec<_>>(),
vec![3, 4, 5]
);
assert!(!all.reset);
let cursor = Cursor::parse(&format!("{}:4", hub.boot())).unwrap();
let after = hub.since(Some(&cursor), 100);
assert_eq!(after.events.len(), 1);
assert_eq!(after.events[0].seq, 5);
assert!(!after.reset);
let stale = hub.since(Some(&Cursor::parse("1").unwrap()), 100);
assert!(stale.reset);
assert_eq!(stale.events.len(), 3);
let other = hub.since(Some(&Cursor::parse("abc:4").unwrap()), 100);
assert!(other.reset);
assert_eq!(other.events.len(), 3);
let page = hub.since(None, 2);
assert_eq!(page.events.len(), 2);
assert!(page.more);
assert_eq!(hub.head(), format!("{}:5", hub.boot()));
}
#[test]
fn ephemeral_events_reach_live_readers_only() {
let hub = EventHub::new();
let (replay, mut receiver) = hub.subscribe(None);
assert!(replay.events.is_empty());
hub.publish_ephemeral("chain.clock", json!({ "slot": 1 }));
hub.publish("build.started", json!({}));
let first = receiver.try_recv().unwrap();
assert!(first.ephemeral());
assert_eq!(receiver.try_recv().unwrap().seq, 1);
assert_eq!(hub.since(None, 10).events.len(), 1);
}
#[test]
fn parses_cursors() {
assert_eq!(
Cursor::parse("b00t:12"),
Some(Cursor {
boot: Some("b00t".into()),
seq: 12
})
);
assert_eq!(Cursor::parse("7").unwrap().seq, 7);
assert!(Cursor::parse(":7").is_none());
assert!(Cursor::parse("x:y").is_none());
assert!(Cursor::parse("").is_none());
}
}