// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// The event side of a `text/event-stream`: each `send` is one frame, written as its own chunk.
pub struct Events {
priv writer : Writer
priv space : Bool
}
///|
/// Send `event` as one frame. A client dispatches it when it reads the frame's blank line, so
/// it goes out on its own rather than waiting for the next.
pub async fn Events::send(self : Events, event : @sse.Event) -> Unit {
self.writer.write(@utf8.encode(event.encode(space=self.space)[:]))
}
///|
/// Send a comment line, which a client ignores and an idle proxy takes as traffic.
pub async fn Events::ping(self : Events, comment~ : String = "") -> Unit {
self.writer.write_text(": \{comment}\n\n")
}
///|
/// A `text/event-stream` response (← sse-starlette's `EventSourceResponse`). `body` sends
/// frames for as long as it likes; the app cancels it when the client goes.
///
/// Framing an event is `moonhttp/sse`'s job. What belongs here is the response around the
/// frames, and the three headers that stop an intermediary buffering or closing the stream.
/// One named in `headers` too replaces ours rather than joining it, because a response carrying
/// two `content-type` headers is not a response with a choice in it. `wins=Base` keeps ours,
/// and `clash` (moonknob's) can warn, abort or take a callback when a header is given a
/// different value — worth setting when overriding `content-type`, which stops the stream
/// being a stream.
pub fn sse(
body : async (Events) -> Unit,
status~ : Int32 = 200,
headers~ : Array[(String, String)] = [],
space~ : Bool = true,
wins~ : @moonknob.Wins = Extra,
clash~ : @moonknob.OnClash[Array[(String, String)]] = Ignore,
) -> Stream {
let base : Array[(String, String)] = [
("content-type", "\{@sse.media_type}; charset=utf-8"),
("cache-control", "no-cache"),
("connection", "keep-alive"),
]
let merge = @moonknob.Merge::new(wins)
let merged = base.copy()
for header in headers {
let name = header.0.to_lower()
match merged.search_by(seen => seen.0.to_lower() == name) {
None => merged.push(header)
Some(at) =>
if merge.pick(name, merged[at].1, Some(header.1)) != merged[at].1 {
merged[at] = header
}
}
}
let hs = merge.settle(clash, base, merged, "sse: our headers and `headers`")
Stream::new(status~, headers=hs, writer => body({ writer, space, }))
}
///|
pub using @sse {type Event}