From ccdd22b61a878f8ff3da587b73bc513b6964c89d Mon Sep 17 00:00:00 2001 From: Paul-Henri Froidmont Date: Mon, 25 Aug 2025 01:17:02 +0200 Subject: [PATCH] Rough websocket responses --- build.mill | 2 +- core/src/scalive/Socket.scala | 7 +++ zio/src/scalive/LiveRouter.scala | 77 +++++++++++++++++++++++++------- 3 files changed, 70 insertions(+), 16 deletions(-) diff --git a/build.mill b/build.mill index e5d9f4a..ac5a193 100644 --- a/build.mill +++ b/build.mill @@ -52,5 +52,5 @@ object example extends ScalaCommon: } object js extends TypeScriptModule: - def mainFileName = "app.ts" + def mainFileName = "app.js" def npmDeps = Seq("phoenix@1.7.21", "phoenix_live_view@1.1.8") diff --git a/core/src/scalive/Socket.scala b/core/src/scalive/Socket.scala index 9d7c21b..2451733 100644 --- a/core/src/scalive/Socket.scala +++ b/core/src/scalive/Socket.scala @@ -26,3 +26,10 @@ final case class Socket[Cmd](lv: LiveView[Cmd]): println(DiffBuilder.build(element, trackUpdates = clientInitialized).toJsonPretty) clientInitialized = true element.setAllUnchanged() + + def diff: Diff = + element.syncAll() + val diff = DiffBuilder.build(element, trackUpdates = clientInitialized) + clientInitialized = true + element.setAllUnchanged() + diff diff --git a/zio/src/scalive/LiveRouter.scala b/zio/src/scalive/LiveRouter.scala index 2eecb52..6cc85f3 100644 --- a/zio/src/scalive/LiveRouter.scala +++ b/zio/src/scalive/LiveRouter.scala @@ -1,5 +1,7 @@ package scalive +import scalive.SocketMessage.LiveResponse +import scalive.SocketMessage.Payload import zio.* import zio.http.* import zio.http.ChannelEvent.Read @@ -22,17 +24,41 @@ class LiveRouter(rootLayout: HtmlElement => HtmlElement, liveRoutes: List[LiveRo private val socketApp: WebSocketApp[Any] = Handler.webSocket { channel => - channel.receiveAll { - case Read(WebSocketFrame.Text(content)) => - for - _ <- - content.fromJson[SocketMessage].fold(m => ZIO.logError(m), m => ZIO.log(m.toString)) - _ <- channel.send(Read(WebSocketFrame.text("bar"))) - yield () - case _ => ZIO.unit - } + channel + .receiveAll { + case Read(WebSocketFrame.Text(content)) => + for + message <- ZIO + .fromEither(content.fromJson[SocketMessage]) + .mapError(new IllegalArgumentException(_)) + reply <- handleMessage(message) + _ <- channel.send(Read(WebSocketFrame.text(reply.toJson))) + yield () + case _ => ZIO.unit + }.tapErrorCause(ZIO.logErrorCause(_)) } + def handleMessage(message: SocketMessage): Task[SocketMessage] = + val reply = message.payload match + case Payload.Heartbeat => ZIO.succeed(Payload.Reply("ok", LiveResponse.Empty)) + case Payload.Join(url, session, static, sticky) => + // TODO very rough handling + ZIO + .fromEither(URL.decode(url)).map(url => + val req = Request(url = url) + liveRoutes + .collectFirst { route => + val pathParams = route.path.decode(req.path).getOrElse(???) + val lv = route.liveviewBuilder(pathParams, req) + val s = Socket(lv) + Payload.Reply("ok", LiveResponse.InitDiff(s.diff)) + + }.getOrElse(???) + ) + case Payload.Reply(_, _) => ZIO.die(new IllegalArgumentException()) + + reply.map(SocketMessage(message.joinRef, message.messageRef, message.topic, "phx_reply", _)) + val routes: Routes[Any, Response] = Routes.fromIterable( liveRoutes @@ -41,6 +67,7 @@ class LiveRouter(rootLayout: HtmlElement => HtmlElement, liveRoutes: List[LiveRo Method.GET / "live" / "websocket" -> handler(socketApp.toResponse) ) ) +end LiveRouter final case class SocketMessage( // Live session ID, auto increment defined by the client on join @@ -58,8 +85,9 @@ object SocketMessage: Chunk(joinRef, Json.Str(messageRef), Json.Str(topic), Json.Str(eventType), payload) ) => val payloadParsed = eventType match - case "phx_join" => payload.as[Payload.Join] - case s => Left(s"Unknown event type : $s") + case "heartbeat" => Right(Payload.Heartbeat) + case "phx_join" => payload.as[Payload.Join] + case s => Left(s"Unknown event type : $s") payloadParsed.map( SocketMessage( @@ -74,22 +102,41 @@ object SocketMessage: }, m => Json.Arr( - m.joinRef.map(Json.Num(_)).getOrElse(Json.Null), - Json.Num(m.messageRef), + m.joinRef.map(ref => Json.Str(ref.toString)).getOrElse(Json.Null), + Json.Str(m.messageRef.toString), Json.Str(m.topic), Json.Str(m.eventType), m.payload.match - case p: Payload.Join => p.toJsonAST.getOrElse(throw new IllegalArgumentException()) + case Payload.Heartbeat => Json.Obj.empty + case p: Payload.Join => p.toJsonAST.getOrElse(throw new IllegalArgumentException()) + case p: Payload.Reply => p.toJsonAST.getOrElse(throw new IllegalArgumentException()) ) ) enum Payload: + case Heartbeat case Join( url: String, // params: Map[String, String], session: String, static: Option[String], sticky: Boolean) + case Reply(status: String, response: LiveResponse) object Payload: - given JsonCodec[Payload.Join] = JsonCodec.derived + given JsonCodec[Payload.Join] = JsonCodec.derived + given JsonEncoder[Payload.Reply] = JsonEncoder.derived + + enum LiveResponse: + case Empty + case InitDiff(rendered: scalive.Diff) + object LiveResponse: + given JsonEncoder[LiveResponse] = + JsonEncoder[Json].contramap { + case Empty => Json.Obj.empty + case InitDiff(rendered) => + Json.Obj( + "liveview_version" -> Json.Str("1.1.8"), + "rendered" -> rendered.toJsonAST.getOrElse(throw new IllegalArgumentException()) + ) + } end SocketMessage