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")
  }
}
true
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.