use std::time::{Duration, Instant};
use zeph_core::channel::ChannelError;
pub struct StreamingBuffer {
accumulated: String,
last_edit: Option<Instant>,
throttle: Duration,
}
impl StreamingBuffer {
#[must_use]
pub fn new(throttle: Duration) -> Self {
Self {
accumulated: String::new(),
last_edit: None,
throttle,
}
}
pub fn push(&mut self, chunk: &str) {
self.accumulated.push_str(chunk);
}
#[must_use]
pub fn should_flush(&self) -> bool {
self.last_edit
.is_none_or(|last| last.elapsed() > self.throttle)
}
pub fn take(&mut self) -> String {
self.last_edit = Some(Instant::now());
std::mem::take(&mut self.accumulated)
}
pub fn reset(&mut self) {
self.accumulated.clear();
self.last_edit = None;
}
pub fn mark_flushed(&mut self) {
self.last_edit = Some(Instant::now());
}
#[must_use]
pub fn text(&self) -> &str {
&self.accumulated
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.accumulated.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.accumulated.len()
}
}
#[allow(async_fn_in_trait)]
pub trait StreamingSend {
async fn send_or_edit(&mut self) -> Result<(), ChannelError>;
fn streaming_buffer(&self) -> &StreamingBuffer;
fn streaming_buffer_mut(&mut self) -> &mut StreamingBuffer;
fn has_pending_message(&self) -> bool;
fn clear_pending_message(&mut self);
async fn streaming_send_chunk(&mut self, chunk: &str) -> Result<(), ChannelError> {
self.streaming_buffer_mut().push(chunk);
if self.streaming_buffer().should_flush() {
self.send_or_edit().await?;
}
Ok(())
}
async fn streaming_flush_chunks(&mut self) -> Result<(), ChannelError> {
if self.has_pending_message() || !self.streaming_buffer().is_empty() {
self.send_or_edit().await?;
}
self.streaming_buffer_mut().reset();
self.clear_pending_message();
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
#[test]
fn new_is_empty_and_ready_to_flush() {
let buf = StreamingBuffer::new(Duration::from_secs(2));
assert!(buf.is_empty());
assert_eq!(buf.len(), 0);
assert!(buf.should_flush());
assert_eq!(buf.text(), "");
}
#[test]
fn push_accumulates() {
let mut buf = StreamingBuffer::new(Duration::from_secs(2));
buf.push("hello ");
buf.push("world");
assert_eq!(buf.text(), "hello world");
assert_eq!(buf.len(), 11);
assert!(!buf.is_empty());
}
#[test]
fn take_drains_and_returns_text() {
let mut buf = StreamingBuffer::new(Duration::from_secs(2));
buf.push("data");
let text = buf.take();
assert_eq!(text, "data");
assert!(buf.is_empty());
assert_eq!(buf.len(), 0);
}
#[test]
fn take_activates_throttle() {
let mut buf = StreamingBuffer::new(Duration::from_mins(1));
buf.push("x");
buf.take();
assert!(!buf.should_flush());
}
#[test]
fn reset_clears_all_state() {
let mut buf = StreamingBuffer::new(Duration::from_mins(1));
buf.push("something");
buf.take(); buf.push("more");
buf.reset();
assert!(buf.is_empty());
assert!(buf.should_flush()); }
#[test]
fn mark_flushed_records_timestamp_without_draining() {
let mut buf = StreamingBuffer::new(Duration::from_mins(1));
buf.push("data");
buf.mark_flushed();
assert_eq!(buf.text(), "data");
assert!(!buf.should_flush());
}
#[test]
fn should_flush_false_within_throttle() {
let mut buf = StreamingBuffer::new(Duration::from_mins(1));
buf.push("x");
buf.mark_flushed();
assert!(!buf.should_flush());
}
#[test]
fn should_flush_true_after_throttle_elapsed() {
let mut buf = StreamingBuffer::new(Duration::from_millis(1));
buf.push("x");
buf.mark_flushed();
std::thread::sleep(Duration::from_millis(5));
assert!(buf.should_flush());
}
#[test]
fn take_empty_buffer_returns_empty_string() {
let mut buf = StreamingBuffer::new(Duration::from_secs(2));
assert_eq!(buf.take(), "");
}
}