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))
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
// 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)
|
||||
val liveRouter =
|
||||
LiveRouter(
|
||||
RootLayout(_),
|
||||
List(
|
||||
LiveRoute(
|
||||
Root / "test",
|
||||
(_, req) =>
|
||||
val q = req.queryParam("q").map("Param : " ++ _).getOrElse("No param")
|
||||
TestView(q)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
|
|
|
|||
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
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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