Hybrid regions
A hybrid UI keeps chrome on the client (inputs, layout, local Squawks) and owns a region on
the server. The client mounts serverRegion("messages") (an empty #messages container). The
server fills it with AscentDatastar.patchRegion("messages", ui).
Docs pages have no live datastar server, so the client shape is shown as source only. For a
working chat, see example/hybrid-chat.
Client declaration
E.div(
E.h2("Chat"),
serverRegion("messages"),
E.input(A.placeholder("type…")),
)
Server patchRegion
patchRegion targets #id with inner mode by default; the same id the client mounted.
{
val list = E.ul(E.li("hello"), E.li("world"))
val handler0: Handler[Datastar, Nothing, Any, Unit] = Handler.fromZIO {
AscentDatastar.patchRegion("messages", list)
}
val sse = events(handler0)
for
response <- sse(())
body <- response.body.asString.orDie
yield (
body.contains("event: datastar-patch-elements"),
body.contains("data: selector #messages"),
body.contains("hello"),
)
}(true,true,true)Scoped live server
Same Scope-bound pattern as the counter: start routes, POST a message, confirm OK. Full chat
wiring lives in example/hybrid-chat.
{
def pushMessages(msgs: Ref[Vector[String]]): ZIO[Datastar, Nothing, Unit] =
msgs.get.flatMap { items =>
AscentDatastar.patchRegion(
"messages",
E.ul(items.map(s => E.li(s))*),
ascent.datastar.ElementPatchMode.Inner,
)
}
def routes(msgs: Ref[Vector[String]], pulse: Hub[Unit]): Routes[Any, Nothing] =
Routes(
Method.GET / "sse" -> events {
handler { (_: Request) =>
for
_ <- pushMessages(msgs)
stream <- pulse.subscribe.map(zio.stream.ZStream.fromQueue(_))
_ <- stream.mapZIO(_ => pushMessages(msgs)).runDrain
yield ()
}
},
Method.POST / "send" -> handler { (_: Request) =>
(msgs.update(_ :+ "ping") *> pulse.publish(()).unit).as(Response.ok)
},
).sandbox
(for
pulse <- Hub.unbounded[Unit]
msgs <- Ref.make(Vector("welcome"))
port <- Server.install(routes(msgs, pulse))
base = s"http://localhost:$port"
st <- Client.batched(Request.post(s"$base/send", Body.empty)).map(_.status)
yield st)
.provideSomeLayer(Server.defaultWith(_.port(0)) ++ Client.default)
.orDie
}Ok