Realtime
SSE, WebSocket, and Datastar are public subpackages. They are not on import heddle.*.
They are HTTP hosts for streams. They are not MCP tools. OpArgs.promotable will refuse them.
Server-Sent Events
Sse.response wraps a ZStream of ServerSentEvent. Each event is flushed as its own HTTP
chunk. Sse.session is a writer you send / heartbeat into. JSON in data is JsonCodec.encode.
{
val routes = Routes(
Method.GET / "ticks" -> handler(
ZIO.succeed(
Sse.response(
ZStream.fromIterable(
List(
ServerSentEvent(data = "0", event = Some("tick"), id = Some("0")),
ServerSentEvent(data = "1", event = Some("tick"), id = Some("1")),
)
)
)
)
)
)
routes(Request.get("/ticks")).flatMap(_.body.utf8).map { raw =>
raw.contains("event: tick") && raw.contains("data: 0") && raw.contains("data: 1")
}
}trueid 0
Sse.response
event: tick id: 0 data: 0
Each ServerSentEvent is its own HTTP chunk. The fiber dies with the connection.
Datastar
heddle.datastar is SSE with Datastar event names. events { ... } is a Handler that runs
with a Datastar service. readSignals is an extension on Request, not a Request method.
{
val h = events {
ServerSentEventGenerator.patchElements(
"<div>hello</div>",
PatchElementOptions(selector = Some("#app"), mode = ElementPatchMode.Inner),
) *> ServerSentEventGenerator.patchSignals("""{"count":1}""")
}
Routes(Method.GET / "sse" -> h).runZIO(Request.get("/sse")).flatMap(_.body.utf8).map { raw =>
raw.contains("event: datastar-patch-elements") &&
raw.contains("data: selector #app") &&
raw.contains("event: datastar-patch-signals")
}
}trueheddle.datastar
event: datastar-patch-elements data: selector #app data: mode inner data: elements <div>hello</div>
WebSocket
The server speaks WebSocket upgrade (heddle.ws). There is no WebSocket client in this release.
Prefer SSE when a stream of events is enough.