// SSE 自动重连:`SseStream::next_event` 用到的运行时状态与重连循环。
//
// 为什么循环放在 client 包而不是 sse 包:重连要重新走 HTTP 管线
// (合并配置、请求拦截器、重定向、状态码校验),还要用取消信号等待;
// sse 包保持纯逻辑,只暴露 last_id / retry 的持久状态。
///|
/// 重连所需的运行时状态。
priv struct SseReconnectState {
/// 用来重新走完整管线(合并默认值、请求拦截器、重定向、cookie、状态码校验)。
client : Client
/// 调用方传进来的**原始**配置(未合并、未经过拦截器)。
/// 每次重连都从它出发,让请求拦截器 / 重定向 / cookie 重新生效。
config : Config
/// 与首次请求共享的取消信号。
signal : AbortSignal?
/// 建流时确定的策略。
policy : SseReconnect
/// 连续重连次数;交付事件后清零。
mut attempts : Int32
}
///|
/// 手写 Debug:Client 与 AbortSignal 都不该整段渲染,只给出连续失败次数。
impl @debug.Debug for SseReconnectState with fn to_repr(self) {
@debug.Repr::opaque_(
"SseReconnectState",
@debug.Repr::integer(self.attempts.to_string()),
)
}
///|
fn SseReconnectState::new(
client : Client,
config : Config,
signal : AbortSignal?,
policy : SseReconnect,
) -> SseReconnectState {
{ client, config, signal, policy, attempts: 0, }
}
///|
/// 这个失败值不值得重连。
///
/// 先看响应状态码:4xx 是「请求本身有问题」,重试只会得到同样结果;
/// 5xx 通常是暂时性故障,可以按 retry 等待后重试。没有响应(连不上、
/// 建连超时)或 2xx 上的读失败(网络断、单次读取超时)再看错误码。
/// 取消永远不重试——那是调用方主动叫停,不是连接故障。
fn retryable_error(error : HttpError) -> Bool {
if error.is_cancelled() {
return false
}
match error.response() {
Some(response) => {
if response.status >= 500 && response.status < 600 {
return true
}
if response.status < 200 || response.status >= 300 {
return false
}
}
None => ()
}
match error.code() {
ErrorCode::Network | ErrorCode::Timeout => true
_ => false
}
}
///|
/// 把重连请求的配置准备好:在原始配置上补 `Last-Event-ID`。
///
/// 空字符串 id 表示「服务端用 `id:` 空值清掉了」,按规范不再发这个头;
/// 解析器还没有 id 时也保留调用方自己设置的 `Last-Event-ID`。
fn SseReconnectState::next_config(
self : SseReconnectState,
parser : @sse.SseParser,
) -> Config {
match parser.last_event_id() {
Some(id) =>
if id.is_empty() {
self.config
} else {
self.config.with_header("Last-Event-ID", id)
}
None => self.config
}
}
///|
/// 按策略重连,直到拿到一条可读的连接或决定放弃。
///
/// 返回 `true`:`self.body` 已经换成新连接,调用方继续读。
/// 返回 `false`:`closed` 了,或干净 EOF 下次数用尽——调用方应返回 `None`。
/// 抛错:最后一次失败是错误且次数用尽,或遇到不可重试的错误。
///
/// `last_error` 是触发这次重连的失败:`None` 表示干净 EOF。
async fn SseStream::reconnect_loop(
self : SseStream,
state : SseReconnectState,
last_error : HttpError?,
) -> Bool raise HttpError {
let mut last_error = last_error
while true {
if self.closed {
return false
}
match state.policy.max_retries {
Some(limit) =>
if state.attempts >= limit {
match last_error {
Some(error) => raise error
None => return false
}
}
None => ()
}
let delay = match self.parser.retry_interval() {
Some(milliseconds) => milliseconds
None => state.policy.default_delay
}
// 等待期间取消要立刻生效;`close()` 不打断等待(见 docs/06-sse.md)。
@transport.sleep_with_abort(state.signal, delay) catch {
error => raise self.read_error(error)
}
if self.closed {
return false
}
state.attempts = state.attempts + 1
let config = state.next_config(self.parser)
let opened = open_sse(state.client, config, state.signal) catch {
error => {
if !retryable_error(error) {
raise error
}
last_error = Some(error)
continue
}
}
let (raw, _merged) = opened
if self.closed {
// 等待 / 建连期间调用方关掉了流:新连接也不能留。
raw.body.close()
return false
}
// 同一个解析器接新流:id / retry 保留,BOM 判定与上一连接残留清掉。
self.parser.begin_stream()
self.body = raw.body
return true
} nobreak {
false
}
// `while true` 的兜底值;正常路径都走 return / raise,不会到这里。
}