diff --git a/core/src/TestLiveView.scala b/core/src/TestLiveView.scala
index 2781ffc..6ada1bb 100644
--- a/core/src/TestLiveView.scala
+++ b/core/src/TestLiveView.scala
@@ -2,28 +2,23 @@ package scalive
package playground
import scalive.*
+import zio.*
-final case class MyModel(
- cls: String = "text-xs",
- bool: Boolean = true,
- elems: List[Elem] = List.empty)
-final case class Elem(name: String, age: Int)
+import TestView.*
+class TestView extends LiveView[Msg, Model]:
-class TestView(initialModel: MyModel) extends LiveView[TestView.Event]:
- import TestView.Event.*
+ def init = ZIO.succeed(Model())
- private val modelVar = Var[MyModel](initialModel)
+ def update(model: Model) =
+ case Msg.UpdateModel(f) => ZIO.succeed(f(model))
- def handleEvent =
- case UpdateModel(f) => modelVar.update(f)
-
- val el: HtmlElement =
+ def view(model: Dyn[Model]) =
div(
idAttr := "42",
- cls := modelVar(_.cls),
- disabled := modelVar(_.bool),
+ cls := model(_.cls),
+ disabled := model(_.bool),
ul(
- modelVar(_.elems).splitByIndex((_, elem) =>
+ model(_.elems).splitByIndex((_, elem) =>
li(
"Nom: ",
elem(_.name),
@@ -35,5 +30,12 @@ class TestView(initialModel: MyModel) extends LiveView[TestView.Event]:
)
object TestView:
- enum Event:
- case UpdateModel(f: MyModel => MyModel)
+
+ enum Msg:
+ case UpdateModel(f: Model => Model)
+
+ final case class Model(
+ cls: String = "text-xs",
+ bool: Boolean = true,
+ elems: List[Elem] = List.empty)
+ final case class Elem(name: String, age: Int)
diff --git a/core/src/main.scala b/core/src/main.scala
index 83a4e92..8df2e26 100644
--- a/core/src/main.scala
+++ b/core/src/main.scala
@@ -4,87 +4,78 @@ package playground
import scalive.*
import zio.json.*
-extension (lv: LiveView[?])
- def renderHtml: String =
- HtmlBuilder.build(lv.el)
+extension (el: HtmlElement) def html: String = HtmlBuilder.build(el)
+import TestView.*
@main
def main =
- val initModel = MyModel(elems =
+ val initModel = Model(elems =
List(
Elem("a", 10),
Elem("b", 15),
Elem("c", 30)
)
)
- val lv = TestView(initModel)
+ val modelVar = Var(initModel)
+ val lv = TestView()
+ val el = lv.view(modelVar)
println("Init")
- println(lv.renderHtml)
- println(lv.diff().toJsonPretty)
+ println(el.html)
+ println(el.diff().toJsonPretty)
println("Edit class attribue")
- lv.handleEvent(
- TestView.Event.UpdateModel(_.copy(cls = "text-lg"))
- )
- println(lv.diff().toJsonPretty)
+ modelVar.update(_.copy(cls = "text-lg"))
+ println(el.diff().toJsonPretty)
println("Edit first and last")
- lv.handleEvent(
- TestView.Event.UpdateModel(
- _.copy(elems =
- List(
- Elem("x", 10),
- Elem("b", 15),
- Elem("c", 99)
- )
+ modelVar.update(
+ _.copy(elems =
+ List(
+ Elem("x", 10),
+ Elem("b", 15),
+ Elem("c", 99)
)
)
)
- println(lv.diff().toJsonPretty)
- println(lv.diff().toJsonPretty)
+ println(el.diff().toJsonPretty)
+ println(el.diff().toJsonPretty)
println("Add one")
- lv.handleEvent(
- TestView.Event.UpdateModel(
- _.copy(elems =
- List(
- Elem("x", 10),
- Elem("b", 15),
- Elem("c", 99),
- Elem("d", 35)
- )
+ modelVar.update(
+ _.copy(elems =
+ List(
+ Elem("x", 10),
+ Elem("b", 15),
+ Elem("c", 99),
+ Elem("d", 35)
)
)
)
- println(lv.diff().toJsonPretty)
- println(lv.renderHtml)
+ println(el.diff().toJsonPretty)
+ println(el.html)
println("Remove first")
- lv.handleEvent(
- TestView.Event.UpdateModel(
- _.copy(elems =
- List(
- Elem("b", 15),
- Elem("c", 99),
- Elem("d", 35)
- )
+ modelVar.update(
+ _.copy(elems =
+ List(
+ Elem("b", 15),
+ Elem("c", 99),
+ Elem("d", 35)
)
)
)
- println(lv.diff().toJsonPretty)
- println(lv.renderHtml)
+ println(el.diff().toJsonPretty)
+ println(el.html)
println("Remove all")
- lv.handleEvent(
- TestView.Event.UpdateModel(
- _.copy(
- cls = "text-lg",
- bool = false,
- elems = List.empty
- )
+ modelVar.update(
+ _.copy(
+ cls = "text-lg",
+ bool = false,
+ elems = List.empty
)
)
- println(lv.diff().toJsonPretty)
- println(lv.diff().toJsonPretty)
- println(lv.renderHtml)
+ println(el.diff().toJsonPretty)
+ println(el.diff().toJsonPretty)
+ println(el.html)
end main
diff --git a/core/src/scalive/Diff.scala b/core/src/scalive/Diff.scala
index f6819d9..86071e3 100644
--- a/core/src/scalive/Diff.scala
+++ b/core/src/scalive/Diff.scala
@@ -46,5 +46,5 @@ object Diff:
case Diff.Value(value) => Json.Str(value)
case Diff.Dynamic(index, diff) =>
Json.Obj(index.toString -> toJson(diff))
- case Diff.Deleted => Json.Bool(false)
+ case Diff.Deleted => Json.Str("")
end Diff
diff --git a/core/src/scalive/Dyn.scala b/core/src/scalive/Dyn.scala
index 32e2e4d..cfc0884 100644
--- a/core/src/scalive/Dyn.scala
+++ b/core/src/scalive/Dyn.scala
@@ -46,7 +46,7 @@ extension [T](parent: Dyn[List[T]])
)
)
-class Var[T] private (initial: T) extends Dyn[T]:
+private class Var[T] private (initial: T) extends Dyn[T]:
private[scalive] var currentValue: T = initial
private[scalive] var changed: Boolean = true
def set(value: T): Unit =
@@ -61,10 +61,10 @@ class Var[T] private (initial: T) extends Dyn[T]:
private[scalive] def setUnchanged(): Unit = changed = false
private[scalive] inline def sync(): Unit = ()
private[scalive] def callOnEveryChild(f: T => Unit): Unit = f(currentValue)
-object Var:
+private object Var:
def apply[T](initial: T): Var[T] = new Var(initial)
-class DerivedVar[I, O] private[scalive] (parent: Var[I], f: I => O) extends Dyn[O]:
+private class DerivedVar[I, O] private[scalive] (parent: Var[I], f: I => O) extends Dyn[O]:
private[scalive] var currentValue: O = f(parent.currentValue)
private[scalive] var changed: Boolean = true
@@ -88,7 +88,7 @@ class DerivedVar[I, O] private[scalive] (parent: Var[I], f: I => O) extends Dyn[
private[scalive] def callOnEveryChild(f: O => Unit): Unit = f(currentValue)
-class SplitVar[I, O, Key](
+private class SplitVar[I, O, Key](
parent: Dyn[List[I]],
key: I => Key,
project: (Key, Dyn[I]) => O):
diff --git a/core/src/scalive/HtmlElement.scala b/core/src/scalive/HtmlElement.scala
index d63e423..b14a243 100644
--- a/core/src/scalive/HtmlElement.scala
+++ b/core/src/scalive/HtmlElement.scala
@@ -22,12 +22,18 @@ class HtmlElement(val tag: HtmlTag, val mods: Vector[Mod]):
mods.collect { case mod: (Mod.Attr & DynamicMod) => mod }
def dynamicContentMods: Seq[Mod.Content & DynamicMod] =
mods.collect { case mod: (Mod.Content & DynamicMod) => mod }
- private[scalive] def syncAll(): Unit = mods.foreach(_.syncAll())
- private[scalive] def setAllUnchanged(): Unit = dynamicMods.foreach(_.setAllUnchanged())
def prepended(mod: Mod*): HtmlElement = HtmlElement(tag, mods.prependedAll(mod))
def apended(mod: Mod*): HtmlElement = HtmlElement(tag, mods.appendedAll(mod))
+ private[scalive] def syncAll(): Unit = mods.foreach(_.syncAll())
+ private[scalive] def setAllUnchanged(): Unit = dynamicMods.foreach(_.setAllUnchanged())
+ private[scalive] def diff(trackUpdates: Boolean = true): Diff =
+ syncAll()
+ val diff = DiffBuilder.build(this, trackUpdates = trackUpdates)
+ setAllUnchanged()
+ diff
+
class HtmlTag(val name: String, val void: Boolean = false):
def apply(mods: Mod*): HtmlElement = HtmlElement(this, mods.toVector)
diff --git a/core/src/scalive/LiveView.scala b/core/src/scalive/LiveView.scala
index 12e7dd6..360e4fe 100644
--- a/core/src/scalive/LiveView.scala
+++ b/core/src/scalive/LiveView.scala
@@ -1,11 +1,9 @@
package scalive
-trait LiveView[Event]:
- def handleEvent: Event => Unit
- val el: HtmlElement
+import zio.*
- private[scalive] def diff(trackUpdates: Boolean = true): Diff =
- el.syncAll()
- val diff = DiffBuilder.build(el, trackUpdates = trackUpdates)
- el.setAllUnchanged()
- diff
+trait LiveView[Msg, Model]:
+ def init: Task[Model]
+ def update(model: Model): Msg => Task[Model]
+ def view(model: Dyn[Model]): HtmlElement
+ // def subscriptions(model: Model): ZStream[Any, Nothing, Msg]
diff --git a/core/test/src/scalive/LiveViewSpec.scala b/core/test/src/scalive/LiveViewSpec.scala
index ac516e9..d3c6eab 100644
--- a/core/test/src/scalive/LiveViewSpec.scala
+++ b/core/test/src/scalive/LiveViewSpec.scala
@@ -14,7 +14,6 @@ object LiveViewSpec extends TestSuite:
cls: String = "text-sm",
items: List[NestedModel] = List.empty)
final case class NestedModel(name: String, age: Int)
- final case class UpdateEvent(f: TestModel => TestModel)
def assertEqualsDiff(el: HtmlElement, expected: Json, trackChanges: Boolean = true) =
el.syncAll()
@@ -26,15 +25,12 @@ object LiveViewSpec extends TestSuite:
val tests = Tests {
test("Static only") {
- val lv =
- new LiveView[Nothing]:
- val el = div("Static string")
- def handleEvent = _ => ()
- lv.el.syncAll()
+ val el = div("Static string")
+ el.syncAll()
test("init") {
assertEqualsDiff(
- lv.el,
+ el,
Json.Obj(
"s" -> Json.Arr(Json.Str("
Static string
"))
),
@@ -42,27 +38,24 @@ object LiveViewSpec extends TestSuite:
)
}
test("diff") {
- assertEqualsDiff(lv.el, emptyDiff)
+ assertEqualsDiff(el, emptyDiff)
}
}
test("Dynamic string") {
- val lv =
- new LiveView[UpdateEvent]:
- val model = Var(TestModel())
- val el =
- div(
- h1(model(_.title)),
- p(model(_.otherString))
- )
- def handleEvent = evt => model.update(evt.f)
+ val model = Var(TestModel())
+ val el =
+ div(
+ h1(model(_.title)),
+ p(model(_.otherString))
+ )
- lv.el.syncAll()
- lv.el.setAllUnchanged()
+ el.syncAll()
+ el.setAllUnchanged()
test("init") {
assertEqualsDiff(
- lv.el,
+ el,
Json
.Obj(
"s" -> Json.Arr(Json.Str(""), Json.Str("
"), Json.Str("
")),
@@ -73,24 +66,24 @@ object LiveViewSpec extends TestSuite:
)
}
test("diff no update") {
- assertEqualsDiff(lv.el, emptyDiff)
+ assertEqualsDiff(el, emptyDiff)
}
test("diff with update") {
- lv.handleEvent(UpdateEvent(_.copy(title = "title updated")))
+ model.update(_.copy(title = "title updated"))
assertEqualsDiff(
- lv.el,
+ el,
Json.Obj("0" -> Json.Str("title updated"))
)
}
test("diff with update and no change") {
- lv.handleEvent(UpdateEvent(_.copy(title = "title value")))
- assertEqualsDiff(lv.el, emptyDiff)
+ model.update(_.copy(title = "title value"))
+ assertEqualsDiff(el, emptyDiff)
}
test("diff with update in multiple commands") {
- lv.handleEvent(UpdateEvent(_.copy(title = "title updated")))
- lv.handleEvent(UpdateEvent(_.copy(otherString = "other string updated")))
+ model.update(_.copy(title = "title updated"))
+ model.update(_.copy(otherString = "other string updated"))
assertEqualsDiff(
- lv.el,
+ el,
Json
.Obj(
"0" -> Json.Str("title updated"),
@@ -101,19 +94,16 @@ object LiveViewSpec extends TestSuite:
}
test("Dynamic attribute") {
- val lv =
- new LiveView[UpdateEvent]:
- val model = Var(TestModel())
- val el =
- div(cls := model(_.cls))
- def handleEvent = evt => model.update(evt.f)
+ val model = Var(TestModel())
+ val el =
+ div(cls := model(_.cls))
- lv.el.syncAll()
- lv.el.setAllUnchanged()
+ el.syncAll()
+ el.setAllUnchanged()
test("init") {
assertEqualsDiff(
- lv.el,
+ el,
Json
.Obj(
"s" -> Json
@@ -124,35 +114,32 @@ object LiveViewSpec extends TestSuite:
)
}
test("diff no update") {
- assertEqualsDiff(lv.el, emptyDiff)
+ assertEqualsDiff(el, emptyDiff)
}
test("diff with update") {
- lv.handleEvent(UpdateEvent(_.copy(cls = "text-md")))
+ model.update(_.copy(cls = "text-md"))
assertEqualsDiff(
- lv.el,
+ el,
Json.Obj("0" -> Json.Str("text-md"))
)
}
}
test("when mod") {
- val lv =
- new LiveView[UpdateEvent]:
- val model = Var(TestModel())
- val el =
- div(
- model.when(_.bool)(
- div("static string", model(_.nestedTitle))
- )
- )
- def handleEvent = evt => model.update(evt.f)
+ val model = Var(TestModel())
+ val el =
+ div(
+ model.when(_.bool)(
+ div("static string", model(_.nestedTitle))
+ )
+ )
- lv.el.syncAll()
- lv.el.setAllUnchanged()
+ el.syncAll()
+ el.setAllUnchanged()
test("init") {
assertEqualsDiff(
- lv.el,
+ el,
Json
.Obj(
"s" -> Json.Arr(Json.Str(""), Json.Str("
")),
@@ -162,16 +149,16 @@ object LiveViewSpec extends TestSuite:
)
}
test("diff no update") {
- assertEqualsDiff(lv.el, emptyDiff)
+ assertEqualsDiff(el, emptyDiff)
}
test("diff with unrelated update") {
- lv.handleEvent(UpdateEvent(_.copy(title = "title updated")))
- assertEqualsDiff(lv.el, emptyDiff)
+ model.update(_.copy(title = "title updated"))
+ assertEqualsDiff(el, emptyDiff)
}
test("diff when true and nested update") {
- lv.handleEvent(UpdateEvent(_.copy(bool = true)))
+ model.update(_.copy(bool = true))
assertEqualsDiff(
- lv.el,
+ el,
Json.Obj(
"0" ->
Json
@@ -184,12 +171,12 @@ object LiveViewSpec extends TestSuite:
)
}
test("diff when nested change") {
- lv.handleEvent(UpdateEvent(_.copy(bool = true)))
- lv.el.syncAll()
- lv.el.setAllUnchanged()
- lv.handleEvent(UpdateEvent(_.copy(bool = true, nestedTitle = "nested title updated")))
+ model.update(_.copy(bool = true))
+ el.syncAll()
+ el.setAllUnchanged()
+ model.update(_.copy(bool = true, nestedTitle = "nested title updated"))
assertEqualsDiff(
- lv.el,
+ el,
Json.Obj(
"0" ->
Json
@@ -209,30 +196,27 @@ object LiveViewSpec extends TestSuite:
NestedModel("c", 20)
)
)
- val lv =
- new LiveView[UpdateEvent]:
- val model = Var(initModel)
- val el =
- div(
- ul(
- model(_.items).splitByIndex((_, elem) =>
- li(
- "Nom: ",
- elem(_.name),
- " Age: ",
- elem(_.age.toString)
- )
- )
+ val model = Var(initModel)
+ val el =
+ div(
+ ul(
+ model(_.items).splitByIndex((_, elem) =>
+ li(
+ "Nom: ",
+ elem(_.name),
+ " Age: ",
+ elem(_.age.toString)
)
)
- def handleEvent = evt => model.update(evt.f)
+ )
+ )
- lv.el.syncAll()
- lv.el.setAllUnchanged()
+ el.syncAll()
+ el.setAllUnchanged()
test("init") {
assertEqualsDiff(
- lv.el,
+ el,
Json
.Obj(
"s" -> Json.Arr(Json.Str("")),
@@ -263,18 +247,16 @@ object LiveViewSpec extends TestSuite:
)
}
test("diff no update") {
- assertEqualsDiff(lv.el, emptyDiff)
+ assertEqualsDiff(el, emptyDiff)
}
test("diff with unrelated update") {
- lv.handleEvent(UpdateEvent(_.copy(title = "title updated")))
- assertEqualsDiff(lv.el, emptyDiff)
+ model.update(_.copy(title = "title updated"))
+ assertEqualsDiff(el, emptyDiff)
}
test("diff with item changed") {
- lv.handleEvent(
- UpdateEvent(_.copy(items = initModel.items.updated(2, NestedModel("c", 99))))
- )
+ model.update(_.copy(items = initModel.items.updated(2, NestedModel("c", 99))))
assertEqualsDiff(
- lv.el,
+ el,
Json.Obj(
"0" ->
Json
@@ -290,13 +272,9 @@ object LiveViewSpec extends TestSuite:
)
}
test("diff with item added") {
- lv.handleEvent(
- UpdateEvent(
- _.copy(items = initModel.items.appended(NestedModel("d", 35)))
- )
- )
+ model.update(_.copy(items = initModel.items.appended(NestedModel("d", 35))))
assertEqualsDiff(
- lv.el,
+ el,
Json.Obj(
"0" ->
Json
@@ -313,13 +291,9 @@ object LiveViewSpec extends TestSuite:
)
}
test("diff with first item removed") {
- lv.handleEvent(
- UpdateEvent(
- _.copy(items = initModel.items.tail)
- )
- )
+ model.update(_.copy(items = initModel.items.tail))
assertEqualsDiff(
- lv.el,
+ el,
Json.Obj(
"0" ->
Json
@@ -340,9 +314,9 @@ object LiveViewSpec extends TestSuite:
)
}
test("diff all removed") {
- lv.handleEvent(UpdateEvent(_.copy(items = List.empty)))
+ model.update(_.copy(items = List.empty))
assertEqualsDiff(
- lv.el,
+ el,
Json.Obj(
"0" ->
Json
diff --git a/example/src/ExampleLiveView.scala b/example/src/ExampleLiveView.scala
index 60b77d6..dadf79e 100644
--- a/example/src/ExampleLiveView.scala
+++ b/example/src/ExampleLiveView.scala
@@ -1,16 +1,16 @@
-import ExampleLiveView.Event
import monocle.syntax.all.*
import scalive.*
+import zio.*
import zio.json.*
-final case class ExampleModel(elems: List[NestedModel], cls: String = "text-xs")
-final case class NestedModel(name: String, age: Int)
+import ExampleLiveView.*
+class ExampleLiveView(someParam: String) extends LiveView[Msg, Model]:
-class ExampleLiveView(someParam: String) extends LiveView[Event]:
-
- val model = Var(
- ExampleModel(
- List(
+ def init = ZIO.succeed(
+ Model(
+ isVisible = true,
+ counter = 0,
+ elems = List(
NestedModel("a", 10),
NestedModel("b", 15),
NestedModel("c", 20)
@@ -18,11 +18,17 @@ class ExampleLiveView(someParam: String) extends LiveView[Event]:
)
)
- def handleEvent =
- case Event.Event(value) =>
- model.update(_.focus(_.elems.index(2).age).modify(_ + value))
+ def update(model: Model) =
+ case Msg.IncAge(value) =>
+ ZIO.succeed(model.focus(_.elems.index(2).age).modify(_ + value))
+ case Msg.ToggleCounter =>
+ ZIO.succeed(model.focus(_.isVisible).modify(!_))
+ case Msg.IncCounter =>
+ ZIO.succeed(model.focus(_.counter).modify(_ + 1))
+ case Msg.DecCounter =>
+ ZIO.succeed(model.focus(_.counter).modify(_ - 1))
- val el =
+ def view(model: Dyn[Model]) =
div(
h1(someParam),
h2(model(_.cls)),
@@ -39,12 +45,39 @@ class ExampleLiveView(someParam: String) extends LiveView[Event]:
)
),
button(
- phx.click := Event.Event(1),
+ phx.click := Msg.IncAge(1),
"Inc age"
+ ),
+ br(),
+ br(),
+ br(),
+ button(
+ phx.click := Msg.ToggleCounter,
+ model(_.isVisible match
+ case true => "Hide counter"
+ case false => "Show counter")
+ ),
+ model.when(_.isVisible)(
+ div(
+ button(phx.click := Msg.DecCounter, "-"),
+ div(model(_.counter.toString)),
+ button(phx.click := Msg.IncCounter, "+")
+ )
)
)
end ExampleLiveView
object ExampleLiveView:
- enum Event derives JsonCodec:
- case Event(value: Int)
+
+ enum Msg derives JsonCodec:
+ case IncAge(value: Int)
+ case ToggleCounter
+ case IncCounter
+ case DecCounter
+
+ final case class Model(
+ isVisible: Boolean,
+ counter: Int,
+ elems: List[NestedModel],
+ cls: String = "text-xs")
+ final case class NestedModel(name: String, age: Int)
diff --git a/zio/src/scalive/LiveRouter.scala b/zio/src/scalive/LiveRouter.scala
index b8285fd..7a528b4 100644
--- a/zio/src/scalive/LiveRouter.scala
+++ b/zio/src/scalive/LiveRouter.scala
@@ -15,19 +15,22 @@ import zio.stream.ZStream
import java.util.Base64
import scala.util.Random
-final case class LiveRoute[A, Event: JsonCodec](
+final case class LiveRoute[A, Msg: JsonCodec, Model](
path: PathCodec[A],
- liveviewBuilder: (A, Request) => LiveView[Event]):
- val eventCodec = JsonCodec[Event]
+ liveviewBuilder: (A, Request) => LiveView[Msg, Model]):
+ val messageCodec = JsonCodec[Msg]
- def toZioRoute(rootLayout: HtmlElement => HtmlElement): Route[Any, Nothing] =
+ def toZioRoute(rootLayout: HtmlElement => HtmlElement): Route[Any, Throwable] =
Method.GET / path -> handler { (params: A, req: Request) =>
val lv = liveviewBuilder(params, req)
val id: String =
s"phx-${Base64.getUrlEncoder().withoutPadding().encodeToString(Random().nextBytes(12))}"
val token = Token.sign("secret", id, "")
- lv.el.syncAll()
- Response.html(
+ for
+ initModel <- lv.init
+ el = lv.view(Var(initModel))
+ _ = el.syncAll()
+ yield Response.html(
Html.raw(
HtmlBuilder.build(
rootLayout(
@@ -35,15 +38,16 @@ final case class LiveRoute[A, Event: JsonCodec](
idAttr := id,
phx.main := true,
phx.session := token,
- lv.el
+ el
)
)
)
)
)
}
+end LiveRoute
-class LiveChannel(private val sockets: SubscriptionRef[Map[String, Socket[?]]]):
+class LiveChannel(private val sockets: SubscriptionRef[Map[String, Socket[?, ?]]]):
def diffsStream: ZStream[Any, Nothing, (LiveResponse, Meta)] =
sockets.changes
.map(m =>
@@ -51,12 +55,12 @@ class LiveChannel(private val sockets: SubscriptionRef[Map[String, Socket[?]]]):
.mergeAllUnbounded()(m.values.map(_.outbox).toList*)
).flatMapParSwitch(1, 1)(identity)
- def join[Event: JsonCodec](
+ def join[Msg: JsonCodec, Model](
id: String,
token: String,
- lv: LiveView[Event],
+ lv: LiveView[Msg, Model],
meta: WebSocketMessage.Meta
- ): URIO[Scope, Unit] =
+ ): RIO[Scope, Unit] =
sockets.updateZIO { m =>
m.get(id) match
case Some(socket) =>
@@ -78,7 +82,7 @@ class LiveChannel(private val sockets: SubscriptionRef[Map[String, Socket[?]]]):
socket.inbox
.offer(
value
- .fromJson(using socket.clientEventCodec.decoder)
+ .fromJson(using socket.messageCodec.decoder)
.getOrElse(throw new IllegalArgumentException())
-> meta
).unit
@@ -91,7 +95,7 @@ object LiveChannel:
def make(): UIO[LiveChannel] =
SubscriptionRef.make(Map.empty).map(new LiveChannel(_))
-class LiveRouter(rootLayout: HtmlElement => HtmlElement, liveRoutes: List[LiveRoute[?, ?]]):
+class LiveRouter(rootLayout: HtmlElement => HtmlElement, liveRoutes: List[LiveRoute[?, ?, ?]]):
private val socketApp: WebSocketApp[Any] =
Handler.webSocket { channel =>
@@ -155,7 +159,7 @@ class LiveRouter(rootLayout: HtmlElement => HtmlElement, liveRoutes: List[LiveRo
val pathParams = route.path.decode(req.path).getOrElse(???)
val lv = route.liveviewBuilder(pathParams, req)
liveChannel
- .join(message.topic, session, lv, message.meta)(using route.eventCodec)
+ .join(message.topic, session, lv, message.meta)(using route.messageCodec)
.map(_ => None)
}.getOrElse(ZIO.succeed(None))
@@ -168,12 +172,15 @@ class LiveRouter(rootLayout: HtmlElement => HtmlElement, liveRoutes: List[LiveRo
end match
end handleMessage
- val routes: Routes[Any, Response] =
- Routes.fromIterable(
- liveRoutes
- .map(route => route.toZioRoute(rootLayout))
- .prepended(
- Method.GET / "live" / "websocket" -> handler(socketApp.toResponse)
- )
- )
+ val routes: Routes[Any, Nothing] =
+ Routes
+ .fromIterable(
+ liveRoutes
+ .map(route => route.toZioRoute(rootLayout))
+ .prepended(
+ Method.GET / "live" / "websocket" -> handler(socketApp.toResponse)
+ )
+ ).handleErrorZIO(e =>
+ ZIO.logErrorCause(Cause.die(e)).as(Response(status = Status.InternalServerError))
+ )
end LiveRouter
diff --git a/zio/src/scalive/Socket.scala b/zio/src/scalive/Socket.scala
index 876d52b..1d0ca3d 100644
--- a/zio/src/scalive/Socket.scala
+++ b/zio/src/scalive/Socket.scala
@@ -6,34 +6,38 @@ import zio.Queue
import zio.json.*
import zio.stream.ZStream
-final case class Socket[Event: JsonCodec] private (
+final case class Socket[Msg: JsonCodec, Model] private (
id: String,
token: String,
- inbox: Queue[(Event, WebSocketMessage.Meta)],
+ inbox: Queue[(Msg, WebSocketMessage.Meta)],
outbox: ZStream[Any, Nothing, (LiveResponse, WebSocketMessage.Meta)],
- fiber: Fiber.Runtime[Nothing, Unit],
+ fiber: Fiber.Runtime[Throwable, Unit],
shutdown: UIO[Unit]):
- val clientEventCodec = JsonCodec[Event]
+ val messageCodec = JsonCodec[Msg]
object Socket:
- def start[Event: JsonCodec](
+ def start[Msg: JsonCodec, Model](
id: String,
token: String,
- lv: LiveView[Event],
+ lv: LiveView[Msg, Model],
meta: WebSocketMessage.Meta
- ): URIO[Scope, Socket[Event]] =
+ ): RIO[Scope, Socket[Msg, Model]] =
for
- inbox <- Queue.bounded[(Event, WebSocketMessage.Meta)](4)
- outHub <- Hub.bounded[(LiveResponse, WebSocketMessage.Meta)](4)
- initDiff = lv.diff(trackUpdates = false)
- lvRef <- Ref.make(lv)
+ inbox <- Queue.bounded[(Msg, WebSocketMessage.Meta)](4)
+ outHub <- Hub.bounded[(LiveResponse, WebSocketMessage.Meta)](4)
+ initModel <- lv.init
+ modelVar = Var(initModel)
+ el = lv.view(modelVar)
+ ref <- Ref.make((modelVar, el))
+ initDiff = el.diff(trackUpdates = false)
fiber <- ZStream
.fromQueue(inbox)
.mapZIO { (msg, meta) =>
for
- lv <- lvRef.get
- _ = lv.handleEvent(msg)
- diff = lv.diff()
+ (modelVar, el) <- ref.get
+ updatedModel <- lv.update(modelVar.currentValue)(msg)
+ _ = modelVar.set(updatedModel)
+ diff = el.diff()
_ <- outHub.publish(LiveResponse.Diff(diff) -> meta)
yield ()
}
@@ -42,4 +46,5 @@ object Socket:
stop = inbox.shutdown *> outHub.shutdown *> fiber.interrupt.unit
diffStream <- ZStream.fromHubScoped(outHub)
outbox = ZStream.succeed(LiveResponse.InitDiff(initDiff) -> meta) ++ diffStream
- yield Socket[Event](id, token, inbox, outbox, fiber, stop)
+ yield Socket[Msg, Model](id, token, inbox, outbox, fiber, stop)
+end Socket