// SSE 解析的字节级细节:扫描行尾、切字段、解析字段值。
//
// 与 sse.mbt 分开是为了守在 AGENTS.md 的单文件 300 行红线内,
// 也因为这一层可以纯粹按「字节进、字段出」来读:它不认识事件,
// 只认识行。规范里的每条规则都在这一层落成一段可以单独讲清的代码。
///|
/// 丢掉流开头的 UTF-8 BOM(规范要求)。只看流开头一次。
///
/// 字节不够判定时先不动:`0xEF` 开头的三字节序列可能是 BOM,
/// 也可能是普通字符的一部分,要凑够三个字节才能定案。
/// 只在开头处理,也就不会误伤值中间的 U+FEFF。
fn SseParser::skip_bom(self : SseParser) -> Unit {
if self.head_done {
return
}
let length = self.pending.length()
if length == 0 {
return
}
if self.pending[0].to_int() != 0xEF {
// 开头不是 BOM 的第一个字节,可以定案了。
self.head_done = true
return
}
if length < 3 {
// 还差字节,下次 push 再判。
return
}
if self.pending[1].to_int() == 0xBB && self.pending[2].to_int() == 0xBF {
self.pending = self.pending.exact_view(start=3).to_owned()
}
self.head_done = true
}
///|
/// 从 `from` 开始找一行的结束位置,返回 `(行尾下标, 行尾字节数)`。
///
/// 三种行尾都认:CRLF 是 2 个字节,单独的 LF 或 CR 是 1 个。
///
/// **CR 落在缓冲区末尾时返回 `None`**:它可能是 CRLF 的前半,
/// 现在消费掉就会把一次换行当成两次,凭空多切出一个空行——也就是一个假事件。
/// 等下一块数据到了再判定,这正是跨块安全的关键。
fn find_line_end(buf : Bytes, from : Int) -> (Int, Int)? {
let length = buf.length()
let mut index = from
while index < length {
let byte = buf[index].to_int()
if byte == LF {
return Some((index, 1))
}
if byte == CR {
if index + 1 >= length {
return None
}
if buf[index + 1].to_int() == LF {
return Some((index, 2))
}
return Some((index, 1))
}
index = index + 1
}
None
}
///|
/// 把 `pending` 里所有**行尾完整**的行交给 `handle_line`,
/// 返回第一个未消费字节的位置。
///
/// 「行尾不完整」有两种:还没收到换行,以及只收到了 CRLF 的前半个 CR。
/// 两种都由 `find_line_end` 返回 `None`,这里就停手等下一块数据。
fn SseParser::consume_lines(self : SseParser) -> Int {
let mut start = 0
let mut progress = true
while progress {
match find_line_end(self.pending, start) {
None => progress = false
Some((terminator, step)) => {
self.handle_line(start, terminator)
start = terminator + step
}
}
}
start
}
///|
/// 处理 `pending[start:end]` 这一行。
fn SseParser::handle_line(self : SseParser, start : Int, end : Int) -> Unit {
// 空行 = 事件边界,分发当前累积的事件。
if start == end {
self.dispatch()
return
}
// `:` 开头的整行是注释,忽略。心跳常写成 `: ping`,
// 它存在的意义就是「别让连接超时」,本身不该变成事件。
if self.pending[start].to_int() == COLON {
return
}
let (field, value_start, value_end) = self.split_field(start, end)
// 值取成独立的一份:后面要写 `self` 的字段,不能让它一直借着 `pending`。
let value = self.pending
.exact_view(start=value_start, end=value_end)
.to_owned()
match field {
"data" => self.data.push(value)
"event" => self.event = @utf8.decode_lossy(value)
// 含 U+0000 的 id 必须忽略(规范),否则重连时会把非法值
// 塞进 `Last-Event-ID` 请求头。
"id" =>
if !has_nul(value) {
self.last_id = Some(@utf8.decode_lossy(value))
}
"retry" =>
match parse_retry(value) {
Some(milliseconds) => self.retry = Some(milliseconds)
None => ()
}
// 其余字段(含规范未定义的)一律忽略——规范也是这么要求的。
_ => ()
}
}
///|
/// 把一行切成字段名与值,返回 `(字段名, 值起点, 值终点)`。
///
/// - 第一个 `:` 之前是字段名、之后是值;
/// - 值只去掉**一个**前导空格,多出来的空格属于值本身;
/// - 整行没有 `:` 时,字段名是整行、值为空串。
fn SseParser::split_field(
self : SseParser,
start : Int,
end : Int,
) -> (String, Int, Int) {
// 只在行内找冒号:`find` 要作用在视图上,否则会从前面的行里找到冒号。
let line = self.pending.exact_view(start~, end~)
match line.find(b":") {
None => (@utf8.decode_lossy(line), end, end)
Some(colon) => {
let mut value_start = start + colon + 1
if value_start < end && self.pending[value_start].to_int() == SPACE {
value_start = value_start + 1
}
let field = self.pending.exact_view(start~, end=start + colon)
(@utf8.decode_lossy(field), value_start, end)
}
}
}
///|
/// 值里是否含 U+0000(只给 `id:` 用)。
fn has_nul(value : BytesView) -> Bool {
for byte in value {
if byte.to_int() == 0 {
return true
}
}
false
}
///|
/// 解析 `retry:` 的值:**必须全为 ASCII 数字**才生效,否则整体忽略。
///
/// 规范明确「其它情况忽略」,所以 `retry: 1x`、`retry: ` 都不生效,
/// 也不会把已经生效的值改掉。
fn parse_retry(value : BytesView) -> Int? {
if value.is_empty() {
return None
}
let mut result = 0
for byte in value {
let code = byte.to_int()
if code < 48 || code > 57 {
return None
}
if result > MAX_RETRY {
return None
}
result = result * 10 + (code - 48)
}
Some(result)
}
///|
/// 把多条 `data:` 行的值用单个 `\n` 连接起来。
///
/// 等价于规范里「逐行把值追加到 data 缓冲再补一个 `\n`,分发前去掉最后一个 `\n`」,
/// 但不需要在每次分发时回头删字符。
fn join_lines(lines : Array[Bytes]) -> Bytes {
let mut result = b""
let mut index = 0
while index < lines.length() {
if index > 0 {
result = result + b"\n"
}
result = result + lines[index]
index = index + 1
}
result
}