Datastar HTTP
ascent-datastar-http makes a zio-http server "an ascent client": render a UI with
ascent-html, push patch-elements / patch-signals through the official datastar SDK.
Keep the SDK's events { handler { … } } idiom; swap templates for ascent views.
patchSignal
Handler-level assert: the SSE body carries a datastar-patch-signals frame with the named value.
{
val handler0: Handler[Datastar, Nothing, Any, Unit] = Handler.fromZIO {
AscentDatastar.patchSignal("count", 42)
}
val sse = events(handler0)
for
response <- sse(())
body <- response.body.asString.orDie
yield (
response.status == Status.Ok,
body.contains("event: datastar-patch-signals"),
body.contains("\"count\":42"),
)
}(true,true,true)Live counter server
End-to-end against an in-process server (ephemeral port, scoped to the example): POST /increment
succeeds while the server is live, then the Scope closes and the port is released.
{
case class State(count: Ref[Int], pulse: Hub[Unit])
def pushCount(state: State): ZIO[Datastar, Nothing, Unit] =
state.count.get.flatMap(c => AscentDatastar.patchSignal("count", c)(using JsonEncoder.int))
def routes(state: State): Routes[Any, Nothing] =
Routes(
Method.GET / "sse" -> events {
handler { (_: Request) =>
for
_ <- pushCount(state)
stream <- state.pulse.subscribe.map(zio.stream.ZStream.fromQueue(_))
_ <- stream.mapZIO(_ => pushCount(state)).runDrain
yield ()
}
},
Method.POST / "increment" -> handler { (_: Request) =>
(state.count.update(_ + 1) *> state.pulse.publish(()).unit).as(Response.ok)
},
).sandbox
(for
count <- Ref.make(0)
pulse <- Hub.unbounded[Unit]
state = State(count, pulse)
port <- Server.install(routes(state))
base = s"http://localhost:$port"
st <- Client.batched(Request.post(s"$base/increment", Body.empty)).map(_.status)
yield st)
.provideSomeLayer(Server.defaultWith(_.port(0)) ++ Client.default)
.orDie
}Ok