use crate::ai::types::Event;
use std::time::{Duration, Instant};
pub struct TextBatcher {
interval: Duration,
pending: String,
last_flush: Instant,
}
impl TextBatcher {
pub fn new(interval: Duration) -> Self {
Self { interval, pending: String::new(), last_flush: Instant::now() }
}
pub fn push(&mut self, event: Event, emit: &mut dyn FnMut(Event)) {
self.push_at(event, Instant::now(), emit);
}
fn push_at(&mut self, event: Event, now: Instant, emit: &mut dyn FnMut(Event)) {
match event {
Event::Text(text) => {
self.pending.push_str(&text);
if now.duration_since(self.last_flush) >= self.interval {
self.flush_at(now, emit);
}
}
other => {
self.flush_at(now, emit);
emit(other);
}
}
}
pub fn finish(&mut self, emit: &mut dyn FnMut(Event)) {
self.flush_at(Instant::now(), emit);
}
fn flush_at(&mut self, now: Instant, emit: &mut dyn FnMut(Event)) {
self.last_flush = now;
if !self.pending.is_empty() {
emit(Event::Text(std::mem::take(&mut self.pending)));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ai::types::StopReason;
fn run(events: Vec<(u64, Event)>, interval_ms: u64) -> Vec<Event> {
let start = Instant::now();
let mut batcher =
TextBatcher { interval: Duration::from_millis(interval_ms), pending: String::new(), last_flush: start };
let mut out = Vec::new();
for (at, event) in events {
batcher.push_at(event, start + Duration::from_millis(at), &mut |event| out.push(event));
}
batcher.finish(&mut |event| out.push(event));
out
}
#[test]
fn junta_texto_dentro_del_intervalo() {
let events = (0..10).map(|index| (index, Event::Text(format!("{index}")))).collect();
assert_eq!(run(events, 33), [Event::Text("0123456789".into())]);
}
#[test]
fn entrega_cuando_pasa_el_intervalo() {
let out =
run(vec![(0, Event::Text("a".into())), (40, Event::Text("b".into())), (45, Event::Text("c".into()))], 33);
assert_eq!(out, [Event::Text("ab".into()), Event::Text("c".into())]);
}
#[test]
fn otros_eventos_vacian_antes_para_conservar_el_orden() {
let tool = Event::ToolUse { id: "t".into(), name: "x".into(), input: serde_json::json!({}) };
let out = run(vec![(0, Event::Text("a".into())), (1, tool.clone()), (2, Event::Done(StopReason::ToolUse))], 33);
assert_eq!(out, [Event::Text("a".into()), tool, Event::Done(StopReason::ToolUse)]);
}
}