use chromiumoxide::Page;
use serde::Deserialize;
use crate::error::{BrowserError, BrowserResult};
use crate::stealth::CAPTURE_GLOBAL;
#[derive(Debug, Clone, Deserialize)]
pub struct CaptureState {
pub status: u16,
pub body: String,
pub done: bool,
pub streaming: bool,
}
pub async fn read_last_capture(page: &Page) -> BrowserResult<Option<CaptureState>> {
let js = format!(
r"(function() {{
var s = window.{CAPTURE_GLOBAL};
if (!s || !s.last) return '';
var rec = s.byUrl[s.last];
if (!rec) return '';
return JSON.stringify({{
status: rec.status,
body: rec.chunks.join(''),
done: rec.done,
streaming: rec.streaming
}});
}})()"
);
let result = page.evaluate(js).await.map_err(|e| BrowserError::Browser {
reason: format!("Failed to read capture buffer: {e}"),
})?;
let raw = result
.value()
.and_then(|v| v.as_str().map(String::from))
.unwrap_or_default();
if raw.is_empty() {
return Ok(None);
}
let state: CaptureState = serde_json::from_str(&raw).map_err(|e| BrowserError::Browser {
reason: format!("Failed to parse capture state: {e}"),
})?;
Ok(Some(state))
}
#[must_use]
pub fn parse_sse_data(body: &str) -> Vec<String> {
let mut events = Vec::new();
let mut current: Vec<String> = Vec::new();
let flush = |current: &mut Vec<String>, events: &mut Vec<String>| {
if !current.is_empty() {
events.push(current.join("\n"));
current.clear();
}
};
for line in body.lines() {
if line.is_empty() {
flush(&mut current, &mut events);
continue;
}
if line.starts_with(':') {
continue;
}
if let Some(rest) = line.strip_prefix("data:") {
current.push(rest.strip_prefix(' ').unwrap_or(rest).to_owned());
}
}
flush(&mut current, &mut events);
events
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_basic_events() {
let body = "data: hello\n\ndata: world\n\n";
assert_eq!(parse_sse_data(body), vec!["hello", "world"]);
}
#[test]
fn joins_multiline_data() {
let body = "data: line1\ndata: line2\n\n";
assert_eq!(parse_sse_data(body), vec!["line1\nline2"]);
}
#[test]
fn ignores_comments_and_other_fields() {
let body = ": keep-alive\nevent: completion\ndata: {\"x\":1}\n\n";
assert_eq!(parse_sse_data(body), vec![r#"{"x":1}"#]);
}
#[test]
fn handles_trailing_event_without_blank_line() {
let body = "data: a\n\ndata: b";
assert_eq!(parse_sse_data(body), vec!["a", "b"]);
}
#[test]
fn preserves_done_sentinel() {
let body = "data: {\"type\":\"x\"}\n\ndata: [DONE]\n\n";
let events = parse_sse_data(body);
assert_eq!(events.last().map(String::as_str), Some("[DONE]"));
}
#[test]
fn empty_body_yields_no_events() {
assert!(parse_sse_data("").is_empty());
}
#[test]
fn capture_state_deserializes() {
let raw = r#"{"status":200,"body":"data: hi\n\n","done":true,"streaming":true}"#;
let s: CaptureState = serde_json::from_str(raw).unwrap();
assert_eq!(s.status, 200);
assert!(s.done);
assert!(s.streaming);
assert_eq!(parse_sse_data(&s.body), vec!["hi"]);
}
}