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. data may span lines. event and id are SseFields, which never hold a CR or LF, so a value cannot end its field and forge another: SseField("tick") checks a literal at compile time, SseField.from(raw) returns Left(SseFieldError.LineBreak(at)) for a value that breaks a line, and SseField.of(n) takes a number.

{
  val tick   = SseField("tick")
  val routes = Routes(
    Method.GET / "ticks" -> handler(
      ZIO.succeed(
        Sse.response(
          ZStream.fromIterable(
            List(
              ServerSentEvent(data = "0", event = Some(tick), id = Some(SseField.of(0))),
              ServerSentEvent(data = "1", event = Some(tick), id = Some(SseField.of(1))),
            )
          )
        )
      )
    )
  )
  routes(Request.get("/ticks")).flatMap(_.body.utf8).map { raw =>
    raw.contains("event: tick") && raw.contains("data: 0") && raw.contains("data: 1")
  }
}
true
SseField("tick\ndata: forged")
an SSE field holds no CR or LF, and this has one at 4
  expectFail("""SseField("tick\ndata: forged")""").assert { errors =>
id 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")
  }
}
true
heddle.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.