1use chromiumoxide::Page;
8use serde::Deserialize;
9
10use crate::error::{BrowserError, BrowserResult};
11use crate::stealth::CAPTURE_GLOBAL;
12
13#[derive(Debug, Clone, Deserialize)]
15pub struct CaptureState {
16 pub status: u16,
18 pub body: String,
20 pub done: bool,
22 pub streaming: bool,
24}
25
26pub async fn read_last_capture(page: &Page) -> BrowserResult<Option<CaptureState>> {
30 let js = format!(
31 r"(function() {{
32 var s = window.{CAPTURE_GLOBAL};
33 if (!s || !s.last) return '';
34 var rec = s.byUrl[s.last];
35 if (!rec) return '';
36 return JSON.stringify({{
37 status: rec.status,
38 body: rec.chunks.join(''),
39 done: rec.done,
40 streaming: rec.streaming
41 }});
42 }})()"
43 );
44
45 let result = page.evaluate(js).await.map_err(|e| BrowserError::Browser {
46 reason: format!("Failed to read capture buffer: {e}"),
47 })?;
48
49 let raw = result
50 .value()
51 .and_then(|v| v.as_str().map(String::from))
52 .unwrap_or_default();
53
54 if raw.is_empty() {
55 return Ok(None);
56 }
57
58 let state: CaptureState = serde_json::from_str(&raw).map_err(|e| BrowserError::Browser {
59 reason: format!("Failed to parse capture state: {e}"),
60 })?;
61 Ok(Some(state))
62}
63
64#[must_use]
72pub fn parse_sse_data(body: &str) -> Vec<String> {
73 let mut events = Vec::new();
74 let mut current: Vec<String> = Vec::new();
75
76 let flush = |current: &mut Vec<String>, events: &mut Vec<String>| {
77 if !current.is_empty() {
78 events.push(current.join("\n"));
79 current.clear();
80 }
81 };
82
83 for line in body.lines() {
84 if line.is_empty() {
85 flush(&mut current, &mut events);
86 continue;
87 }
88 if line.starts_with(':') {
89 continue;
91 }
92 if let Some(rest) = line.strip_prefix("data:") {
93 current.push(rest.strip_prefix(' ').unwrap_or(rest).to_owned());
95 }
96 }
98 flush(&mut current, &mut events);
100
101 events
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn parses_basic_events() {
110 let body = "data: hello\n\ndata: world\n\n";
111 assert_eq!(parse_sse_data(body), vec!["hello", "world"]);
112 }
113
114 #[test]
115 fn joins_multiline_data() {
116 let body = "data: line1\ndata: line2\n\n";
117 assert_eq!(parse_sse_data(body), vec!["line1\nline2"]);
118 }
119
120 #[test]
121 fn ignores_comments_and_other_fields() {
122 let body = ": keep-alive\nevent: completion\ndata: {\"x\":1}\n\n";
123 assert_eq!(parse_sse_data(body), vec![r#"{"x":1}"#]);
124 }
125
126 #[test]
127 fn handles_trailing_event_without_blank_line() {
128 let body = "data: a\n\ndata: b";
129 assert_eq!(parse_sse_data(body), vec!["a", "b"]);
130 }
131
132 #[test]
133 fn preserves_done_sentinel() {
134 let body = "data: {\"type\":\"x\"}\n\ndata: [DONE]\n\n";
135 let events = parse_sse_data(body);
136 assert_eq!(events.last().map(String::as_str), Some("[DONE]"));
137 }
138
139 #[test]
140 fn empty_body_yields_no_events() {
141 assert!(parse_sse_data("").is_empty());
142 }
143
144 #[test]
145 fn capture_state_deserializes() {
146 let raw = r#"{"status":200,"body":"data: hi\n\n","done":true,"streaming":true}"#;
147 let s: CaptureState = serde_json::from_str(raw).unwrap();
148 assert_eq!(s.status, 200);
149 assert!(s.done);
150 assert!(s.streaming);
151 assert_eq!(parse_sse_data(&s.body), vec!["hi"]);
152 }
153}