chunk_flush/lib.rs
1//! # chunk-flush
2//!
3//! Flush-on-newline buffer for streaming LLM output. Holds chars until
4//! either a newline arrives or `max_pending_chars` is exceeded, then
5//! emits the buffered chunk.
6//!
7//! The point: keep the UI/log from churning on every single token
8//! delta, without making it laggy on long lines.
9//!
10//! ## Example
11//!
12//! ```
13//! use chunk_flush::Flusher;
14//! let mut f = Flusher::new(500);
15//! assert_eq!(f.push("hello"), None); // no \n yet, under cap
16//! assert_eq!(f.push(" world\n").as_deref(), Some("hello world\n"));
17//! ```
18
19#![deny(missing_docs)]
20
21/// Streaming buffer with flush-on-newline + size cap.
22#[derive(Debug, Clone)]
23pub struct Flusher {
24 buf: String,
25 max_pending_chars: usize,
26}
27
28impl Flusher {
29 /// Build a flusher with a max-pending cap. Beyond this many chars
30 /// the buffer auto-flushes even without a newline.
31 pub fn new(max_pending_chars: usize) -> Self {
32 Self {
33 buf: String::new(),
34 max_pending_chars,
35 }
36 }
37
38 /// Push next chunk. Returns the new emitted chunk when one is
39 /// ready, else `None`.
40 pub fn push(&mut self, chunk: &str) -> Option<String> {
41 self.buf.push_str(chunk);
42 // If the buffer contains a newline, flush everything up to
43 // (and including) the last newline.
44 if let Some(last_nl) = self.buf.rfind('\n') {
45 let split = last_nl + 1;
46 let out: String = self.buf.drain(..split).collect();
47 return Some(out);
48 }
49 // No newline yet — check the size cap.
50 if self.buf.chars().count() > self.max_pending_chars {
51 return Some(self.flush());
52 }
53 None
54 }
55
56 /// Force-flush whatever is buffered.
57 pub fn flush(&mut self) -> String {
58 std::mem::take(&mut self.buf)
59 }
60
61 /// True when nothing is buffered.
62 pub fn is_empty(&self) -> bool {
63 self.buf.is_empty()
64 }
65}