vtcode_desktop_protocol/
live_events.rs1use std::collections::VecDeque;
3
4use vtcode_exec_events::VersionedThreadEvent;
5
6pub fn live_event_channel(capacity: usize) -> (LiveEventSender, LiveEventReceiver) {
16 assert!(capacity > 0, "live event capacity must be positive");
17 let inner = std::sync::Arc::new(LiveEventQueueInner {
18 capacity,
19 queue: std::sync::Mutex::new(VecDeque::new()),
20 senders: std::sync::atomic::AtomicUsize::new(1),
21 receiver_alive: std::sync::atomic::AtomicBool::new(true),
22 dropped_ordered: std::sync::atomic::AtomicU64::new(0),
23 dropped_stale: std::sync::atomic::AtomicU64::new(0),
24 notify: tokio::sync::Notify::new(),
25 });
26 (LiveEventSender { inner: inner.clone() }, LiveEventReceiver { inner })
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31enum CoalesceOp {
32 Replace,
34 Merge,
36}
37
38struct LiveEventSlot {
41 key: Option<String>,
42 op: CoalesceOp,
43 event: VersionedThreadEvent,
44}
45
46struct LiveEventQueueInner {
47 capacity: usize,
48 queue: std::sync::Mutex<VecDeque<LiveEventSlot>>,
49 senders: std::sync::atomic::AtomicUsize,
50 receiver_alive: std::sync::atomic::AtomicBool,
51 dropped_ordered: std::sync::atomic::AtomicU64,
52 dropped_stale: std::sync::atomic::AtomicU64,
53 notify: tokio::sync::Notify,
54}
55
56impl LiveEventQueueInner {
57 fn classify(event: &VersionedThreadEvent) -> Option<(String, CoalesceOp)> {
58 match event.event() {
59 vtcode_exec_events::ThreadEvent::ItemUpdated(update) => Some((update.item.id.clone(), CoalesceOp::Replace)),
60 vtcode_exec_events::ThreadEvent::PlanDelta(delta) => Some((delta.item_id.clone(), CoalesceOp::Merge)),
61 _ => None,
62 }
63 }
64
65 fn publish(&self, event: VersionedThreadEvent) -> Result<(), ()> {
68 if !self.receiver_alive.load(std::sync::atomic::Ordering::Acquire) {
69 return Err(());
70 }
71 let mut queue = self.queue.lock().map_err(|_poisoned| ())?;
72 let classified = Self::classify(&event);
73 let slot_key = classified.as_ref().map(|(key, _)| key.clone());
74 let slot_op = classified.as_ref().map_or(CoalesceOp::Replace, |(_, op)| *op);
75 if let Some((key, op)) = classified {
76 let pending_slot = queue
77 .iter()
78 .position(|slot| slot.key.as_deref() == Some(key.as_str()) && slot.op == op)
79 .and_then(|index| queue.remove(index).map(|removed| (index, removed)));
80 if let Some((index, pending)) = pending_slot {
81 let merged = match op {
84 CoalesceOp::Replace => event,
85 CoalesceOp::Merge => Self::merge_plan_deltas(pending.event, event),
86 };
87 queue.insert(index, LiveEventSlot { key: Some(key), op, event: merged });
88 drop(queue);
89 self.notify.notify_waiters();
90 return Ok(());
91 }
92 }
93 if queue.len() >= self.capacity {
94 let victim = queue.iter().position(|slot| slot.key.is_some());
98 match victim {
99 Some(index) => {
100 drop(queue.remove(index));
101 let _ = self.dropped_stale.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
102 }
103 None => {
104 drop(queue.pop_front());
105 let _ = self.dropped_ordered.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
106 }
107 }
108 }
109 queue.push_back(LiveEventSlot { key: slot_key, op: slot_op, event });
110 drop(queue);
111 self.notify.notify_waiters();
112 Ok(())
113 }
114
115 fn merge_plan_deltas(pending: VersionedThreadEvent, next: VersionedThreadEvent) -> VersionedThreadEvent {
118 let (pending, next) = (pending.into_event(), next.into_event());
119 match (pending, next) {
120 (vtcode_exec_events::ThreadEvent::PlanDelta(older), vtcode_exec_events::ThreadEvent::PlanDelta(newer)) => {
121 let mut merged = newer.clone();
122 let mut text = older.delta;
123 text.push_str(&newer.delta);
124 merged.delta = text;
125 if merged.recovery.is_none() {
126 merged.recovery = older.recovery;
127 }
128 VersionedThreadEvent::new(vtcode_exec_events::ThreadEvent::PlanDelta(merged))
129 }
130 (_, fallback) => VersionedThreadEvent::new(fallback),
131 }
132 }
133}
134
135pub struct LiveEventSender {
137 inner: std::sync::Arc<LiveEventQueueInner>,
138}
139
140impl Clone for LiveEventSender {
141 fn clone(&self) -> Self {
142 let _ = self.inner.senders.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
143 Self { inner: std::sync::Arc::clone(&self.inner) }
144 }
145}
146
147impl LiveEventSender {
148 pub fn publish(&self, event: VersionedThreadEvent) -> Result<(), LiveEventSendError> {
151 self.inner.publish(event).map_err(|()| LiveEventSendError(()))
152 }
153}
154
155impl Drop for LiveEventSender {
156 fn drop(&mut self) {
157 let _ = self.inner.senders.fetch_sub(1, std::sync::atomic::Ordering::Release);
158 self.inner.notify.notify_waiters();
159 }
160}
161
162pub struct LiveEventReceiver {
164 inner: std::sync::Arc<LiveEventQueueInner>,
165}
166
167impl LiveEventReceiver {
168 pub async fn recv(&self) -> Option<VersionedThreadEvent> {
172 loop {
173 {
174 let mut queue = self.inner.queue.lock().ok()?;
175 if let Some(slot) = queue.pop_front() {
176 return Some(slot.event);
177 }
178 if self.inner.senders.load(std::sync::atomic::Ordering::Acquire) == 0 {
179 return None;
180 }
181 }
182 let notified = self.inner.notify.notified();
183 {
184 let queue = self.inner.queue.lock().ok()?;
185 if !queue.is_empty() {
186 continue;
187 }
188 if self.inner.senders.load(std::sync::atomic::Ordering::Acquire) == 0 {
189 return None;
190 }
191 }
192 notified.await;
193 }
194 }
195
196 pub fn take_dropped_ordered(&self) -> u64 {
198 self.inner.dropped_ordered.swap(0, std::sync::atomic::Ordering::Relaxed)
199 }
200
201 pub fn take_dropped_stale(&self) -> u64 {
204 self.inner.dropped_stale.swap(0, std::sync::atomic::Ordering::Relaxed)
205 }
206}
207
208impl Drop for LiveEventReceiver {
209 fn drop(&mut self) {
210 self.inner.receiver_alive.store(false, std::sync::atomic::Ordering::Release);
211 self.inner.notify.notify_waiters();
212 }
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub struct LiveEventSendError(());
218
219impl std::fmt::Display for LiveEventSendError {
220 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221 f.write_str("live event consumer disconnected")
222 }
223}
224
225impl std::error::Error for LiveEventSendError {}