use std::collections::VecDeque;
use vtcode_exec_events::VersionedThreadEvent;
pub fn live_event_channel(capacity: usize) -> (LiveEventSender, LiveEventReceiver) {
assert!(capacity > 0, "live event capacity must be positive");
let inner = std::sync::Arc::new(LiveEventQueueInner {
capacity,
queue: std::sync::Mutex::new(VecDeque::new()),
senders: std::sync::atomic::AtomicUsize::new(1),
receiver_alive: std::sync::atomic::AtomicBool::new(true),
dropped_ordered: std::sync::atomic::AtomicU64::new(0),
dropped_stale: std::sync::atomic::AtomicU64::new(0),
notify: tokio::sync::Notify::new(),
});
(LiveEventSender { inner: inner.clone() }, LiveEventReceiver { inner })
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CoalesceOp {
Replace,
Merge,
}
struct LiveEventSlot {
key: Option<String>,
op: CoalesceOp,
event: VersionedThreadEvent,
}
struct LiveEventQueueInner {
capacity: usize,
queue: std::sync::Mutex<VecDeque<LiveEventSlot>>,
senders: std::sync::atomic::AtomicUsize,
receiver_alive: std::sync::atomic::AtomicBool,
dropped_ordered: std::sync::atomic::AtomicU64,
dropped_stale: std::sync::atomic::AtomicU64,
notify: tokio::sync::Notify,
}
impl LiveEventQueueInner {
fn classify(event: &VersionedThreadEvent) -> Option<(String, CoalesceOp)> {
match event.event() {
vtcode_exec_events::ThreadEvent::ItemUpdated(update) => Some((update.item.id.clone(), CoalesceOp::Replace)),
vtcode_exec_events::ThreadEvent::PlanDelta(delta) => Some((delta.item_id.clone(), CoalesceOp::Merge)),
_ => None,
}
}
fn publish(&self, event: VersionedThreadEvent) -> Result<(), ()> {
if !self.receiver_alive.load(std::sync::atomic::Ordering::Acquire) {
return Err(());
}
let mut queue = self.queue.lock().map_err(|_poisoned| ())?;
let classified = Self::classify(&event);
let slot_key = classified.as_ref().map(|(key, _)| key.clone());
let slot_op = classified.as_ref().map_or(CoalesceOp::Replace, |(_, op)| *op);
if let Some((key, op)) = classified {
let pending_slot = queue
.iter()
.position(|slot| slot.key.as_deref() == Some(key.as_str()) && slot.op == op)
.and_then(|index| queue.remove(index).map(|removed| (index, removed)));
if let Some((index, pending)) = pending_slot {
let merged = match op {
CoalesceOp::Replace => event,
CoalesceOp::Merge => Self::merge_plan_deltas(pending.event, event),
};
queue.insert(index, LiveEventSlot { key: Some(key), op, event: merged });
drop(queue);
self.notify.notify_waiters();
return Ok(());
}
}
if queue.len() >= self.capacity {
let victim = queue.iter().position(|slot| slot.key.is_some());
match victim {
Some(index) => {
drop(queue.remove(index));
let _ = self.dropped_stale.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
None => {
drop(queue.pop_front());
let _ = self.dropped_ordered.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
}
queue.push_back(LiveEventSlot { key: slot_key, op: slot_op, event });
drop(queue);
self.notify.notify_waiters();
Ok(())
}
fn merge_plan_deltas(pending: VersionedThreadEvent, next: VersionedThreadEvent) -> VersionedThreadEvent {
let (pending, next) = (pending.into_event(), next.into_event());
match (pending, next) {
(vtcode_exec_events::ThreadEvent::PlanDelta(older), vtcode_exec_events::ThreadEvent::PlanDelta(newer)) => {
let mut merged = newer.clone();
let mut text = older.delta;
text.push_str(&newer.delta);
merged.delta = text;
if merged.recovery.is_none() {
merged.recovery = older.recovery;
}
VersionedThreadEvent::new(vtcode_exec_events::ThreadEvent::PlanDelta(merged))
}
(_, fallback) => VersionedThreadEvent::new(fallback),
}
}
}
pub struct LiveEventSender {
inner: std::sync::Arc<LiveEventQueueInner>,
}
impl Clone for LiveEventSender {
fn clone(&self) -> Self {
let _ = self.inner.senders.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Self { inner: std::sync::Arc::clone(&self.inner) }
}
}
impl LiveEventSender {
pub fn publish(&self, event: VersionedThreadEvent) -> Result<(), LiveEventSendError> {
self.inner.publish(event).map_err(|()| LiveEventSendError(()))
}
}
impl Drop for LiveEventSender {
fn drop(&mut self) {
let _ = self.inner.senders.fetch_sub(1, std::sync::atomic::Ordering::Release);
self.inner.notify.notify_waiters();
}
}
pub struct LiveEventReceiver {
inner: std::sync::Arc<LiveEventQueueInner>,
}
impl LiveEventReceiver {
pub async fn recv(&self) -> Option<VersionedThreadEvent> {
loop {
{
let mut queue = self.inner.queue.lock().ok()?;
if let Some(slot) = queue.pop_front() {
return Some(slot.event);
}
if self.inner.senders.load(std::sync::atomic::Ordering::Acquire) == 0 {
return None;
}
}
let notified = self.inner.notify.notified();
{
let queue = self.inner.queue.lock().ok()?;
if !queue.is_empty() {
continue;
}
if self.inner.senders.load(std::sync::atomic::Ordering::Acquire) == 0 {
return None;
}
}
notified.await;
}
}
pub fn take_dropped_ordered(&self) -> u64 {
self.inner.dropped_ordered.swap(0, std::sync::atomic::Ordering::Relaxed)
}
pub fn take_dropped_stale(&self) -> u64 {
self.inner.dropped_stale.swap(0, std::sync::atomic::Ordering::Relaxed)
}
}
impl Drop for LiveEventReceiver {
fn drop(&mut self) {
self.inner.receiver_alive.store(false, std::sync::atomic::Ordering::Release);
self.inner.notify.notify_waiters();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LiveEventSendError(());
impl std::fmt::Display for LiveEventSendError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("live event consumer disconnected")
}
}
impl std::error::Error for LiveEventSendError {}