use std::collections::VecDeque;
use std::pin::Pin;
use std::task::{Context, Poll};
use futures_util::Stream;
use crate::error::{SdkError, SdkResult};
use crate::models::UpdateEvent;
type ByteChunkStream = Pin<Box<dyn Stream<Item = Result<Vec<u8>, reqwest::Error>> + Send>>;
pub struct UpdateEventStream {
inner: ByteChunkStream,
line_buf: String,
pending: VecDeque<UpdateEvent>,
}
impl UpdateEventStream {
pub fn new<S>(stream: S) -> Self
where
S: Stream<Item = Result<Vec<u8>, reqwest::Error>> + Send + 'static,
{
Self {
inner: Box::pin(stream),
line_buf: String::new(),
pending: VecDeque::new(),
}
}
fn drain_complete_dispatches(&mut self) {
while let Some(idx) = self.line_buf.find("\n\n") {
let dispatch: String = self.line_buf.drain(..idx + 2).collect();
if let Some(ev) = parse_dispatch(&dispatch) {
self.pending.push_back(ev);
}
}
}
}
impl Stream for UpdateEventStream {
type Item = SdkResult<UpdateEvent>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
loop {
if let Some(ev) = this.pending.pop_front() {
return Poll::Ready(Some(Ok(ev)));
}
match this.inner.as_mut().poll_next(cx) {
Poll::Ready(Some(Ok(chunk))) => {
this.line_buf.push_str(&String::from_utf8_lossy(&chunk));
this.drain_complete_dispatches();
}
Poll::Ready(Some(Err(e))) => {
return Poll::Ready(Some(Err(SdkError::RequestError(e))));
}
Poll::Ready(None) => {
if !this.line_buf.is_empty() {
let rest: String = this.line_buf.drain(..).collect();
if let Some(ev) = parse_dispatch(&rest) {
this.pending.push_back(ev);
}
}
if let Some(ev) = this.pending.pop_front() {
return Poll::Ready(Some(Ok(ev)));
}
return Poll::Ready(None);
}
Poll::Pending => {
if let Some(ev) = this.pending.pop_front() {
return Poll::Ready(Some(Ok(ev)));
}
return Poll::Pending;
}
}
}
}
}
fn parse_dispatch(dispatch: &str) -> Option<UpdateEvent> {
let mut data_lines: Vec<String> = Vec::new();
let mut id: Option<u64> = None;
let mut _event_type: Option<String> = None;
for line in dispatch.lines() {
if line.is_empty() {
continue;
}
if let Some(_comment) = line.strip_prefix(':') {
continue;
}
if let Some(rest) = line.strip_prefix("data:") {
let rest = rest.strip_prefix(' ').unwrap_or(rest);
data_lines.push(rest.to_string());
} else if let Some(rest) = line.strip_prefix("id:") {
let rest = rest.strip_prefix(' ').unwrap_or(rest);
if let Ok(parsed) = rest.trim().parse::<u64>() {
id = Some(parsed);
}
} else if let Some(rest) = line.strip_prefix("event:") {
let rest = rest.strip_prefix(' ').unwrap_or(rest);
_event_type = Some(rest.to_string());
}
}
if data_lines.is_empty() {
return None;
}
let data = data_lines.join("\n");
match serde_json::from_str::<UpdateEvent>(&data) {
Ok(mut ev) => {
if let Some(id) = id {
ev.id = id;
}
Some(ev)
}
Err(_) => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_single_update_event() {
let frame = "id: 42\nevent: update\ndata: {\"software_id\":\"s1\",\"version\":\"1.2.0\",\"platform\":\"linux\",\"channel\":\"stable\",\"force_update\":false}\n\n";
let ev = parse_dispatch(frame).expect("should parse");
assert_eq!(ev.id, 42);
assert_eq!(ev.software_id, "s1");
assert_eq!(ev.version, "1.2.0");
assert_eq!(ev.platform, "linux");
assert_eq!(ev.channel, "stable");
assert!(!ev.force_update);
}
#[test]
fn parse_ignores_comments() {
assert!(parse_dispatch(": keepalive\n\n").is_none());
assert!(parse_dispatch(": lagged\n\n").is_none());
}
#[test]
fn parse_multi_line_data() {
let frame = "data: {\"software_id\":\"s2\",\ndata: \"version\":\"2.0\",\"platform\":\"win\",\"channel\":\"beta\",\"force_update\":true}\n\n";
let ev = parse_dispatch(frame).expect("should parse");
assert_eq!(ev.software_id, "s2");
assert_eq!(ev.version, "2.0");
assert!(ev.force_update);
}
#[test]
fn parse_skips_malformed() {
assert!(parse_dispatch("data: not-json\n\n").is_none());
}
#[test]
fn parse_id_overrides_json() {
let frame = "id: 99\ndata: {\"id\":5,\"software_id\":\"s\",\"version\":\"1\",\"platform\":\"p\",\"channel\":\"c\",\"force_update\":false}\n\n";
let ev = parse_dispatch(frame).expect("should parse");
assert_eq!(ev.id, 99);
}
}