mirror of
https://github.com/phfroidmont/scalive.git
synced 2025-12-25 05:26:59 +01:00
Basic zio-http based live router
This commit is contained in:
parent
cff02a4c96
commit
385436f8fe
6 changed files with 80 additions and 96 deletions
|
|
@ -11,7 +11,7 @@ def main =
|
||||||
)
|
)
|
||||||
val s = Socket(TestView(initModel))
|
val s = Socket(TestView(initModel))
|
||||||
println("Init")
|
println("Init")
|
||||||
println(s.renderHtml)
|
println(s.renderHtml())
|
||||||
s.syncClient
|
s.syncClient
|
||||||
s.syncClient
|
s.syncClient
|
||||||
|
|
||||||
|
|
@ -34,7 +34,7 @@ def main =
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
s.syncClient
|
s.syncClient
|
||||||
println(s.renderHtml)
|
println(s.renderHtml())
|
||||||
|
|
||||||
println("Add one")
|
println("Add one")
|
||||||
s.receiveCommand(
|
s.receiveCommand(
|
||||||
|
|
@ -50,7 +50,7 @@ def main =
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
s.syncClient
|
s.syncClient
|
||||||
println(s.renderHtml)
|
println(s.renderHtml())
|
||||||
|
|
||||||
println("Remove first")
|
println("Remove first")
|
||||||
s.receiveCommand(
|
s.receiveCommand(
|
||||||
|
|
@ -65,7 +65,7 @@ def main =
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
s.syncClient
|
s.syncClient
|
||||||
println(s.renderHtml)
|
println(s.renderHtml())
|
||||||
|
|
||||||
println("Remove all")
|
println("Remove all")
|
||||||
s.receiveCommand(
|
s.receiveCommand(
|
||||||
|
|
@ -79,5 +79,5 @@ def main =
|
||||||
)
|
)
|
||||||
s.syncClient
|
s.syncClient
|
||||||
s.syncClient
|
s.syncClient
|
||||||
println(s.renderHtml)
|
println(s.renderHtml())
|
||||||
end main
|
end main
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@ final case class Socket[Cmd](lv: LiveView[Cmd]):
|
||||||
def receiveCommand(cmd: Cmd): Unit =
|
def receiveCommand(cmd: Cmd): Unit =
|
||||||
lv.handleCommand(cmd)
|
lv.handleCommand(cmd)
|
||||||
|
|
||||||
def renderHtml: String =
|
def renderHtml(rootLayout: HtmlElement => HtmlElement = identity): String =
|
||||||
lv.el.syncAll()
|
lv.el.syncAll()
|
||||||
HtmlBuilder.build(lv.el, isRoot = true)
|
HtmlBuilder.build(rootLayout(lv.el))
|
||||||
|
|
||||||
def syncClient: Unit =
|
def syncClient: Unit =
|
||||||
lv.el.syncAll()
|
lv.el.syncAll()
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,29 @@
|
||||||
package scalive
|
package scalive
|
||||||
|
|
||||||
import zio.*
|
import zio.*
|
||||||
|
|
||||||
import zio.http.ChannelEvent.{ExceptionCaught, Read, UserEvent, UserEventTriggered}
|
|
||||||
import zio.http.*
|
import zio.http.*
|
||||||
import zio.http.template.Html
|
|
||||||
|
|
||||||
object Example extends ZIOAppDefault:
|
object Example extends ZIOAppDefault:
|
||||||
|
|
||||||
val s = Socket(new TestView())
|
val liveRouter =
|
||||||
|
LiveRouter(
|
||||||
val socketApp: WebSocketApp[Any] =
|
RootLayout(_),
|
||||||
Handler.webSocket { channel =>
|
List(
|
||||||
channel.receiveAll {
|
LiveRoute(
|
||||||
case Read(WebSocketFrame.Text("end")) =>
|
Root / "test",
|
||||||
channel.shutdown
|
(_, req) =>
|
||||||
|
val q = req.queryParam("q").map("Param : " ++ _).getOrElse("No param")
|
||||||
// Send a "bar" if the client sends a "foo"
|
TestView(q)
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Print the exception if it's not a normal close
|
override val run = Server.serve(liveRouter.routes).provide(Server.default)
|
||||||
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
|
|
||||||
|
|
||||||
final case class MyModel(elems: List[NestedModel], cls: String = "text-xs")
|
final case class MyModel(elems: List[NestedModel], cls: String = "text-xs")
|
||||||
final case class NestedModel(name: String, age: Int)
|
final case class NestedModel(name: String, age: Int)
|
||||||
|
|
||||||
class TestView extends LiveView[Nothing]:
|
class TestView(someParam: String) extends LiveView[Nothing]:
|
||||||
|
|
||||||
val model = Var(
|
val model = Var(
|
||||||
MyModel(
|
MyModel(
|
||||||
|
|
@ -82,6 +39,7 @@ class TestView extends LiveView[Nothing]:
|
||||||
|
|
||||||
val el =
|
val el =
|
||||||
div(
|
div(
|
||||||
|
h1(someParam),
|
||||||
idAttr := "42",
|
idAttr := "42",
|
||||||
cls := model(_.cls),
|
cls := model(_.cls),
|
||||||
ul(
|
ul(
|
||||||
|
|
@ -95,3 +53,4 @@ class TestView extends LiveView[Nothing]:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
end TestView
|
||||||
|
|
|
||||||
53
zio/src/scalive/LiveRouter.scala
Normal file
53
zio/src/scalive/LiveRouter.scala
Normal 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
|
||||||
|
|
@ -3,11 +3,13 @@ package scalive
|
||||||
import scalive.HtmlElement
|
import scalive.HtmlElement
|
||||||
|
|
||||||
object RootLayout:
|
object RootLayout:
|
||||||
def apply[RootModel](content: HtmlElement): HtmlElement =
|
def apply(content: HtmlElement): HtmlElement =
|
||||||
htmlRootTag(
|
htmlRootTag(
|
||||||
lang := "en",
|
lang := "en",
|
||||||
metaTag(charset := "utf-8"),
|
headTag(
|
||||||
|
metaTag(charset := "utf-8")
|
||||||
|
),
|
||||||
bodyTag(
|
bodyTag(
|
||||||
// content
|
content
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 = ???
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue