Basic zio-http based live router

This commit is contained in:
Paul-Henri Froidmont 2025-08-20 01:31:10 +02:00
parent cff02a4c96
commit 385436f8fe
Signed by: phfroidmont
GPG key ID: BE948AFD7E7873BE
6 changed files with 80 additions and 96 deletions

View file

@ -11,7 +11,7 @@ def main =
)
val s = Socket(TestView(initModel))
println("Init")
println(s.renderHtml)
println(s.renderHtml())
s.syncClient
s.syncClient
@ -34,7 +34,7 @@ def main =
)
)
s.syncClient
println(s.renderHtml)
println(s.renderHtml())
println("Add one")
s.receiveCommand(
@ -50,7 +50,7 @@ def main =
)
)
s.syncClient
println(s.renderHtml)
println(s.renderHtml())
println("Remove first")
s.receiveCommand(
@ -65,7 +65,7 @@ def main =
)
)
s.syncClient
println(s.renderHtml)
println(s.renderHtml())
println("Remove all")
s.receiveCommand(
@ -79,5 +79,5 @@ def main =
)
s.syncClient
s.syncClient
println(s.renderHtml)
println(s.renderHtml())
end main

View file

@ -12,9 +12,9 @@ final case class Socket[Cmd](lv: LiveView[Cmd]):
def receiveCommand(cmd: Cmd): Unit =
lv.handleCommand(cmd)
def renderHtml: String =
def renderHtml(rootLayout: HtmlElement => HtmlElement = identity): String =
lv.el.syncAll()
HtmlBuilder.build(lv.el, isRoot = true)
HtmlBuilder.build(rootLayout(lv.el))
def syncClient: Unit =
lv.el.syncAll()

View file

@ -1,72 +1,29 @@
package scalive
import zio.*
import zio.http.ChannelEvent.{ExceptionCaught, Read, UserEvent, UserEventTriggered}
import zio.http.*
import zio.http.template.Html
object Example extends ZIOAppDefault:
val s = Socket(new TestView())
val socketApp: WebSocketApp[Any] =
Handler.webSocket { channel =>
channel.receiveAll {
case Read(WebSocketFrame.Text("end")) =>
channel.shutdown
// Send a "bar" if the client sends a "foo"
case Read(WebSocketFrame.Text("foo")) =>
channel.send(Read(WebSocketFrame.text("bar")))
// Send a "foo" if the client sends a "bar"
case Read(WebSocketFrame.Text("bar")) =>
channel.send(Read(WebSocketFrame.text("foo")))
// Echo the same message 10 times if it's not "foo" or "bar"
case Read(WebSocketFrame.Text(text)) =>
channel
.send(Read(WebSocketFrame.text(s"echo $text")))
.repeatN(10)
.catchSomeCause { case cause =>
ZIO.logErrorCause(s"failed sending", cause)
}
// Send a "greeting" message to the client once the connection is established
case UserEventTriggered(UserEvent.HandshakeComplete) =>
channel.send(Read(WebSocketFrame.text("Greetings!")))
// Log when the channel is getting closed
case Read(WebSocketFrame.Close(status, reason)) =>
Console.printLine(
"Closing channel with status: " + status + " and reason: " + reason
val liveRouter =
LiveRouter(
RootLayout(_),
List(
LiveRoute(
Root / "test",
(_, req) =>
val q = req.queryParam("q").map("Param : " ++ _).getOrElse("No param")
TestView(q)
)
)
)
// Print the exception if it's not a normal close
case ExceptionCaught(cause) =>
Console.printLine(s"Channel error!: ${cause.getMessage}")
case _ =>
ZIO.unit
}
}
val routes: Routes[Any, Response] =
Routes(
Method.GET / "" -> handler { (_: Request) =>
Response.html(Html.raw(s.renderHtml))
},
Method.GET / "live" / "ws" -> handler(socketApp.toResponse)
)
override val run = Server.serve(routes).provide(Server.default)
end Example
override val run = Server.serve(liveRouter.routes).provide(Server.default)
final case class MyModel(elems: List[NestedModel], cls: String = "text-xs")
final case class NestedModel(name: String, age: Int)
class TestView extends LiveView[Nothing]:
class TestView(someParam: String) extends LiveView[Nothing]:
val model = Var(
MyModel(
@ -82,6 +39,7 @@ class TestView extends LiveView[Nothing]:
val el =
div(
h1(someParam),
idAttr := "42",
cls := model(_.cls),
ul(
@ -95,3 +53,4 @@ class TestView extends LiveView[Nothing]:
)
)
)
end TestView

View file

@ -0,0 +1,53 @@
package scalive
import zio.*
import zio.http.*
import zio.http.ChannelEvent.Read
import zio.http.codec.PathCodec
import zio.http.template.Html
import zio.json.JsonCodec
final case class LiveRoute[A, Cmd](
path: PathCodec[A],
liveviewBuilder: (A, Request) => LiveView[Cmd]):
def toZioRoute(rootLayout: HtmlElement => HtmlElement): Route[Any, Nothing] =
Method.GET / path -> handler { (params: A, req: Request) =>
val s = Socket(liveviewBuilder(params, req))
Response.html(Html.raw(s.renderHtml(rootLayout)))
}
// 1 Request to live route
// 2 Create live view with stateless token containing user id if connected, http params, live view id
// 3 Response with HTML and token
// 4 Websocket connection with token
// 5 Recreate exact same liveview as before using token data
class LiveRouter(rootLayout: HtmlElement => HtmlElement, liveRoutes: List[LiveRoute[?, ?]]):
private val socketApp: WebSocketApp[Any] =
Handler.webSocket { channel =>
channel.receiveAll {
case Read(WebSocketFrame.Text(content)) =>
// content.fromJson[SocketMessage]
channel.send(Read(WebSocketFrame.text("bar")))
case _ => ZIO.unit
}
}
val routes: Routes[Any, Response] =
Routes.fromIterable(
liveRoutes
.map(route => route.toZioRoute(rootLayout)).prepended(
Method.GET / "live" / "ws" -> handler(socketApp.toResponse)
)
)
final case class SocketMessage(
// Live session ID, auto increment defined by the client on join
joinRef: Option[Int],
// Message ID, global auto increment defined by the client on every message
messageRef: Int,
// LiveView instance id
topic: String,
payload: String)
derives JsonCodec

View file

@ -3,11 +3,13 @@ package scalive
import scalive.HtmlElement
object RootLayout:
def apply[RootModel](content: HtmlElement): HtmlElement =
def apply(content: HtmlElement): HtmlElement =
htmlRootTag(
lang := "en",
metaTag(charset := "utf-8"),
headTag(
metaTag(charset := "utf-8")
),
bodyTag(
// content
content
)
)

View file

@ -1,30 +0,0 @@
package scalive
import zio.http.Response
import zio.http.template.Html
// trait LiveRouter:
// type RootModel
// private lazy val viewsMap: Map[String, View] = views.map(r => (r.name, r.view)).toMap
// def rootLayout: HtmlElement[RootModel]
// def views: Seq[LiveRoute]
//
// final case class LiveRoute(name: String, view: View)
object ZioLiveApp:
// 1 Request to live route
// 2 Create live view with stateless token containing user id if connected, http params, live view id
// 3 Response with HTML and token
// 4 Websocket connection with token
// 5 Recreate exact same liveview as before using token data
// val testRoute = LiveRoute("test", TestView)
// val router = new LiveRouter:
// val rootLayout = htmlRootTag()
// val views = Seq(testRoute)
// def htmlRender(v: View, model: v.Model) =
// val lv = LiveView(v, model)
// Response.html(Html.raw(HtmlBuilder.build(lv, isRoot = true)))
private val socketApp = ???