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