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 sse  = events {
    AscentDatastar.patchRegion("messages", list)
  }
  (for
    response <- sse.run(Request.get("/sse"))
    body     <- response.body.utf8
  yield (
    body.contains("event: datastar-patch-elements"),
    body.contains("data: selector #messages"),
    body.contains("hello"),
  )).orDie
}
(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 {
        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)
      },
    )

  for
    pulse  <- Hub.unbounded[Unit]
    msgs   <- Ref.make(Vector("welcome"))
    result <- ZIO
      .scoped {
        Server.install(routes(msgs, pulse)).flatMap { server =>
          server.port.flatMap { port =>
            Client.request(Method.POST, s"http://127.0.0.1:$port/send").map(_.status)
          }
        }
      }
      .provide(Server.defaultWith(_.port(Port(0))))
  yield result
  end for
}
Status(200,OK)