AkurAI Build
Menu

Public repository / master

AkurAI-Framework / crates/http/src/sse.rs

Latest change 5458532ad0ba451030d0554d03be8ff383ccddb8 - feat(ws,http): pure-std WebSocket (RFC 6455) + Server-Sent Events by Ólafur Búi Ólafsson

//! Server-Sent Events — a one-way `text/event-stream` over a hijacked
//! connection, per the WHATWG HTML "server-sent events" spec.
//!
//! A handler calls [`sse`] with a closure; the framework writes the
//! `text/event-stream` head, then hands the closure an [`SseSink`] it writes
//! events to for as long as the client stays connected. When the client goes
//! away the next write fails and the closure returns — the dedicated upgrade
//! thread then ends. No framing library, no async: just bytes to a socket.
//!
//! Wire format (each event ends with a blank line):
//! ```text
//! event: tick
//! id: 7
//! data: {"n":1}
//! <blank line>
//! ```
//! Multi-line `data` is emitted as one `data:` line per source line, which the
//! browser rejoins with `\n`. Lines beginning with `:` are comments — used here
//! for keep-alive heartbeats.

use crate::{Reply, Response};
use std::io::{self, Write};
use std::net::TcpStream;

/// Build an SSE [`Reply`]: a `200 text/event-stream` head plus a hijack that
/// runs `body` against the live connection.
pub fn sse<F>(body: F) -> Reply
where
    F: FnOnce(SseSink) -> io::Result<()> + Send + 'static,
{
    let head = Response::new(200)
        .with_header("Content-Type", "text/event-stream; charset=utf-8")
        .with_header("Cache-Control", "no-cache")
        .with_header("Connection", "keep-alive")
        // Defeat proxy buffering so events are delivered as they happen.
        .with_header("X-Accel-Buffering", "no");
    Reply::upgrade(head, move |stream| body(SseSink::new(stream)))
}

/// The writable end of an SSE stream. Every method flushes, so an event reaches
/// the client immediately rather than sitting in a buffer.
pub struct SseSink {
    stream: TcpStream,
}

impl SseSink {
    fn new(stream: TcpStream) -> SseSink {
        SseSink { stream }
    }

    /// Send a `data`-only event. The common case: `sink.data("hello")?`.
    pub fn data(&mut self, data: &str) -> io::Result<()> {
        self.send(&Event::new().data(data))
    }

    /// Send a named event: `sink.event("tick", payload)?`.
    pub fn event(&mut self, name: &str, data: &str) -> io::Result<()> {
        self.send(&Event::new().name(name).data(data))
    }

    /// Send a fully-specified event (id / event / data / retry).
    pub fn send(&mut self, event: &Event) -> io::Result<()> {
        self.stream.write_all(&event.encode())?;
        self.stream.flush()
    }

    /// Write a comment line (`: ...`). Sends no event; use as a heartbeat to
    /// keep the connection (and intermediary proxies) alive.
    pub fn comment(&mut self, text: &str) -> io::Result<()> {
        let mut out = Vec::new();
        for line in split_lines(text) {
            out.extend_from_slice(b": ");
            out.extend_from_slice(line.as_bytes());
            out.push(b'\n');
        }
        out.push(b'\n');
        self.stream.write_all(&out)?;
        self.stream.flush()
    }

    /// Advise the client's reconnection delay, in milliseconds.
    pub fn retry(&mut self, millis: u64) -> io::Result<()> {
        self.send(&Event::new().retry(millis))
    }
}

/// A builder for one SSE event. Any combination of fields; at least one should
/// be set for the event to mean anything.
#[derive(Default, Clone)]
pub struct Event {
    id: Option<String>,
    name: Option<String>,
    data: Option<String>,
    retry: Option<u64>,
}

impl Event {
    pub fn new() -> Event {
        Event::default()
    }
    /// Set the event id (becomes the client's `Last-Event-ID` on reconnect).
    pub fn id(mut self, id: &str) -> Event {
        self.id = Some(id.to_string());
        self
    }
    /// Set the event type (`event:` field); the browser dispatches it by name.
    pub fn name(mut self, name: &str) -> Event {
        self.name = Some(name.to_string());
        self
    }
    pub fn data(mut self, data: &str) -> Event {
        self.data = Some(data.to_string());
        self
    }
    pub fn retry(mut self, millis: u64) -> Event {
        self.retry = Some(millis);
        self
    }

    /// Serialize to the wire form, ending with the blank line that dispatches
    /// the event. Field order follows the spec's processing model.
    pub fn encode(&self) -> Vec<u8> {
        let mut out = Vec::new();
        if let Some(id) = &self.id {
            // An id must be single-line; ignore embedded newlines defensively.
            field(&mut out, "id", &first_line(id));
        }
        if let Some(name) = &self.name {
            field(&mut out, "event", &first_line(name));
        }
        if let Some(retry) = self.retry {
            field(&mut out, "retry", &retry.to_string());
        }
        if let Some(data) = &self.data {
            for line in split_lines(data) {
                field(&mut out, "data", line);
            }
        }
        out.push(b'\n'); // blank line → dispatch
        out
    }
}

/// Write one `field: value\n` line.
fn field(out: &mut Vec<u8>, name: &str, value: &str) {
    out.extend_from_slice(name.as_bytes());
    out.extend_from_slice(b": ");
    out.extend_from_slice(value.as_bytes());
    out.push(b'\n');
}

/// Split on any newline style (`\r\n`, `\r`, `\n`) — the event stream uses `\n`
/// as its own separator, so embedded newlines must each become a new line.
fn split_lines(s: &str) -> impl Iterator<Item = &str> {
    s.split('\n').map(|l| l.strip_suffix('\r').unwrap_or(l))
}

fn first_line(s: &str) -> String {
    split_lines(s).next().unwrap_or("").to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    fn encoded(e: &Event) -> String {
        String::from_utf8(e.encode()).unwrap()
    }

    #[test]
    fn data_event_ends_with_blank_line() {
        assert_eq!(encoded(&Event::new().data("hello")), "data: hello\n\n");
    }

    #[test]
    fn named_event_orders_fields() {
        assert_eq!(
            encoded(&Event::new().id("7").name("tick").data("{\"n\":1}")),
            "id: 7\nevent: tick\ndata: {\"n\":1}\n\n"
        );
    }

    #[test]
    fn multiline_data_becomes_multiple_data_lines() {
        assert_eq!(
            encoded(&Event::new().data("line one\nline two")),
            "data: line one\ndata: line two\n\n"
        );
    }

    #[test]
    fn crlf_in_data_is_normalized() {
        assert_eq!(
            encoded(&Event::new().data("a\r\nb")),
            "data: a\ndata: b\n\n"
        );
    }

    #[test]
    fn retry_only_event() {
        assert_eq!(encoded(&Event::new().retry(3000)), "retry: 3000\n\n");
    }

    #[test]
    fn id_newlines_are_stripped() {
        // An id field must not smuggle a second line into the stream.
        assert_eq!(
            encoded(&Event::new().id("a\nb").data("x")),
            "id: a\ndata: x\n\n"
        );
    }
}