diff --git a/build.sbt b/build.sbt index 2eaaceb..85d9b5b 100644 --- a/build.sbt +++ b/build.sbt @@ -23,16 +23,16 @@ libraryDependencies ++= Seq( "com.softwaremill.sttp.client" %% "core" % "2.2.9", "com.softwaremill.sttp.client" %% "monix" % "2.2.9", "com.softwaremill.sttp.client" %% "circe" % "2.2.9", - "com.softwaremill.sttp.client" %% "async-http-client-backend-monix" % "2.2.9", "com.softwaremill.sttp.client" %% "httpclient-backend-monix" % "2.2.9", "com.softwaremill.quicklens" %% "quicklens" % "1.6.1", - "com.github.valskalla" %% "odin-monix" % "0.8.1", "com.typesafe.akka" %% "akka-actor-typed" % "2.6.8", "com.softwaremill.macwire" %% "util" % "2.3.7", "com.softwaremill.macwire" %% "macros" % "2.3.6" % "provided", "com.softwaremill.macwire" %% "macrosakka" % "2.3.6" % "provided", - "com.github.valskalla" %% "odin-slf4j" % "0.8.1", + "com.github.valskalla" %% "odin-monix" % "0.9.1", + "com.github.valskalla" %% "odin-slf4j" % "0.9.1", "com.github.valskalla" %% "odin-json" % "0.9.1", + "com.github.valskalla" %% "odin-extras" % "0.9.1", "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2", "com.jfoenix" % "jfoenix" % "9.0.10", "org.kordamp.ikonli" % "ikonli-core" % "12.0.0", diff --git a/src/main/scala/nova/monadic_sfx/Main.scala b/src/main/scala/nova/monadic_sfx/Main.scala index b3b50c8..3cfd307 100644 --- a/src/main/scala/nova/monadic_sfx/Main.scala +++ b/src/main/scala/nova/monadic_sfx/Main.scala @@ -1,16 +1,12 @@ package nova.monadic_sfx -import scala.concurrent.duration._ - import _root_.monix.bio.BIOApp import _root_.monix.bio.Task import _root_.monix.bio.UIO import cats.effect.ExitCode import cats.effect.Resource -import cats.implicits._ import com.softwaremill.macwire._ import io.odin._ -import io.odin.syntax._ import nova.monadic_sfx.executors._ // import nova.monadic_sfx.util.IOUtils._ // import sttp.client.httpclient.monix.HttpClientMonixBackend @@ -18,10 +14,7 @@ object Main extends MainModule with BIOApp { def appResource(startTime: Long) = for { - implicit0(logger: Logger[Task]) <- - consoleLogger().withAsync(timeWindow = 1.millis) |+| fileLogger( - "application.log" - ).withAsync() + implicit0(logger: Logger[Task]) <- makeLogger schedulers = new Schedulers() // backend <- Resource.make( // toIO(HttpClientMonixBackend()(schedulers.async)) diff --git a/src/main/scala/nova/monadic_sfx/MainApp.scala b/src/main/scala/nova/monadic_sfx/MainApp.scala index 4b03f8d..ef85122 100644 --- a/src/main/scala/nova/monadic_sfx/MainApp.scala +++ b/src/main/scala/nova/monadic_sfx/MainApp.scala @@ -7,6 +7,8 @@ import nova.monadic_sfx.executors.Schedulers import nova.monadic_sfx.implicits.JFXButton import nova.monadic_sfx.implicits.JavaFXMonixObservables._ import nova.monadic_sfx.ui.MyFxApp +import nova.monadic_sfx.ui.components.router.BrainNotWorking +import nova.monadic_sfx.ui.components.router.FXRouter import nova.monadic_sfx.ui.components.todo.TodoListStore import nova.monadic_sfx.ui.components.todo.TodoListView import org.gerweck.scalafx.util._ @@ -33,7 +35,7 @@ class MainApp( text = "Add" } - lazy val addTodoObs = addTodoButton.observableAction() + lazy val addTodoObs = addTodoButton.observableAction // lazy val todoListView = TodoListView.defaultListView @@ -46,14 +48,14 @@ class MainApp( fill = Color.DeepSkyBlue } children ++= Seq( - new JFXButton { - text = "DummyButton" - }, - new JFXButton { - text = "DummyButton2" - }, - addTodoButton, - Test.ttv + // new JFXButton { + // text = "DummyButton" + // }, + // new JFXButton { + // text = "DummyButton2" + // }, + // addTodoButton, + // Test.ttv // todoListView ) } @@ -62,7 +64,7 @@ class MainApp( private lazy val stage = new PrimaryStage { title = "Simple ScalaFX App" scene = _scene - width = 800 + width = 1000 height = 400 } @@ -85,6 +87,30 @@ class MainApp( // .startAndForget // ) _ <- createTodoComponent.executeOn(schedulers.fx) + router <- Task(BrainNotWorking.router) + routerStore <- router.store(BrainNotWorking.Page.Home, logger) + routerNode <- for { + node <- + Task + .deferAction(implicit s => + Task(new HBox { + children <-- router + .render(BrainNotWorking.resolver)(routerStore) + .map(_.delegate) + }) + ) + .executeOn(schedulers.fx) + _ <- Task.deferFuture( + routerStore.onNext(FXRouter.Replace(BrainNotWorking.Page.UserHome(1))) + ) + + } yield node + // _ <- + // BrainNotWorking + // .routerTask(logger) + // .flatMap(node => Task(_scene.getChildren += node)) + // .executeOn(schedulers.fx) + _ <- Task(_scene.getChildren += routerNode).executeOn(schedulers.fx) _ <- logger.info( s"Application started in ${(System.currentTimeMillis() - startTime) / 1000f} seconds" ) diff --git a/src/main/scala/nova/monadic_sfx/MainModule.scala b/src/main/scala/nova/monadic_sfx/MainModule.scala index 03c4c80..0a8c3da 100644 --- a/src/main/scala/nova/monadic_sfx/MainModule.scala +++ b/src/main/scala/nova/monadic_sfx/MainModule.scala @@ -1,7 +1,41 @@ package nova.monadic_sfx +import scala.concurrent.duration._ + +import _root_.monix.bio.Task +import cats.implicits._ +import io.odin._ +import io.odin.config._ +import io.odin.syntax._ import nova.monadic_sfx.actors.ActorModule import nova.monadic_sfx.http.HttpModule import nova.monadic_sfx.ui.UiModule +import nova.monadic_sfx.util.reactive.Middlewares + +trait MainModule extends ActorModule with UiModule with HttpModule { + def routerLogger(defaultLogger: Logger[Task], storeLogger: Logger[Task]) = + enclosureRouting[Task]( + "nova.monadic_sfx.util.reactive.Middlewares" -> storeLogger, + "nova.monadic_sfx.util.reactive.Store" -> storeLogger + ) + .withFallback(defaultLogger) + .withAsync() -trait MainModule extends ActorModule with UiModule with HttpModule + def makeLogger = + for { + defaultLogger <- consoleLogger[Task]() + .withAsync(timeWindow = 1.millis) |+| fileLogger[Task]( + "application.log" + ).withAsync() + middlewareLogger <- + consoleLogger[ + Task + ](formatter = Middlewares.format) + .withMinimalLevel(Level.Trace) + .withAsync() |+| fileLogger[Task]( + "stores.log", + formatter = Middlewares.format + ).withAsync() + routerLogger <- routerLogger(defaultLogger, middlewareLogger) + } yield (routerLogger) +} diff --git a/src/main/scala/nova/monadic_sfx/Types.scala b/src/main/scala/nova/monadic_sfx/Types.scala index 273b2e0..f6f5182 100644 --- a/src/main/scala/nova/monadic_sfx/Types.scala +++ b/src/main/scala/nova/monadic_sfx/Types.scala @@ -5,7 +5,7 @@ import java.nio.ByteBuffer import monix.eval.Task import monix.reactive.Observable import sttp.client.SttpBackend -import sttp.client.asynchttpclient.WebSocketHandler +import sttp.client.httpclient.WebSocketHandler trait AppTypes { type HttpBackend = diff --git a/src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala b/src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala index 02b8adc..4bca855 100644 --- a/src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala +++ b/src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala @@ -7,9 +7,11 @@ import javafx.event.EventHandler import javafx.scene.{input => jfxsi} import javafx.{event => jfxe} import monix.bio.Task +import monix.eval.Coeval import monix.execution.Ack import monix.execution.Cancelable import monix.execution.Scheduler +import monix.execution.cancelables.SingleAssignCancelable import monix.reactive.Observable import monix.reactive.Observer import monix.reactive.OverflowStrategy @@ -19,8 +21,8 @@ import org.gerweck.scalafx.util._ import scalafx.Includes._ import scalafx.beans.property.Property import scalafx.beans.property.ReadOnlyProperty -import scalafx.beans.value.ObservableValue import scalafx.collections.ObservableBuffer +import scalafx.event.subscriptions.Subscription import scalafx.scene.Scene import scalafx.scene.control.ButtonBase import scalafx.scene.control.MenuItem @@ -84,17 +86,20 @@ object JavaFXMonixObservables { } def <--(obs: Observable[T])(implicit s: Scheduler) = { - obs.doOnNext(v => me.Task(prop.value = v)).subscribe() + obs.doOnNextF(v => Coeval(prop.value = v)).subscribe() } def asOption = prop.map(Option(_)) - def observableChange[J1 >: J](): Observable[J1] = { + def observableChange[J1 >: J]: Observable[J1] = { import monix.execution.cancelables.SingleAssignCancelable Observable.create(OverflowStrategy.Unbounded) { sub => val c = SingleAssignCancelable() - val canc = prop.onChange((a, b, c) => sub.onNext(c)) + val canc = + prop.onChange((a, b, c1) => + if (c1 != null && sub.onNext(c1) == Ack.Stop) c.cancel() + ) c := Cancelable(() => canc.cancel()) c @@ -113,21 +118,75 @@ object JavaFXMonixObservables { } def <--(obs: Observable[A])(implicit s: Scheduler) = { - obs.doOnNext(v => me.Task(prop() = v)).subscribe() + obs.doOnNextF(v => Coeval(prop() = v)).subscribe() + } + + def observableChange[J1 >: A]: Observable[J1] = { + import monix.execution.cancelables.SingleAssignCancelable + Observable.create(OverflowStrategy.Unbounded) { sub => + val c = SingleAssignCancelable() + + val canc = prop.onChange((_, _, c1) => + if (c1 != null && sub.onNext(c1) == Ack.Stop) c.cancel() + ) + + c := Cancelable(() => canc.cancel()) + c + } + } + } + + implicit final class ObservableListExt[A]( + private val buffer: ObservableList[A] + ) extends AnyVal { + + // def -->(sub: Observer[A]) = + // buffer.onChange((a, b, c) => if (c != null) sub.onNext(c)) + + // def -->(op: Property[A, A]) = { + // buffer.onChange((a, b, c) => if (c != null) op() = c) + // } + + def <--(obs: Observable[A])(implicit s: Scheduler) = { + obs + .doOnNextF(v => + for { + _ <- Coeval(buffer.clear()) + _ <- Coeval(buffer += v) + } yield () + ) + .subscribe() } - def observableChange[J1 >: A]() - : Observable[(ObservableValue[A, A], J1, J1)] = { + def observableChange[J1 >: A]: Observable[J1] = { import monix.execution.cancelables.SingleAssignCancelable Observable.create(OverflowStrategy.Unbounded) { sub => val c = SingleAssignCancelable() - val canc = prop.onChange((a, b, c) => sub.onNext((a, b, c))) + implicit val s = sub.scheduler + + val canc = + buffer.onChange((buf, _) => + loop(sub, buf.toIterable.iterator, c).runToFuture + ) c := Cancelable(() => canc.cancel()) c } } + + private def loop( + sub: Observer[A], + it: Iterator[A], + c: Cancelable + ): Task[Unit] = + if (it.hasNext) { + val next = it.next() + Task.deferFuture(sub.onNext(next)).flatMap { + case Ack.Continue => loop(sub, it, c) + case Ack.Stop => Task(c.cancel()) + } + } else Task.unit } implicit final class BindObs3[T, J](private val prop: ReadOnlyProperty[T, J]) @@ -140,12 +199,14 @@ object JavaFXMonixObservables { op <== prop } - def observableChange[J1 >: J](): Observable[J1] = { + def observableChange[J1 >: J]: Observable[J1] = { import monix.execution.cancelables.SingleAssignCancelable Observable.create(OverflowStrategy.Unbounded) { sub => val c = SingleAssignCancelable() - val canc = prop.onChange((a, b, c) => sub.onNext(c)) + val canc = prop.onChange((a, b, c1) => + if (c1 != null && sub.onNext(c1) == Ack.Stop) c.cancel() + ) c := Cancelable(() => canc.cancel()) c @@ -160,15 +221,18 @@ object JavaFXMonixObservables { obs.doOnNext(v => me.Task(prop() = ObservableBuffer.from(v))).subscribe() } - def -->(sub: Observer[A])(implicit s: Scheduler) = - prop.onChange((a, b, c) => - if (c != null) + def -->(sub: Observer[A])(implicit s: Scheduler) = { + val c = SingleAssignCancelable() + val subs: Subscription = prop.onChange((a, b, c1) => + if (c1 != null) Iterant[Task] - .fromIterable(c.toIterable) + .fromIterable(c1.toIterable) .consume - .use(consume(sub, _)) + .use(consume(sub, c, _)) .runToFuture ) + c := Cancelable(() => subs.cancel()) + } private def loop(sub: Observer[A], it: Iterator[A]): Task[Unit] = if (it.hasNext) { @@ -181,14 +245,15 @@ object JavaFXMonixObservables { private def consume( sub: Observer[A], + c: Cancelable, consumer: Iterant.Consumer[Task, A] ): Task[Unit] = consumer.pull.flatMap { case Left(value) => Task.unit case Right(value) => Task.deferFuture(sub.onNext(value)).flatMap { - case Ack.Continue => consume(sub, consumer) - case Ack.Stop => Task.unit + case Ack.Continue => consume(sub, c, consumer) + case Ack.Stop => Task(c.cancel()) } } @@ -212,7 +277,7 @@ object JavaFXMonixObservables { private val button: ButtonBase ) extends AnyVal { - def observableAction(): Observable[jfxe.ActionEvent] = { + def observableAction: Observable[jfxe.ActionEvent] = { import monix.execution.cancelables.SingleAssignCancelable Observable.create(OverflowStrategy.Unbounded) { sub => val c = SingleAssignCancelable() @@ -238,7 +303,7 @@ object JavaFXMonixObservables { private val item: MenuItem ) extends AnyVal { - def observableAction(): Observable[jfxe.ActionEvent] = { + def observableAction: Observable[jfxe.ActionEvent] = { import monix.execution.cancelables.SingleAssignCancelable Observable.create(OverflowStrategy.Unbounded) { sub => val c = SingleAssignCancelable() diff --git a/src/main/scala/nova/monadic_sfx/implicits/MenuItem.scala b/src/main/scala/nova/monadic_sfx/implicits/MenuItem.scala index 577ce75..adf6864 100644 --- a/src/main/scala/nova/monadic_sfx/implicits/MenuItem.scala +++ b/src/main/scala/nova/monadic_sfx/implicits/MenuItem.scala @@ -5,6 +5,5 @@ import scalafx.scene.{control => sfxc} import JavaFXMonixObservables._ class MenuItem extends sfxc.MenuItem { - def obsAction = - new ActionObservableBuilder(this.observableAction()) + def obsAction = new ActionObservableBuilder(this.observableAction) } diff --git a/src/main/scala/nova/monadic_sfx/ui/MyFxApp.scala b/src/main/scala/nova/monadic_sfx/ui/MyFxApp.scala index 186039f..34f7400 100644 --- a/src/main/scala/nova/monadic_sfx/ui/MyFxApp.scala +++ b/src/main/scala/nova/monadic_sfx/ui/MyFxApp.scala @@ -21,14 +21,14 @@ class MyFxApp(val schedulers: Schedulers)(implicit logger: Logger[Task]) { // def stage_=(stage: PrimaryStage) = Task(internal.stage = stage) - def useInternal[T](f: JFXApp => Task[T]): Task[T] = - for { - _ <- logger.debug("Request for using internal value") - res <- f(internal).executeOn(schedulers.fx) - _ <- logger.debug(s"Result was ${res.toString()}") - } yield (res) - - def init(stage: => PrimaryStage, delay: FiniteDuration = 2000.millis) = + // def useInternal[T](f: JFXApp => Task[T]): Task[T] = + // for { + // _ <- logger.debug("Request for using internal value") + // res <- f(internal).executeOn(schedulers.fx) + // _ <- logger.debug(s"Result was ${res.toString()}") + // } yield (res) + + def init(stage: => PrimaryStage, delay: FiniteDuration = 200.millis) = for { _ <- logger.info("Starting FX App") fib <- Task(internal.main(Array.empty)).start diff --git a/src/main/scala/nova/monadic_sfx/ui/components/router/FXRouter.scala b/src/main/scala/nova/monadic_sfx/ui/components/router/FXRouter.scala new file mode 100644 index 0000000..87d35a2 --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/ui/components/router/FXRouter.scala @@ -0,0 +1,107 @@ +package nova.monadic_sfx.ui.components.router + +import enumeratum._ +import io.circe.Encoder +import io.circe.generic.JsonCodec +import io.odin.Logger +import monix.bio.Task +import nova.monadic_sfx.util.IOUtils +import nova.monadic_sfx.util.reactive.Reducer +import nova.monadic_sfx.util.reactive.Store +import scalafx.scene.Parent +import scalafx.scene.control.Label + +object FXRouter { + + final case class State[P](page: P) + + @JsonCodec + sealed abstract class Action[T] + // final case object Init extends Action + final case class Replace[T](p: T) extends Action[T] + + type FXStore[P] = Store[Action[P], State[P]] + + // def resolver2 = resolver.lift.andThen(_.getOrElse(notFound)) + + // def resolver: PartialFunction[P <: Enum[P]][P, Parent] = { + // case Home => new TextField + // } + +} + +class FXRouter[P <: EnumEntry]( +)(implicit E: Encoder[P]) { + import FXRouter._ + + def store(initialPage: P, logger: Logger[Task]) = + Task.deferAction(implicit s => + Store.createL[Action[P], State[P]]( + Replace(initialPage), + State(initialPage), + Reducer.withOptionalEffects[Task, Action[P], State[P]](reducer _) + // Seq(actionLoggerMiddleware(logger, "RouterStore")) + ) + ) + + def reducer( + state: State[P], + action: Action[P] + ): (State[P], Option[Task[Action[P]]]) = + action match { + // case Init => (state, None) + case Replace(p) => + (state.copy(page = p), None) + } + + def render( + resolver: P => Task[Parent] + )(implicit store: FXStore[P]) = + store.mapEval { case (_, FXRouter.State(p)) => IOUtils.toTask(resolver(p)) } + + def link( + page: P, + store: FXStore[P] + ) = { + store.onNext(Replace(page)) + } +} + +object BrainNotWorking { + + @JsonCodec + sealed trait Page extends EnumEntry + object Page extends Enum[Page] { + val values = findValues + final case object Home extends Page + final case class UserHome(id: Int) extends Page + } + + def resolver: PartialFunction[Page, Task[Parent]] = { + case Page.Home => + Task(new Label { + text = "HomePage" + }) + case Page.UserHome(id0) => + Task(new Label { + text = s"User Home, Id = $id0" + }) + } + + val router = new FXRouter[Page] +} + +// case class State() +// object RouterStore { + +// sealed trait Action +// case object Init extends Action + +// def reducer(state: State, action: Action) = +// action match { +// case Init => state + +// } +// def apply() = +// Store.createL[Action, State](Init, State(), Reducer(reducer _), Seq.empty) +// } diff --git a/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListStore.scala b/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListStore.scala index b424e1f..891a9cc 100644 --- a/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListStore.scala +++ b/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListStore.scala @@ -13,40 +13,77 @@ case class Todo(id: Int, content: String) object TodoListStore { @JsonCodec - sealed trait Command - case object Init extends Command - case class Add(content: String) extends Command - case class Edit(id: Int, content: String) extends Command - case class Delete(id: Int) extends Command + sealed trait Action + case object Init extends Action + case class Add(content: String) extends Action + case class Edit(id: Int, content: String) extends Action + case class Delete(id: Int) extends Action + + private case class InternalAdd(content: String) extends Action + private case object End extends Action case class State(todos: Vector[Todo], counter: Int) - def reducer( + def reducer(logger: Logger[Task])( state: State, - action: Command - ) = + action: Action + ): (State, Option[Task[Action]]) = action match { - case Init => state + case Init => (state, None) case Add(content) => - state.copy( - todos = state.todos :+ Todo(state.counter, content), - counter = state.counter + 1 - ) + val nextAction = Some(for { + // _ <- logger.debug(s"Received $content") + res <- Task.pure(InternalAdd(content)) + } yield res) + (state, nextAction) case Edit(_id, content) => val condition: Todo => Boolean = _.id == _id - state + val nextState = state .modify(_.todos.eachWhere(condition)) .using(_.copy(content = content)) + (nextState, None) case Delete(id) => - state.copy(state.todos.filterNot(_.id == id)) + (state.copy(state.todos.filterNot(_.id == id)), None) + + case InternalAdd(content) => + val nextState = state.copy( + todos = state.todos :+ Todo(state.counter, content), + counter = state.counter + 1 + ) + (nextState, Some(logger.debug(s"Received $content") >> Task.pure(End))) + case End => (state, None) } def apply(logger: Logger[Task]) = - Store - .createL[Command, State]( - Init, - State(Vector.empty[Todo], 0), - Reducer(reducer _), - Seq(actionLoggerMiddleware(logger)) - ) + Task.deferAction(implicit s => + for { + logMware <- actionLoggerMiddleware[Action, State](logger, "TodoStore") + store <- + Store + .createL[Action, State]( + Init, + State(Vector.empty[Todo], 0), + Reducer.withOptionalEffects(reducer(logger) _), + Seq( + // actionLoggerMiddleware(logger, "TodoStore2") + logMware + ) + ) + } yield (store) + ) } + +// Task.deferAction(implicit s => +// Store +// .createJsonL[Action, State]( +// Init, +// State(Vector.empty[Todo], 0), +// Reducer.withOptionalEffects(reducer(logger) _), +// "TodoStore", +// logger +// // Seq( +// // actionLoggerMiddleware(logger, "TodoStore") +// // // actionLoggerMiddleware(logger, "TodoStore2") +// // ) +// ) +// ) diff --git a/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListView.scala b/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListView.scala index 3c2fa69..a49313c 100644 --- a/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListView.scala +++ b/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListView.scala @@ -2,6 +2,7 @@ package nova.monadic_sfx.ui.components.todo import monix.bio.Task import monix.execution.cancelables.CompositeCancelable +import monix.{eval => me} import nova.monadic_sfx.implicits.FontIcon import nova.monadic_sfx.implicits.IconLiteral import nova.monadic_sfx.implicits.JFXButton @@ -21,13 +22,12 @@ import scalafx.scene.control.ListCell import scalafx.scene.control.SelectionMode import scalafx.scene.layout.HBox import scalafx.scene.text.Text -import monix.{eval => me} object TodoListView { def apply( store: MonixProSubject[ - TodoListStore.Command, - (TodoListStore.Command, TodoListStore.State) + TodoListStore.Action, + (TodoListStore.Action, TodoListStore.State) ] ): Task[Node] = Task.deferAction(implicit s => @@ -48,6 +48,9 @@ object TodoListView { new JFXListView[Todo] { def selectedItems = selectionModel().selectedItems.view + selectionModel().selectionMode = SelectionMode.Multiple + selectionModel().selectedItems.observableSeqValue ==> _selectedItems + cc += items <-- todos val emptyCell = ObjectProperty(new HBox) @@ -78,9 +81,6 @@ object TodoListView { } - selectionModel().selectionMode = SelectionMode.Multiple - selectionModel().selectedItems.observableSeqValue ==> _selectedItems - contextMenu = new ContextMenu { items ++= Seq( new MenuItem { @@ -94,6 +94,12 @@ object TodoListView { .map(todo => TodoListStore.Delete(todo.id)) .toList ) --> store + + // obsAction.split( + // _.useLazyEval(me.Task(TodoListStore.Delete(0))) --> store, + // _.useLazyEval(me.Task(TodoListStore.Delete(0))) --> store, + // _.useLazyEval(me.Task(TodoListStore.Delete(0))) --> store + // ) }, new MenuItem { text = "Edit" @@ -103,7 +109,7 @@ object TodoListView { }, new JFXButton { text = "Add" - disable <== _selectedItems.map(_.length > 0) + // disable <== _selectedItems.map(_.length > 0) obsAction .useLazyEval(me.Task(TodoListStore.Add(_content()))) --> store }, diff --git a/src/main/scala/nova/monadic_sfx/util/Misc.scala b/src/main/scala/nova/monadic_sfx/util/Misc.scala new file mode 100644 index 0000000..2886966 --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/util/Misc.scala @@ -0,0 +1,63 @@ +package nova.monadic_sfx.util + +import scalafx.beans.property.ObjectProperty +import scalafx.beans.property.ReadOnlyObjectProperty +import scalafx.beans.value.ObservableValue + +object Misc { + + implicit final class MyRichObservable[A, C](val self: ObservableValue[A, C]) + extends AnyVal { + def filter(f: A => Boolean): ReadOnlyObjectProperty[A] = + Method.filter(self)(f) + def filterNull: ReadOnlyObjectProperty[A] = Method.filterNull(self) + } + +} +object Method { + type Observable[A] = ObservableValue[A, _] + + def filter[B]( + a: Observable[B] + )(f: B => Boolean): ReadOnlyObjectProperty[B] = { + + val prop = ObjectProperty[B](a.value) + + def changeHandler() = + prop.synchronized { + if (f(a.value)) { + prop.value = a.value + } + } + + a onChange changeHandler() + prop + } + + /** + * Simply creates a new observable that mirrors the source observable but + * doesn't emit null values. JavaFX likes to work with null values in scene + * nodes/properties (shrugs) and observables by default emit null values + * that can cause crashes. ScalaFX does not offer any *fixes* for this + * + * @param a + * @return + */ + def filterNull[B]( + a: Observable[B] + ): ReadOnlyObjectProperty[B] = { + + val prop = ObjectProperty[B](a.value) + + def changeHandler() = + prop.synchronized { + if (a.value != null) { + prop.value = a.value + } + } + + a onChange changeHandler() + prop + } + +} diff --git a/src/main/scala/nova/monadic_sfx/util/reactive/Middlewares.scala b/src/main/scala/nova/monadic_sfx/util/reactive/Middlewares.scala index 0df6a30..c1b444c 100644 --- a/src/main/scala/nova/monadic_sfx/util/reactive/Middlewares.scala +++ b/src/main/scala/nova/monadic_sfx/util/reactive/Middlewares.scala @@ -1,27 +1,75 @@ package nova.monadic_sfx.util.reactive +import java.time.LocalDateTime + +import io.circe.Encoder +import io.circe.Printer +import io.circe.generic.JsonCodec +import io.circe.syntax._ import io.odin.Logger +import io.odin.LoggerMessage +import io.odin.formatter.options.PositionFormat +import io.odin.formatter.options.ThrowableFormat +import io.odin.meta.Render import monix.bio.Task import monix.reactive.Observable -import nova.monadic_sfx.util.IOUtils._ - // object Middleware { // def apply[A,M,T](ob: Observable[(A,M)], cb: (A,M) => T): Observable[(A,M)] = ob // } + +@JsonCodec +final case class StoreInfo[A]( + name: String, + action: A, + time: LocalDateTime = LocalDateTime.now() +) + +object StoreInfo { + val printer = Printer.noSpaces + implicit def render[T: Encoder]: Render[StoreInfo[T]] = + new Render[StoreInfo[T]] { + override def render(m: StoreInfo[T]): String = printer.print(m.asJson) + } +} + object Middlewares { + + // val encoder: Encoder[LoggerMessage] = + // Encoder.forProduct1("message")(m => m.message.value) + + val format = create(ThrowableFormat.Default, PositionFormat.Full) + + def create( + throwableFormat: ThrowableFormat, + positionFormat: PositionFormat + ): io.odin.formatter.Formatter = { + // val encoder: Encoder[LoggerMessage] = + // Encoder.forProduct1("message")(m => m.message.value) + (msg: LoggerMessage) => msg.message.value + } + def actionStateLoggerMiddleware[A, M]( logger: Logger[Task] - ): Middleware[A, M] = - (obs: Observable[(A, M)]) => - obs.doOnNext { - case (a, m) => toTask(logger.debug(s"Received action $a with state $m")) - } + ): Task[Middleware[A, M]] = + Task.deferAction(implicit s => + Task((obs: Observable[(A, M)]) => + obs.doOnNextF { + case (a, m) => + logger.debug(s"Received action $a with state $m") + } + ) + ) - def actionLoggerMiddleware[A, M]( - logger: Logger[Task] - ): Middleware[A, M] = - (obs: Observable[(A, M)]) => - obs.doOnNext { - case (a, _) => toTask(logger.debug(s"Received action $a ")) - } + def actionLoggerMiddleware[A: Encoder, M]( + logger: Logger[Task], + name: String + ): Task[Middleware[A, M]] = + Task.deferAction(implicit s => + Task((obs: Observable[(A, M)]) => + obs.doOnNextF { + case (a, _) => + logger.debug(StoreInfo(name, a)) + } + ) + ) } diff --git a/src/main/scala/nova/monadic_sfx/util/reactive/Store.scala b/src/main/scala/nova/monadic_sfx/util/reactive/Store.scala index 963a576..db51afe 100644 --- a/src/main/scala/nova/monadic_sfx/util/reactive/Store.scala +++ b/src/main/scala/nova/monadic_sfx/util/reactive/Store.scala @@ -1,9 +1,8 @@ package nova.monadic_sfx.util.reactive -import cats.effect.Sync +import io.circe.Encoder +import io.odin.Logger import monix.bio.Task -import monix.execution.Scheduler -import monix.reactive.Observable import monix.reactive.OverflowStrategy import monix.reactive.subjects.ConcurrentSubject @@ -15,7 +14,7 @@ object Store { middlewares: Seq[Middleware[A, M]] = Seq.empty, overflowStrategy: OverflowStrategy.Synchronous[A] = OverflowStrategy.DropOld(50) - ) = + ): Task[Store[A, M]] = Task.deferAction { implicit s => Task { val subject = ConcurrentSubject.publish[A](overflowStrategy) @@ -35,7 +34,7 @@ object Store { .behavior(initialAction -> initialState) .refCount - val res = middlewares.view.reverse.foldLeft(obs) { + val res = middlewares.foldLeft(obs) { case (obs, middleware) => middleware(obs) } @@ -46,28 +45,53 @@ object Store { } } - def create[F[_], A, M]( + def createJsonL[A: Encoder, M]( initialAction: A, initialState: M, - reducer: Reducer[A, M] - )(implicit s: Scheduler, F: Sync[F]): F[Observable[(A, M)]] = - F.delay { - val subject = ConcurrentSubject.publish[A] + reducer: Reducer[A, M], + storeName: String, + logger: Logger[Task], + middlewares: Seq[Middleware[A, M]] = Seq.empty, + overflowStrategy: OverflowStrategy.Synchronous[A] = + OverflowStrategy.DropOld(50) + ): Task[Store[A, M]] = + Task.deferAction { implicit s => + Task { + val subject = ConcurrentSubject.publish[A](overflowStrategy) - val fold: ((A, M), A) => (A, M) = { - case ((_, state), action) => { - val (newState, effects) = reducer(state, action) + val fold: ((A, M), A) => Task[(A, M)] = { + case ((_, state), action) => + Task { + val (newState, effects) = reducer(state, action) - effects.subscribe(subject.onNext _) + effects.subscribe(subject.onNext _) - action -> newState + action -> newState + } } - } - subject - .scan[(A, M)](initialAction -> initialState)(fold) - .behavior(initialAction -> initialState) - .refCount + val obs = subject + .doOnNextF(action => + logger.debug( + StoreInfo(storeName, action) + ) // .executeOn(Scheduler.global) + ) + // .doOnNextF(action => Coeval(println(action))) + .scanEvalF[Task, (A, M)](Task.pure(initialAction -> initialState))( + fold + ) + .behavior(initialAction -> initialState) + .refCount + + // val res = middlewares.foldLeft(obs) { + // case (obs, middleware) => middleware(obs) + // } + + MonixProSubject.from( + subject, + obs + ) + } } } diff --git a/src/main/scala/nova/monadic_sfx/util/reactive/package.scala b/src/main/scala/nova/monadic_sfx/util/reactive/package.scala index 6199457..09315db 100644 --- a/src/main/scala/nova/monadic_sfx/util/reactive/package.scala +++ b/src/main/scala/nova/monadic_sfx/util/reactive/package.scala @@ -6,6 +6,7 @@ import monix.reactive.Observer package object reactive { type MonixProSubject[-I, +O] = Observable[O] with Observer[I] type Middleware[A, M] = Observable[(A, M)] => Observable[(A, M)] + type Store[A, M] = MonixProSubject[A, (A, M)] /** * A Function that applies an Action onto the Stores current state.