use std::collections::VecDeque;
use std::pin::Pin;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError, Weak};
use std::task::{Context, Poll, Waker};
use std::time::{Duration, Instant};
use futures::Stream;
use crate::events::{AgentSessionEvent, ClientEvent, OinoEvent};
pub const SUBSCRIBER_CAPACITY: usize = 1024;
pub const LAG_LIMIT: Duration = Duration::from_secs(5);
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}
#[derive(Default)]
struct Queue {
events: VecDeque<ClientEvent>,
waker: Option<Waker>,
over_since: Option<Instant>,
dropped: u64,
closed: bool,
}
impl Queue {
fn push(&mut self, event: ClientEvent, capacity: usize) {
if self.closed {
return;
}
if self.events.len() >= capacity {
if self.coalesce(&event) {
self.dropped += 1;
self.wake();
return;
}
let since = *self.over_since.get_or_insert_with(Instant::now);
if since.elapsed() >= LAG_LIMIT {
self.events.push_back(ClientEvent::Oino(OinoEvent::Lagged {
dropped: self.dropped,
}));
self.closed = true;
self.wake();
return;
}
} else {
self.over_since = None;
}
self.events.push_back(event);
self.wake();
}
fn coalesce(&mut self, event: &ClientEvent) -> bool {
let Some(last) = self.events.back_mut() else {
return false;
};
let replace = match (&*last, event) {
(
ClientEvent::Session(AgentSessionEvent::MessageUpdate { .. }),
ClientEvent::Session(AgentSessionEvent::MessageUpdate { .. }),
) => true,
(
ClientEvent::Session(AgentSessionEvent::ToolExecutionUpdate {
tool_call_id: a,
..
}),
ClientEvent::Session(AgentSessionEvent::ToolExecutionUpdate {
tool_call_id: b,
..
}),
) => a == b,
(
ClientEvent::Oino(OinoEvent::AgentEvent {
agent_id: a,
event: first,
}),
ClientEvent::Oino(OinoEvent::AgentEvent {
agent_id: b,
event: second,
}),
) => {
a == b
&& matches!(
(&**first, &**second),
(
ClientEvent::Session(AgentSessionEvent::MessageUpdate { .. }),
ClientEvent::Session(AgentSessionEvent::MessageUpdate { .. })
)
)
}
_ => false,
};
if replace {
*last = event.clone();
return true;
}
if let (
ClientEvent::Session(AgentSessionEvent::BashExecutionUpdate {
id: a,
delta: existing,
}),
ClientEvent::Session(AgentSessionEvent::BashExecutionUpdate { id: b, delta }),
) = (last, event)
&& a == b
{
existing.push_str(delta);
return true;
}
false
}
fn wake(&mut self) {
if let Some(waker) = self.waker.take() {
waker.wake();
}
}
}
#[derive(Clone, Default)]
pub struct EventHub {
subscribers: Arc<Mutex<Vec<Weak<Mutex<Queue>>>>>,
capacity: Option<usize>,
}
impl std::fmt::Debug for EventHub {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EventHub")
.field("subscribers", &lock(&self.subscribers).len())
.finish_non_exhaustive()
}
}
impl EventHub {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
subscribers: Arc::default(),
capacity: Some(capacity.max(1)),
}
}
#[must_use]
pub fn subscribe(&self) -> EventStream {
let queue = Arc::new(Mutex::new(Queue::default()));
lock(&self.subscribers).push(Arc::downgrade(&queue));
EventStream { queue }
}
pub fn publish(&self, event: impl Into<ClientEvent>) {
let event = event.into();
let capacity = self.capacity.unwrap_or(SUBSCRIBER_CAPACITY);
let mut subscribers = lock(&self.subscribers);
subscribers.retain(|weak| {
let Some(queue) = weak.upgrade() else {
return false;
};
let mut queue = lock(&queue);
queue.push(event.clone(), capacity);
!queue.closed
});
}
pub fn close(&self) {
for weak in lock(&self.subscribers).drain(..) {
if let Some(queue) = weak.upgrade() {
let mut queue = lock(&queue);
queue.closed = true;
queue.wake();
}
}
}
#[must_use]
pub fn subscriber_count(&self) -> usize {
lock(&self.subscribers)
.iter()
.filter(|w| w.strong_count() > 0)
.count()
}
}
pub struct EventStream {
queue: Arc<Mutex<Queue>>,
}
impl std::fmt::Debug for EventStream {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EventStream")
.field("queued", &lock(&self.queue).events.len())
.finish()
}
}
impl EventStream {
#[must_use]
pub fn drain_now(&self) -> Vec<ClientEvent> {
lock(&self.queue).events.drain(..).collect()
}
}
impl Stream for EventStream {
type Item = ClientEvent;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<ClientEvent>> {
let mut queue = lock(&self.queue);
if let Some(event) = queue.events.pop_front() {
return Poll::Ready(Some(event));
}
if queue.closed {
return Poll::Ready(None);
}
queue.waker = Some(cx.waker().clone());
Poll::Pending
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures::StreamExt;
fn bash(delta: &str) -> ClientEvent {
ClientEvent::Session(AgentSessionEvent::BashExecutionUpdate {
id: None,
delta: delta.into(),
})
}
#[tokio::test]
async fn delivers_in_order_and_coalesces_when_full() {
let hub = EventHub::with_capacity(2);
let mut stream = hub.subscribe();
hub.publish(AgentSessionEvent::AgentStart);
hub.publish(bash("a"));
hub.publish(bash("b"));
hub.publish(bash("c"));
hub.close();
let events: Vec<_> = (&mut stream).collect().await;
assert_eq!(
events,
vec![
ClientEvent::Session(AgentSessionEvent::AgentStart),
bash("abc")
]
);
}
#[test]
fn dropped_subscriptions_are_pruned() {
let hub = EventHub::new();
let stream = hub.subscribe();
assert_eq!(hub.subscriber_count(), 1);
drop(stream);
hub.publish(AgentSessionEvent::AgentStart);
assert_eq!(hub.subscriber_count(), 0);
}
}