sim_lib_net_core/sse.rs
1//! Server-Sent Events record decoding.
2
3use crate::line::LineDecoder;
4
5/// A decoded Server-Sent Events record.
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub struct SseEvent {
8 /// The `event:` field value, if any was present in the record.
9 pub event: Option<String>,
10 /// The `data:` payload. Multiple `data:` lines are joined with `\n`.
11 pub data: String,
12}
13
14/// Incremental Server-Sent Events decoder.
15///
16/// Accumulates `event:` and `data:` field lines and emits an [`SseEvent`] when a
17/// blank line terminates the record (standard SSE dispatch). Multiple `data:`
18/// lines within one record are joined with `\n`. Comment lines (those starting
19/// with `:`) and unknown fields are ignored. Field values have a single leading
20/// space after the colon stripped, per the SSE specification.
21///
22/// `push` does the newline framing internally via [`LineDecoder`]; callers that
23/// already have whole lines can use [`push_line`](SseDecoder::push_line).
24#[derive(Debug, Default)]
25pub struct SseDecoder {
26 lines: LineDecoder,
27 event: Option<String>,
28 data: Vec<String>,
29 have_record: bool,
30}
31
32impl SseDecoder {
33 /// Create an empty decoder.
34 pub fn new() -> Self {
35 Self::default()
36 }
37
38 /// Feed raw bytes; return any records completed by blank lines.
39 pub fn push(&mut self, bytes: &[u8]) -> Vec<SseEvent> {
40 let mut events = Vec::new();
41 for line in self.lines.push(bytes) {
42 // SSE payloads are UTF-8; lossy decoding keeps the framer robust and
43 // pushes any invalid-byte handling onto the caller's data parse.
44 let text = String::from_utf8_lossy(&line);
45 if let Some(event) = self.push_line(&text) {
46 events.push(event);
47 }
48 }
49 events
50 }
51
52 /// Feed a single already-framed line (without its terminating newline).
53 /// Returns an [`SseEvent`] when this line is the blank line that dispatches
54 /// the accumulated record.
55 pub fn push_line(&mut self, line: &str) -> Option<SseEvent> {
56 if line.is_empty() {
57 return self.dispatch();
58 }
59 if line.starts_with(':') {
60 // Comment line: ignored, but it still marks an active record so a
61 // following blank line dispatches an empty event only if fields set.
62 return None;
63 }
64 let (field, value) = match line.split_once(':') {
65 Some((field, rest)) => (field, rest.strip_prefix(' ').unwrap_or(rest)),
66 None => (line, ""),
67 };
68 match field {
69 "event" => {
70 self.event = Some(value.to_owned());
71 self.have_record = true;
72 }
73 "data" => {
74 self.data.push(value.to_owned());
75 self.have_record = true;
76 }
77 _ => {}
78 }
79 None
80 }
81
82 fn dispatch(&mut self) -> Option<SseEvent> {
83 if !self.have_record {
84 return None;
85 }
86 let event = self.event.take();
87 let data = std::mem::take(&mut self.data).join("\n");
88 self.have_record = false;
89 Some(SseEvent { event, data })
90 }
91
92 /// Flush any buffered partial line and any accumulated-but-undispatched
93 /// record (a stream that ended without a final blank line).
94 pub fn flush(&mut self) -> Option<SseEvent> {
95 if let Some(line) = self.lines.flush() {
96 let text = String::from_utf8_lossy(&line);
97 if let Some(event) = self.push_line(&text) {
98 return Some(event);
99 }
100 }
101 self.dispatch()
102 }
103}