mirror of
https://github.com/phfroidmont/scalive.git
synced 2025-12-25 05:26:59 +01:00
Support live navigation
This commit is contained in:
parent
0b067aa7e1
commit
d42472061b
5 changed files with 63 additions and 30 deletions
|
|
@ -8,6 +8,10 @@ package object scalive extends HtmlTags with HtmlAttrs with ComplexHtmlKeys:
|
||||||
|
|
||||||
lazy val defer = htmlAttr("defer", codecs.BooleanAsAttrPresenceCodec)
|
lazy val defer = htmlAttr("defer", codecs.BooleanAsAttrPresenceCodec)
|
||||||
|
|
||||||
|
object link:
|
||||||
|
def navigate(path: String, mods: Mod*): HtmlElement =
|
||||||
|
a(href := path, phx.link := "redirect", phx.linkState := "push", mods)
|
||||||
|
|
||||||
object phx:
|
object phx:
|
||||||
private def phxAttr(suffix: String): HtmlAttr[String] =
|
private def phxAttr(suffix: String): HtmlAttr[String] =
|
||||||
new HtmlAttr(s"phx-$suffix", StringAsIsCodec)
|
new HtmlAttr(s"phx-$suffix", StringAsIsCodec)
|
||||||
|
|
@ -18,6 +22,8 @@ package object scalive extends HtmlTags with HtmlAttrs with ComplexHtmlKeys:
|
||||||
|
|
||||||
private[scalive] lazy val session = dataPhxAttr("session")
|
private[scalive] lazy val session = dataPhxAttr("session")
|
||||||
private[scalive] lazy val main = htmlAttr("data-phx-main", BooleanAsAttrPresenceCodec)
|
private[scalive] lazy val main = htmlAttr("data-phx-main", BooleanAsAttrPresenceCodec)
|
||||||
|
private[scalive] lazy val link = dataPhxAttr("link")
|
||||||
|
private[scalive] lazy val linkState = dataPhxAttr("link-state")
|
||||||
lazy val click = phxAttrJson("click")
|
lazy val click = phxAttrJson("click")
|
||||||
def value(key: String) = phxAttr(s"value-$key")
|
def value(key: String) = phxAttr(s"value-$key")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,14 @@ final case class WebSocketMessage(
|
||||||
eventType: String,
|
eventType: String,
|
||||||
payload: WebSocketMessage.Payload):
|
payload: WebSocketMessage.Payload):
|
||||||
val meta = WebSocketMessage.Meta(joinRef, messageRef, topic, eventType)
|
val meta = WebSocketMessage.Meta(joinRef, messageRef, topic, eventType)
|
||||||
|
def okReply =
|
||||||
|
WebSocketMessage(
|
||||||
|
joinRef,
|
||||||
|
messageRef,
|
||||||
|
topic,
|
||||||
|
"phx_reply",
|
||||||
|
Payload.Reply("ok", LiveResponse.Empty)
|
||||||
|
)
|
||||||
object WebSocketMessage:
|
object WebSocketMessage:
|
||||||
|
|
||||||
final case class Meta(
|
final case class Meta(
|
||||||
|
|
@ -33,6 +41,8 @@ object WebSocketMessage:
|
||||||
val payloadParsed = eventType match
|
val payloadParsed = eventType match
|
||||||
case "heartbeat" => Right(Payload.Heartbeat)
|
case "heartbeat" => Right(Payload.Heartbeat)
|
||||||
case "phx_join" => payload.as[Payload.Join]
|
case "phx_join" => payload.as[Payload.Join]
|
||||||
|
case "phx_leave" => Right(Payload.Leave)
|
||||||
|
case "phx_close" => Right(Payload.Close)
|
||||||
case "event" => payload.as[Payload.Event]
|
case "event" => payload.as[Payload.Event]
|
||||||
case s => Left(s"Unknown event type : $s")
|
case s => Left(s"Unknown event type : $s")
|
||||||
|
|
||||||
|
|
@ -56,6 +66,8 @@ object WebSocketMessage:
|
||||||
m.payload match
|
m.payload match
|
||||||
case Payload.Heartbeat => Json.Obj.empty
|
case Payload.Heartbeat => Json.Obj.empty
|
||||||
case p: Payload.Join => p.toJsonAST.getOrElse(throw new IllegalArgumentException())
|
case p: Payload.Join => p.toJsonAST.getOrElse(throw new IllegalArgumentException())
|
||||||
|
case Payload.Leave => Json.Obj.empty
|
||||||
|
case Payload.Close => Json.Obj.empty
|
||||||
case p: Payload.Reply => p.toJsonAST.getOrElse(throw new IllegalArgumentException())
|
case p: Payload.Reply => p.toJsonAST.getOrElse(throw new IllegalArgumentException())
|
||||||
case p: Payload.Event => p.toJsonAST.getOrElse(throw new IllegalArgumentException())
|
case p: Payload.Event => p.toJsonAST.getOrElse(throw new IllegalArgumentException())
|
||||||
case p: Payload.Diff => p.toJsonAST.getOrElse(throw new IllegalArgumentException())
|
case p: Payload.Diff => p.toJsonAST.getOrElse(throw new IllegalArgumentException())
|
||||||
|
|
@ -65,11 +77,14 @@ object WebSocketMessage:
|
||||||
enum Payload:
|
enum Payload:
|
||||||
case Heartbeat
|
case Heartbeat
|
||||||
case Join(
|
case Join(
|
||||||
url: String,
|
url: Option[String],
|
||||||
|
redirect: Option[String],
|
||||||
// params: Map[String, String],
|
// params: Map[String, String],
|
||||||
session: String,
|
session: String,
|
||||||
static: Option[String],
|
static: Option[String],
|
||||||
sticky: Boolean)
|
sticky: Boolean)
|
||||||
|
case Leave
|
||||||
|
case Close
|
||||||
case Reply(status: String, response: LiveResponse)
|
case Reply(status: String, response: LiveResponse)
|
||||||
case Diff(diff: scalive.Diff)
|
case Diff(diff: scalive.Diff)
|
||||||
case Event(`type`: Payload.EventType, event: String, value: Map[String, String])
|
case Event(`type`: Payload.EventType, event: String, value: Map[String, String])
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ class HomeLiveView() extends LiveView[String, Unit]:
|
||||||
cls := "space-y-2",
|
cls := "space-y-2",
|
||||||
links.map((path, name) =>
|
links.map((path, name) =>
|
||||||
li(
|
li(
|
||||||
a(
|
link.navigate(
|
||||||
href := path,
|
path,
|
||||||
cls := "block px-4 py-2 rounded-lg text-gray-700 hover:bg-gray-100 hover:text-gray-900 font-medium transition",
|
cls := "block px-4 py-2 rounded-lg text-gray-700 hover:bg-gray-100 hover:text-gray-900 font-medium transition",
|
||||||
name
|
name
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package scalive
|
package scalive
|
||||||
|
|
||||||
import scalive.WebSocketMessage.LiveResponse
|
|
||||||
import scalive.WebSocketMessage.Meta
|
import scalive.WebSocketMessage.Meta
|
||||||
import scalive.WebSocketMessage.Payload
|
import scalive.WebSocketMessage.Payload
|
||||||
import zio.*
|
import zio.*
|
||||||
|
|
@ -75,6 +74,18 @@ class LiveChannel(private val sockets: SubscriptionRef[Map[String, Socket[?, ?]]
|
||||||
.map(m.updated(id, _))
|
.map(m.updated(id, _))
|
||||||
}.flatMap(_ => ZIO.logDebug(s"LiveView joined $id"))
|
}.flatMap(_ => ZIO.logDebug(s"LiveView joined $id"))
|
||||||
|
|
||||||
|
def leave(id: String): UIO[Unit] =
|
||||||
|
sockets.updateZIO { m =>
|
||||||
|
m.get(id) match
|
||||||
|
case Some(socket) =>
|
||||||
|
for
|
||||||
|
_ <- socket.shutdown
|
||||||
|
_ <- ZIO.logDebug(s"Left LiveView $id")
|
||||||
|
yield m.removed(id)
|
||||||
|
case None =>
|
||||||
|
ZIO.logWarning(s"Tried to leave LiveView $id which doesn't exist").as(m)
|
||||||
|
}
|
||||||
|
|
||||||
def event(id: String, value: String, meta: WebSocketMessage.Meta): UIO[Unit] =
|
def event(id: String, value: String, meta: WebSocketMessage.Meta): UIO[Unit] =
|
||||||
sockets.get.flatMap { m =>
|
sockets.get.flatMap { m =>
|
||||||
m.get(id) match
|
m.get(id) match
|
||||||
|
|
@ -109,13 +120,16 @@ class LiveRouter(rootLayout: HtmlElement => HtmlElement, liveRoutes: List[LiveRo
|
||||||
Read(
|
Read(
|
||||||
WebSocketFrame.text(
|
WebSocketFrame.text(
|
||||||
WebSocketMessage(
|
WebSocketMessage(
|
||||||
meta.joinRef,
|
joinRef = meta.joinRef,
|
||||||
meta.messageRef,
|
messageRef = payload match
|
||||||
meta.topic,
|
case Payload.Close => meta.joinRef
|
||||||
payload match
|
case _ => meta.messageRef,
|
||||||
|
topic = meta.topic,
|
||||||
|
eventType = payload match
|
||||||
case Payload.Diff(_) => "diff"
|
case Payload.Diff(_) => "diff"
|
||||||
|
case Payload.Close => "phx_close"
|
||||||
case _ => "phx_reply",
|
case _ => "phx_reply",
|
||||||
payload
|
payload = payload
|
||||||
).toJson
|
).toJson
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -146,21 +160,10 @@ class LiveRouter(rootLayout: HtmlElement => HtmlElement, liveRoutes: List[LiveRo
|
||||||
private def handleMessage(message: WebSocketMessage, liveChannel: LiveChannel)
|
private def handleMessage(message: WebSocketMessage, liveChannel: LiveChannel)
|
||||||
: RIO[Scope, Option[WebSocketMessage]] =
|
: RIO[Scope, Option[WebSocketMessage]] =
|
||||||
message.payload match
|
message.payload match
|
||||||
case Payload.Heartbeat =>
|
case Payload.Heartbeat => ZIO.succeed(Some(message.okReply))
|
||||||
ZIO.succeed(
|
case Payload.Join(url, redirect, session, static, sticky) =>
|
||||||
Some(
|
|
||||||
WebSocketMessage(
|
|
||||||
message.joinRef,
|
|
||||||
message.messageRef,
|
|
||||||
message.topic,
|
|
||||||
"phx_reply",
|
|
||||||
Payload.Reply("ok", LiveResponse.Empty)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
case Payload.Join(url, session, static, sticky) =>
|
|
||||||
ZIO
|
ZIO
|
||||||
.fromEither(URL.decode(url)).flatMap(url =>
|
.fromEither(URL.decode(url.orElse(redirect).getOrElse(???))).flatMap(url =>
|
||||||
val req = Request(url = url)
|
val req = Request(url = url)
|
||||||
liveRoutes.iterator
|
liveRoutes.iterator
|
||||||
.map(route =>
|
.map(route =>
|
||||||
|
|
@ -169,7 +172,7 @@ class LiveRouter(rootLayout: HtmlElement => HtmlElement, liveRoutes: List[LiveRo
|
||||||
.toOption
|
.toOption
|
||||||
.map(route.liveviewBuilder(_, req))
|
.map(route.liveviewBuilder(_, req))
|
||||||
.map(
|
.map(
|
||||||
ZIO.logDebug(s"Joining live view ${route.path.toString} ${message.topic}") *>
|
ZIO.logDebug(s"Joining LiveView ${route.path.toString} ${message.topic}") *>
|
||||||
liveChannel.join(message.topic, session, _, message.meta)(
|
liveChannel.join(message.topic, session, _, message.meta)(
|
||||||
using route.messageCodec
|
using route.messageCodec
|
||||||
)
|
)
|
||||||
|
|
@ -178,12 +181,17 @@ class LiveRouter(rootLayout: HtmlElement => HtmlElement, liveRoutes: List[LiveRo
|
||||||
.collectFirst { case Some(join) => join.map(_ => None) }
|
.collectFirst { case Some(join) => join.map(_ => None) }
|
||||||
.getOrElse(ZIO.succeed(None))
|
.getOrElse(ZIO.succeed(None))
|
||||||
)
|
)
|
||||||
|
case Payload.Leave =>
|
||||||
|
liveChannel
|
||||||
|
.leave(message.topic)
|
||||||
|
.as(Some(message.okReply))
|
||||||
case Payload.Event(_, event, _) =>
|
case Payload.Event(_, event, _) =>
|
||||||
liveChannel
|
liveChannel
|
||||||
.event(message.topic, event, message.meta)
|
.event(message.topic, event, message.meta)
|
||||||
.map(_ => None)
|
.map(_ => None)
|
||||||
case Payload.Reply(_, _) => ZIO.die(new IllegalArgumentException())
|
case Payload.Reply(_, _) => ZIO.die(new IllegalArgumentException())
|
||||||
case Payload.Diff(_) => ZIO.die(new IllegalArgumentException())
|
case Payload.Diff(_) => ZIO.die(new IllegalArgumentException())
|
||||||
|
case Payload.Close => ZIO.die(new IllegalArgumentException())
|
||||||
end match
|
end match
|
||||||
end handleMessage
|
end handleMessage
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,11 @@ object Socket:
|
||||||
yield ()
|
yield ()
|
||||||
}.fork
|
}.fork
|
||||||
stop =
|
stop =
|
||||||
inbox.shutdown *> outHub.shutdown *> clientFiber.interrupt.unit *> serverFiber.interrupt.unit
|
outHub.publish(Payload.Close -> meta) *>
|
||||||
|
inbox.shutdown *>
|
||||||
|
outHub.shutdown *>
|
||||||
|
clientFiber.interrupt.unit *>
|
||||||
|
serverFiber.interrupt.unit
|
||||||
outbox =
|
outbox =
|
||||||
ZStream.succeed(
|
ZStream.succeed(
|
||||||
Payload.okReply(LiveResponse.InitDiff(initDiff)) -> meta
|
Payload.okReply(LiveResponse.InitDiff(initDiff)) -> meta
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue