use crate::error::ChippClientError;
use bytes::Bytes;
use futures::Stream;
use serde::Deserialize;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use tokio::sync::Mutex;
#[derive(Debug, Clone)]
pub enum StreamEvent {
TextDelta(String),
SessionId(String),
Done,
}
#[derive(Debug, Deserialize)]
struct SseEvent {
#[serde(rename = "type")]
event_type: String,
#[serde(default)]
delta: Option<String>,
#[serde(rename = "messageMetadata")]
message_metadata: Option<MessageMetadata>,
}
#[derive(Debug, Deserialize)]
struct MessageMetadata {
annotations: Vec<Annotation>,
}
#[derive(Debug, Deserialize)]
struct Annotation {
#[serde(rename = "persistedMessageId")]
persisted_message_id: Option<String>,
}
pub fn parse_sse_line(line: &str) -> Option<StreamEvent> {
let data = line.strip_prefix("data: ")?;
if data == "[DONE]" {
return Some(StreamEvent::Done);
}
let event: SseEvent = serde_json::from_str(data).ok()?;
match event.event_type.as_str() {
"text-delta" => event.delta.map(StreamEvent::TextDelta),
"message-metadata" => {
event.message_metadata.and_then(|meta| {
meta.annotations
.into_iter()
.find_map(|ann| ann.persisted_message_id.map(StreamEvent::SessionId))
})
}
_ => None,
}
}
pub struct ChippStream {
inner: Pin<Box<dyn Stream<Item = Result<Bytes, reqwest::Error>> + Send>>,
buffer: String,
session_id: Arc<Mutex<Option<String>>>,
finished: bool,
}
impl std::fmt::Debug for ChippStream {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ChippStream")
.field("finished", &self.finished)
.finish_non_exhaustive()
}
}
impl ChippStream {
pub(crate) fn new(
inner: Pin<Box<dyn Stream<Item = Result<Bytes, reqwest::Error>> + Send>>,
session_id: Arc<Mutex<Option<String>>>,
) -> Self {
Self {
inner,
buffer: String::new(),
session_id,
finished: false,
}
}
pub async fn session_id(&self) -> Option<String> {
self.session_id.lock().await.clone()
}
fn process_buffer(&mut self) -> Option<Result<String, ChippClientError>> {
while let Some(newline_pos) = self.buffer.find('\n') {
let line = self.buffer[..newline_pos].trim().to_string();
self.buffer = self.buffer[newline_pos + 1..].to_string();
if line.is_empty() {
continue;
}
if let Some(event) = parse_sse_line(&line) {
match event {
StreamEvent::TextDelta(text) => {
return Some(Ok(text));
}
StreamEvent::SessionId(id) => {
if let Ok(mut guard) = self.session_id.try_lock() {
*guard = Some(id);
}
}
StreamEvent::Done => {
self.finished = true;
return None;
}
}
}
}
None
}
}
impl Stream for ChippStream {
type Item = Result<String, ChippClientError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if self.finished {
return Poll::Ready(None);
}
if let Some(result) = self.process_buffer() {
return Poll::Ready(Some(result));
}
loop {
match self.inner.as_mut().poll_next(cx) {
Poll::Ready(Some(Ok(bytes))) => {
match String::from_utf8(bytes.to_vec()) {
Ok(text) => {
self.buffer.push_str(&text);
if let Some(result) = self.process_buffer() {
return Poll::Ready(Some(result));
}
}
Err(e) => {
return Poll::Ready(Some(Err(ChippClientError::StreamError(format!(
"Invalid UTF-8 in stream: {}",
e
)))));
}
}
}
Poll::Ready(Some(Err(e))) => {
return Poll::Ready(Some(Err(ChippClientError::HttpError(e))));
}
Poll::Ready(None) => {
if !self.buffer.is_empty() {
if let Some(result) = self.process_buffer() {
return Poll::Ready(Some(result));
}
}
self.finished = true;
return Poll::Ready(None);
}
Poll::Pending => {
return Poll::Pending;
}
}
}
}
}