use std::pin::Pin;
use std::task::{Context, Poll};
use futures::Stream;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
#[derive(Debug, Clone)]
pub struct StreamingConfig {
pub buffer_size: usize,
pub include_timing: bool,
pub heartbeat_ms: u64,
}
impl Default for StreamingConfig {
fn default() -> Self {
Self {
buffer_size: 32,
include_timing: true,
heartbeat_ms: 15000, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamEvent {
pub event: String,
pub data: String,
pub id: Option<String>,
pub timestamp_ms: Option<u64>,
}
impl StreamEvent {
pub fn new(event: impl Into<String>, data: impl Into<String>) -> Self {
Self {
event: event.into(),
data: data.into(),
id: None,
timestamp_ms: None,
}
}
pub fn token(token: impl Into<String>) -> Self {
Self::new("token", token)
}
pub fn done() -> Self {
Self::new("done", "[DONE]")
}
pub fn error(msg: impl Into<String>) -> Self {
Self::new("error", msg)
}
pub fn heartbeat() -> Self {
Self::new("heartbeat", "")
}
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn with_timestamp(mut self, timestamp_ms: u64) -> Self {
self.timestamp_ms = Some(timestamp_ms);
self
}
pub fn to_sse(&self) -> String {
let mut result = String::new();
if let Some(id) = &self.id {
result.push_str(&format!("id: {}\n", id));
}
result.push_str(&format!("event: {}\n", self.event));
for line in self.data.lines() {
result.push_str(&format!("data: {}\n", line));
}
if self.data.is_empty() {
result.push_str("data: \n");
}
result.push('\n');
result
}
}
pub struct StreamingResponse {
rx: mpsc::Receiver<StreamEvent>,
config: StreamingConfig,
}
impl StreamingResponse {
pub fn new(config: StreamingConfig) -> (Self, StreamSender) {
let (tx, rx) = mpsc::channel(config.buffer_size);
let response = Self { rx, config };
let sender = StreamSender { tx };
(response, sender)
}
pub fn default_config() -> (Self, StreamSender) {
Self::new(StreamingConfig::default())
}
}
impl Stream for StreamingResponse {
type Item = StreamEvent;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Pin::new(&mut self.rx).poll_recv(cx)
}
}
#[derive(Clone)]
pub struct StreamSender {
tx: mpsc::Sender<StreamEvent>,
}
impl StreamSender {
pub async fn send(&self, event: StreamEvent) -> Result<(), StreamError> {
self.tx.send(event).await.map_err(|_| StreamError::Closed)
}
pub async fn send_token(&self, token: impl Into<String>) -> Result<(), StreamError> {
self.send(StreamEvent::token(token)).await
}
pub async fn send_done(&self) -> Result<(), StreamError> {
self.send(StreamEvent::done()).await
}
pub async fn send_error(&self, msg: impl Into<String>) -> Result<(), StreamError> {
self.send(StreamEvent::error(msg)).await
}
pub fn is_closed(&self) -> bool {
self.tx.is_closed()
}
}
#[derive(Debug, thiserror::Error)]
pub enum StreamError {
#[error("Stream closed")]
Closed,
#[error("Send failed: {0}")]
SendFailed(String),
}
pub struct TokenStream {
tokens: Vec<String>,
index: usize,
delay_ms: u64,
}
impl TokenStream {
pub fn new(tokens: Vec<String>) -> Self {
Self {
tokens,
index: 0,
delay_ms: 0,
}
}
pub fn with_delay(mut self, delay_ms: u64) -> Self {
self.delay_ms = delay_ms;
self
}
pub async fn stream_to(&mut self, sender: &StreamSender) -> Result<(), StreamError> {
while let Some(token) = self.next_token() {
sender.send_token(token).await?;
if self.delay_ms > 0 {
tokio::time::sleep(std::time::Duration::from_millis(self.delay_ms)).await;
}
}
sender.send_done().await
}
pub fn next_token(&mut self) -> Option<String> {
if self.index < self.tokens.len() {
let token = self.tokens[self.index].clone();
self.index += 1;
Some(token)
} else {
None
}
}
pub fn reset(&mut self) {
self.index = 0;
}
pub fn remaining(&self) -> usize {
self.tokens.len().saturating_sub(self.index)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stream_event_sse() {
let event = StreamEvent::token("Hello")
.with_id("1")
.with_timestamp(12345);
let sse = event.to_sse();
assert!(sse.contains("id: 1"));
assert!(sse.contains("event: token"));
assert!(sse.contains("data: Hello"));
}
#[test]
fn test_stream_event_multiline() {
let event = StreamEvent::new("message", "line1\nline2\nline3");
let sse = event.to_sse();
assert!(sse.contains("data: line1"));
assert!(sse.contains("data: line2"));
assert!(sse.contains("data: line3"));
}
#[tokio::test]
async fn test_streaming_response() {
let (mut response, sender) = StreamingResponse::default_config();
tokio::spawn(async move {
sender.send_token("Hello").await.unwrap();
sender.send_token(" World").await.unwrap();
sender.send_done().await.unwrap();
});
use futures::StreamExt;
let events: Vec<_> = response.collect().await;
assert_eq!(events.len(), 3);
assert_eq!(events[0].event, "token");
assert_eq!(events[2].event, "done");
}
#[tokio::test]
async fn test_token_stream() {
let tokens = vec!["Hello".to_string(), " ".to_string(), "World".to_string()];
let mut stream = TokenStream::new(tokens);
assert_eq!(stream.remaining(), 3);
assert_eq!(stream.next_token(), Some("Hello".to_string()));
assert_eq!(stream.remaining(), 2);
stream.reset();
assert_eq!(stream.remaining(), 3);
}
}