feat(sse): Conn::sse() sugar — EventSender + pump heartbeats (v0.3 chunk 3)
New src/sse.rs. Conn::sse() (and sse_with_heartbeat) turns the response
into a text/event-stream: status 200 if unset, content-type +
cache-control: no-cache, and a RespBody::Stream whose heartbeat is wired
to the new StreamBody.heartbeat field. Returns (Conn, EventSender);
EventSender is Clone (stream ends when the LAST clone drops) with
send(event, data) / data(data) / comment(text), all returning
Err(SseClosed) once the conn actor has dropped the stream — the producer
exit signal. Multi-line data becomes one data: line per line (pure
format_event, unit-tested).
Heartbeat lives in the pump, as the roadmap leaned: with
StreamBody.heartbeat set, pump_stream waits recv_timeout(interval) and on
expiry writes the payload (": keep-alive" comment chunk) and keeps
waiting. Liveness inversion is deliberate: no request clock governs an
SSE response (request_timeout is read-phase only since chunk 1); a dead
client is detected when an event or heartbeat write stalls past
write_timeout, and a draining shutdown force-stops the recv park like any
stream.
Tests: 4 unit (3 framing, 1 sse() wiring) + 2 integration (event round
trip over decoded chunked stream incl. named + unnamed events and clean
0-chunk EOS on sender drop; >=2 heartbeats observed across 450ms of
producer silence at a 100ms interval).
This commit is contained in:
+13
-1
@@ -174,11 +174,23 @@ pub enum RespBody {
|
||||
/// body.
|
||||
pub struct StreamBody {
|
||||
pub rx: smarm::Receiver<Vec<u8>>,
|
||||
/// If set, the connection actor's pump waits for each chunk with
|
||||
/// `recv_timeout(interval)` instead of a bare `recv`, and on expiry
|
||||
/// writes `payload` as its own chunk and keeps waiting. This is how
|
||||
/// SSE keep-alive comments work: liveness comes from the ping (a dead
|
||||
/// client is detected when a heartbeat write stalls past
|
||||
/// `write_timeout`), not from any request clock.
|
||||
pub heartbeat: Option<(std::time::Duration, Vec<u8>)>,
|
||||
}
|
||||
|
||||
impl StreamBody {
|
||||
pub fn new(rx: smarm::Receiver<Vec<u8>>) -> Self {
|
||||
Self { rx }
|
||||
Self { rx, heartbeat: None }
|
||||
}
|
||||
|
||||
pub fn with_heartbeat(mut self, interval: std::time::Duration, payload: Vec<u8>) -> Self {
|
||||
self.heartbeat = Some((interval, payload));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user