diff --git a/build.sbt b/build.sbt index fdacc62..2c76210 100644 --- a/build.sbt +++ b/build.sbt @@ -25,6 +25,7 @@ libraryDependencies ++= Seq( "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", diff --git a/src/main/scala/nova/monadic_sfx/Main.scala b/src/main/scala/nova/monadic_sfx/Main.scala index 66d4752..de8d161 100644 --- a/src/main/scala/nova/monadic_sfx/Main.scala +++ b/src/main/scala/nova/monadic_sfx/Main.scala @@ -23,10 +23,10 @@ object Main extends MainModule with BIOApp { "application.log" ).withAsync() schedulers = new Schedulers() - backend <- Resource.make( - toIO(HttpClientMonixBackend()(schedulers.async)) - )(c => toIO(c.close())) - actorSystem <- actorSystemResource(logger) + // backend <- Resource.make( + // toIO(HttpClientMonixBackend()(schedulers.async)) + // )(c => toIO(c.close())) + // actorSystem <- actorSystemResource(logger) _ <- Resource.liftF(wire[MainApp].program) } yield () diff --git a/src/main/scala/nova/monadic_sfx/MainApp.scala b/src/main/scala/nova/monadic_sfx/MainApp.scala index d8a101d..4b2eedb 100644 --- a/src/main/scala/nova/monadic_sfx/MainApp.scala +++ b/src/main/scala/nova/monadic_sfx/MainApp.scala @@ -3,15 +3,11 @@ package nova.monadic_sfx import com.softwaremill.macwire._ import io.odin.Logger import monix.bio.Task -import monix.catnap.ConcurrentChannel 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.todo.Todo -import nova.monadic_sfx.ui.components.todo.TodoListComponent import nova.monadic_sfx.ui.components.todo.TodoListView -import nova.monadic_sfx.util.IOUtils._ import org.gerweck.scalafx.util._ import scalafx.Includes._ import scalafx.application.JFXApp.PrimaryStage @@ -25,6 +21,7 @@ import scalafx.scene.control.TableView import scalafx.scene.layout.HBox import scalafx.scene.paint.Color import scalafx.scene.shape.Rectangle +import nova.monadic_sfx.ui.components.todo.TodoListStore class MainApp( // spawnProtocol: ActorSystem[SpawnProtocol.Command], @@ -38,7 +35,7 @@ class MainApp( lazy val addTodoObs = addTodoButton.observableAction() - lazy val todoListView = TodoListView.defaultListView + // lazy val todoListView = TodoListView.defaultListView lazy val _scene = new Scene { root = new HBox { @@ -77,59 +74,66 @@ class MainApp( // _ <- Task(fxApp.stage = stage) // .executeOn(schedulers.fx) // .delayExecution(2000.millis) - todoComponent <- createTodoComponent - _ <- toIO( - addTodoObs - .mapEval(_ => - toTask(todoComponent.send(TodoListComponent.Add(Todo(1, "blah")))) - ) - .completedL - .executeOn(schedulers.fx) - .startAndForget - ) + // todoComponent <- createTodoComponent + // _ <- toIO( + // addTodoObs + // .mapEval(_ => + // toTask(todoComponent.send(TodoListComponent.Add(Todo(1, "blah")))) + // ) + // .completedL + // .executeOn(schedulers.fx) + // .startAndForget + // ) + _ <- createTodoComponent.executeOn(schedulers.fx) _ <- logger.info( s"Application started in ${(System.currentTimeMillis() - startTime) / 1000f} seconds" ) _ <- fxAppFib.join } yield () - def createTodoComponent: Task[TodoListComponent] = { + // def createTodoComponent: Task[TodoListComponent] = { + // for { + // channel <- + // ConcurrentChannel + // .of[Task, TodoListComponent.Complete, TodoListComponent.Command] + // scheduler = schedulers.fx + // lv <- TodoListView.defaultListView2.executeOn(scheduler) + // // todoLV = new TodoListView(lv) + // todoComponent <- wire[TodoListComponent.Props].create + // // TODO make this a "message pass" instead of mutating directly + // _ <- Task(_scene.getChildren += lv).executeOn(scheduler) + // // _ <- toIO( + // // delObs + // // .doOnNext(_ => toTask(logger.debug("Pressed delete"))) + // // .doOnNext(todo => + // // toTask( + // // for { + // // _ <- logger.debug(s"Got todo $todo") + // // _ <- todoComponent.send(TodoListComponent.Delete(todo.id)) + // // // _ <- Task.sequence( + // // // lst.map(todo => + // // // todoComponent.send(TodoListComponent.Delete(todo.id)) + // // // ) + // // // ) + // // } yield () + // // ) + // // ) + // // .completedL + // // ).startAndForget + // // _ <- toIO( + // // editObs + // // .doOnNext(_ => toTask(logger.debug("Pressed edit"))) + // // .completedL + // // ).startAndForget + // } yield todoComponent + // } + + def createTodoComponent: Task[Unit] = for { - channel <- - ConcurrentChannel - .of[Task, TodoListComponent.Complete, TodoListComponent.Command] - scheduler = schedulers.fx - (lv, delObs, editObs) <- - TodoListView.defaultListView2.executeOn(scheduler) - todoLV = new TodoListView(lv) - todoComponent <- wire[TodoListComponent.Props].create - // TODO make this a "message pass" instead of mutating directly - _ <- Task(_scene.getChildren += lv).executeOn(scheduler) - _ <- toIO( - delObs - .doOnNext(_ => toTask(logger.debug("Pressed delete"))) - .doOnNext(todo => - toTask( - for { - _ <- logger.debug(s"Got todo $todo") - _ <- todoComponent.send(TodoListComponent.Delete(todo.id)) - // _ <- Task.sequence( - // lst.map(todo => - // todoComponent.send(TodoListComponent.Delete(todo.id)) - // ) - // ) - } yield () - ) - ) - .completedL - ).startAndForget - _ <- toIO( - editObs - .doOnNext(_ => toTask(logger.debug("Pressed edit"))) - .completedL - ).startAndForget - } yield todoComponent - } + store <- TodoListStore(logger) + lv <- TodoListView(store) + _ <- Task(_scene.getChildren += lv) + } yield () } diff --git a/src/main/scala/nova/monadic_sfx/implicits/ActionObservable.scala b/src/main/scala/nova/monadic_sfx/implicits/ActionObservable.scala new file mode 100644 index 0000000..cb33d3b --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/implicits/ActionObservable.scala @@ -0,0 +1,37 @@ +package nova.monadic_sfx.implicits + +import monix.reactive.Observable +import monix.reactive.Observer +import monix.{eval => me} +import monix.execution.Scheduler + +class ActionObservableExecuter[T]( + delegate: Observable[T] +) { + def -->(sub: Observer[T])(implicit s: Scheduler) = + delegate + .doOnNext(el => me.Task(sub.onNext(el))) + .subscribe() + +} +class ActionObservableBuilder[A]( + observableAction: Observable[A] +) { + def useLazy[T](v: => T) = + new ActionObservableExecuter[T](observableAction.mapEval(_ => me.Task(v))) + + def use[T](cb: A => T) = + new ActionObservableExecuter[T]( + observableAction.mapEval(ae => me.Task(cb(ae))) + ) + + def useIterable[T](cb: A => collection.immutable.Iterable[T]) = + new ActionObservableExecuter[T]( + observableAction.flatMap(a => + Observable.suspend(Observable.fromIterable(cb(a))) + ) + ) + + def map[B](cb: A => B) = + new ActionObservableBuilder(observableAction.mapEval(v => me.Task(cb(v)))) +} diff --git a/src/main/scala/nova/monadic_sfx/implicits/JFXButton.scala b/src/main/scala/nova/monadic_sfx/implicits/JFXButton.scala index 9ef8508..fc6d562 100644 --- a/src/main/scala/nova/monadic_sfx/implicits/JFXButton.scala +++ b/src/main/scala/nova/monadic_sfx/implicits/JFXButton.scala @@ -39,4 +39,7 @@ class JFXButton( ripplerFill() = b } + def obsAction = + new ActionObservableBuilder(this.observableAction()) + } diff --git a/src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala b/src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala index 660d8e0..dc72d82 100644 --- a/src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala +++ b/src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala @@ -19,6 +19,12 @@ import scalafx.beans.value.ObservableValue import scalafx.collections.ObservableBuffer import scalafx.scene.Scene import scalafx.scene.control.ButtonBase +import scalafx.beans.property.ReadOnlyProperty +import javafx.event.ActionEvent +import javafx.event.EventHandler +import javafx.scene.{control => jfxsc} +import scalafx.scene.control.MenuItem +import org.gerweck.scalafx.util._ object JavaFXMonixObservables { @@ -68,29 +74,35 @@ object JavaFXMonixObservables { } } - // implicit final class BindObs[T, J](private val prop: Property[T, J]) - // extends AnyVal { - // def -->(op: Observer[T]) = { - // op.onNext(prop.value) - // } + implicit final class BindObs[T, J](private val prop: Property[T, J]) + extends AnyVal { + def -->(op: Observer[T]) = { + op.onNext(prop.value) + } - // def <--(obs: Observable[T])(implicit s: Scheduler) = { - // obs.doOnNext(v => me.Task(prop.value = v)).subscribe() - // } + def -->(op: Property[T, J]) = { + op() = prop.value + } + + def <--(obs: Observable[T])(implicit s: Scheduler) = { + obs.doOnNext(v => me.Task(prop.value = v)).subscribe() + } - // def observableChange[J1 >: J]() - // : Observable[(ObservableValue[T, J], J1, J1)] = { - // import monix.execution.cancelables.SingleAssignCancelable - // Observable.create(OverflowStrategy.Unbounded) { sub => - // val c = SingleAssignCancelable() + def asOption = prop.map(Option(_)) - // val canc = prop.onChange((a, b, c) => sub.onNext((a, b, c))) + def observableChange[J1 >: J]() + : Observable[(ObservableValue[T, J], J1, J1)] = { + import monix.execution.cancelables.SingleAssignCancelable + Observable.create(OverflowStrategy.Unbounded) { sub => + val c = SingleAssignCancelable() - // c := Cancelable(() => canc.cancel()) - // c - // } - // } - // } + val canc = prop.onChange((a, b, c) => sub.onNext((a, b, c))) + + c := Cancelable(() => canc.cancel()) + c + } + } + } implicit final class BindObs2[A](private val prop: ObjectProperty[A]) extends AnyVal { @@ -107,6 +119,10 @@ object JavaFXMonixObservables { // op(prop.observableChange().map(_._3)).runToFuture // } + def -->(op: Property[A, A]) = { + prop.onChange((a, b, c) => if (c != null) op() = c) + } + def <--(obs: Observable[A])(implicit s: Scheduler) = { obs.doOnNext(v => me.Task(prop() = v)).subscribe() } @@ -125,6 +141,34 @@ object JavaFXMonixObservables { } } + implicit final class BindObs3[T, J](private val prop: ReadOnlyProperty[T, J]) + extends AnyVal { + def -->(op: Observer[T]) = { + op.onNext(prop.value) + } + + def -->(op: Property[T, J]) = { + op <== prop + } + + // def <--(obs: Observable[T])(implicit s: Scheduler) = { + // obs.doOnNext(v => me.Task(prop.value = v)).subscribe() + // } + + def observableChange[J1 >: J]() + : Observable[(ObservableValue[T, J], J1, 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))) + + c := Cancelable(() => canc.cancel()) + c + } + } + } + implicit final class ObjectPropertyObservableListExt[A]( private val prop: ObjectProperty[ObservableList[A]] ) extends AnyVal { @@ -166,6 +210,20 @@ object JavaFXMonixObservables { } + implicit final class ObjectPropertyActionEvent( + private val prop: ObjectProperty[EventHandler[ActionEvent]] + ) extends AnyVal { + // def <--(obs: Observable[ActionEvent])(implicit s: Scheduler) = { + // obs.doOnNext(v => me.Task(prop() = ObservableBuffer.from(v))).subscribe() + // } + + // def -->(sub: Observer[ActionEvent]) = + // prop(). + + } + + // def emit(prop: ObjectProperty[EventHandler[ActionEvent]]) = + implicit final class OnActionObservable( private val button: ButtonBase ) extends AnyVal { @@ -202,4 +260,41 @@ object JavaFXMonixObservables { } } } + + implicit final class MenuItemActionObservable( + private val et: MenuItem + ) extends AnyVal { + // def -->[T]( + // op: Observable[jfxe.ActionEvent] => me.Task[T] + // )(implicit s: Scheduler) = { + // op(button.observableAction()).runToFuture + // } + + // def -->( + // sub: ConcurrentSubject[jfxe.ActionEvent, jfxe.ActionEvent] + // ) = { + // button.onAction = value => sub.onNext(value) + // } + + def observableAction(): Observable[jfxe.ActionEvent] = { + import monix.execution.cancelables.SingleAssignCancelable + Observable.create(OverflowStrategy.Unbounded) { sub => + val c = SingleAssignCancelable() + val l = new jfxe.EventHandler[jfxe.ActionEvent] { + override def handle(event: jfxe.ActionEvent): Unit = { + if (sub.onNext(event) == Ack.Stop) c.cancel() + } + } + + et.onAction = l + c := Cancelable(() => + et.removeEventHandler( + jfxe.ActionEvent.ACTION, + l + ) + ) + c + } + } + } } diff --git a/src/main/scala/nova/monadic_sfx/implicits/MenuItem.scala b/src/main/scala/nova/monadic_sfx/implicits/MenuItem.scala new file mode 100644 index 0000000..839c65d --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/implicits/MenuItem.scala @@ -0,0 +1,9 @@ +package nova.monadic_sfx.implicits + +import JavaFXMonixObservables._ +import scalafx.scene.{control => sfxc} + +class MenuItem extends sfxc.MenuItem { + def obsAction = + new ActionObservableBuilder(this.observableAction()) +} diff --git a/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListComponent.scala b/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListComponent.scala deleted file mode 100644 index d6365cd..0000000 --- a/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListComponent.scala +++ /dev/null @@ -1,556 +0,0 @@ -package nova.monadic_sfx.ui.components.todo - -import scala.concurrent.Future -import scala.concurrent.duration.FiniteDuration - -import cats.effect.Sync -import cats.effect.concurrent.Deferred -import io.odin.Logger -import monix.bio.Task -import monix.catnap.ConcurrentChannel -import monix.catnap.ConsumerF -import monix.execution.Ack -import monix.execution.Cancelable -import monix.execution.Scheduler -import monix.reactive.Observable -import monix.reactive.Observer -import monix.reactive.OverflowStrategy -import monix.reactive.observers.Subscriber -import monix.reactive.subjects.ConcurrentSubject -import nova.monadic_sfx.implicits.FontIcon -import nova.monadic_sfx.implicits.IconLiteral -import nova.monadic_sfx.implicits.JFXListView -import nova.monadic_sfx.implicits.JavaFXMonixObservables._ -import nova.monadic_sfx.ui.components.todo.TodoListComponent.Add -import nova.monadic_sfx.ui.components.todo.TodoListComponent.Delete -import nova.monadic_sfx.ui.components.todo.TodoListComponent.Edit -import scalafx.Includes._ -import scalafx.beans.property.StringProperty -import scalafx.collections.ObservableBuffer -import scalafx.scene.control.ContextMenu -import scalafx.scene.control.ListCell -import scalafx.scene.control.MenuItem -import scalafx.scene.control.SelectionMode -import scalafx.scene.layout.HBox -import scalafx.scene.text.Text -import nova.monadic_sfx.ui.components.todo.Store.MonixProSubject -import nova.monadic_sfx.util.IOUtils -import monix.tail.Iterant - -case class Todo(id: Int, content: String) - -class TodoListView( - val listView: JFXListView[Todo] = TodoListView.defaultListView, - val lvObs: ObservableBuffer[Todo] = ObservableBuffer.empty -) { - listView.items = lvObs -} - -object TodoListView { - def defaultListView = - new JFXListView[Todo] { - // cellFactory = _ => - // new ListCell[Todo] { - // // item.onChange((a, b, c) => ()) - // overr - // } - contextMenu = new ContextMenu { - items ++= Seq( - new MenuItem { - text = "delete" - }, - new MenuItem { - text = "edit" - } - ) - } - } - // import scalafx.scene.control.MultipleSelectionModel -// .getOrElse(Todo(-1, "blah")) - implicit class Operations[A](val sink: Observer[A]) extends AnyVal {} - - // def reducer( - // stateC: Coeval[ObservableBuffer[Todo]], - // action: TodoListComponent.Command - // ) = - // action match { - // case Add(todo) => - // for { - // state <- stateC - // } yield state :+ todo - // // case Find(id, result) => - // case Edit(id, content) => stateC - // case Delete(id) => - // for { - // state <- stateC - // } yield state.filterNot(_.id == id) - // case _ => stateC - // } - - def reducer( - state: Vector[Todo], - action: TodoListComponent.Command - ) = - action match { - case Add(todo) => state :+ todo - // case Find(id, result) => - case Edit(id, content) => state - case Delete(id) => - state.filterNot(_.id == id) - case _ => state - } - - def defaultListView2: Task[ - ( - JFXListView[Todo], - Observable[Todo], - Observable[Todo] - ) - ] = - Task.deferAction(implicit s => - Store - .createL[TodoListComponent.Command, Vector[Todo]]( - TodoListComponent.Delete(0), - Vector.empty[Todo], - (s: Vector[Todo], a: TodoListComponent.Command) => - reducer(s, a) -> Observable.empty - ) - .flatMap(store => - Task { - val deleteSub = ConcurrentSubject.publish[Todo] - val editSub = ConcurrentSubject.publish[Todo] - - // store.flatMap(st => Task(st.sink)) - - // val deleteSub2 = - // deleteSub.map(todo => (buf: ObservableBuffer[Todo]) => buf :+ todo) - // val addSub = - // ConcurrentSubject - // .publish[Todo] - // .map(todo => (buf: ObservableBuffer[Todo]) => buf :+ todo) - // val state = Observable(deleteSub2, addSub).merge.scan0( - // ObservableBuffer.empty[Todo] - // )((buf, fn) => fn(buf)) - - val todos = - store.map { case (_, items) => items } - - val listView = new JFXListView[Todo] { lv => - def selectedItems = lv.selectionModel().selectedItems.view - // items = todos - items <-- todos - // .map(ObservableBuffer.from(_)) - cellFactory = _ => - new ListCell[Todo] { - val _text = StringProperty("") - val _graphic = new HBox { - children = Seq( - new FontIcon { - iconSize = 10 - iconLiteral = IconLiteral.Gmi10k - }, - new Text { - text <== _text - } - ) - } - item.onChange((_, _, todo) => { - println("called") - if (todo != null) { - _text() = s"${todo.id} - ${todo.content}" - graphic = _graphic - } else { - _text() = "" - graphic = null - } - }) - - } - selectionModel().selectionMode = SelectionMode.Multiple - contextMenu = new ContextMenu { - items ++= Seq( - new MenuItem { - text = "Add" - onAction = _ => - store.sink - .onNext(TodoListComponent.Add(Todo(1, "blah3"))) - }, - new MenuItem { - text = "Delete" - // onAction = _ => - // for { - // items <- Option(lv.selectionModel().selectedItems) - // _ <- Some(items.foreach(item => deleteSub.onNext(item))) - // } yield () - onAction = _ => - selectedItems - .map(todo => TodoListComponent.Delete(todo.id)) - .foreach(store.sink.onNext) - }, - new MenuItem { - text = "Edit" - // onAction = _ => - // Option(lv.selectionModel().selectedItems).foreach(items => - // items.foreach(item => editSub.onNext(item)) - // ) - } - ) - } - } - - (listView, deleteSub, editSub) - } - ) - ) -} - -private[todo] class TodoListComponentImpure( - todoListView: TodoListView -) { - def add(todo: Todo) = todoListView.lvObs += todo - def find(id: Int) = todoListView.lvObs.find(_.id == id) - def edit(id: Int, content: String) = - find(id) - .map(todo => - todoListView.lvObs.replaceAll( - todo, - Todo(id, content) - ) - ) - .getOrElse(false) -} - -class TodoListOps private ( - props: TodoListOps.Props -) { - import props._ -// lazy val internal = new TodoListComponentImpure(todoListView) - -// def add(todo: Todo) = Task(internal.add(todo)) - def add(todo: Todo) = Task(todoListView.lvObs += todo).executeOn(fxScheduler) - def find(id: Int) = - Task(todoListView.lvObs.find(_.id == id)).executeOn(fxScheduler) - def delete(id: Int) = - (for { - mbTodo <- find(id) - _ <- logger.debug(mbTodo.toString()) - res <- Task( - mbTodo.map(todo => todoListView.lvObs.removeAll(todo)) - ) - _ <- logger.debug(todoListView.lvObs.toString()) - } yield res.getOrElse(false)).executeOn(fxScheduler) - def edit(id: Int, content: String) = - (for { - mbTodo <- find(id) - res <- Task( - mbTodo.map(todo => - todoListView.lvObs.replaceAll( - todo, - Todo(id, content) - ) - ) - ) - } yield res.getOrElse(false)).executeOn(fxScheduler) -} - -object TodoListOps { - class Props( - val todoListView: TodoListView, - val fxScheduler: Scheduler, - val logger: Logger[Task] - ) { - def create = Task(new TodoListOps(this)) - } -} - -object TodoListComponent { - sealed trait Complete - object Complete extends Complete - - sealed trait Command - // sealed trait Tell extends Command - // sealed abstract class Ask extends Command - - case class Add(todo: Todo) extends Command - case class Find(id: Int, result: Deferred[Task, Option[Todo]]) extends Command - case class Edit(id: Int, content: String) extends Command - case class Delete(id: Int) extends Command - // private case class FindInternal(id: Int, result: Deferred[Task, Todo]) - // extends Ask - - class Props( - val todoListView: TodoListView, - val fxScheduler: Scheduler, - val channel: ConcurrentChannel[ - Task, - TodoListComponent.Complete, - TodoListComponent.Command - ], - val logger: Logger[Task] - ) { - - def create = - for { - todoListOps <- - new TodoListOps.Props(todoListView, fxScheduler, logger).create - consumer = channel.consume.use(ref => todoConsumer(ref, todoListOps)) - _ <- consumer.startAndForget - } yield (new TodoListComponent(this)) - - private def todoConsumer( - consumer: ConsumerF[Task, Complete, Command], - ops: TodoListOps - ): Task[Unit] = - consumer.pull - .flatMap { - case Left(complete) => logger.info("Received `Complete` event") - case Right(command) => - logger.debug(s"Received command $command") >> - (command match { - // case t: Tell => - // t match { - // case Add(todo) => ops.add(todo) - // case _ => Task.unit - // } - case Add(todo) => ops.add(todo) - // case Find(id) => - // for { - // p <- Deferred[Task, Todo] - // _ <- channel.push(FindInternal(id, p)) - // res <- p.get - // } yield (res) - case Find(id, result) => - for { - mbTodo <- ops.find(id) - } yield result.complete(mbTodo) - // case _ => Task.unit - - case Delete(id) => ops.delete(id) - - case Edit(id, content) => ops.edit(id, content) - }) - } - .flatMap(_ => todoConsumer(consumer, ops)) - - } - -} - -class TodoListComponent(props: TodoListComponent.Props) { - import props._ - import TodoListComponent._ - - def send(command: Command) = channel.push(command) - - def ask[T]( - commandBuilder: Deferred[Task, T] => Command - )(implicit timeout: FiniteDuration) = - for { - p <- Deferred[Task, T] - _ <- channel.push(commandBuilder(p)) - res <- p.get.timeout(timeout) - } yield res - - def stop = channel.halt(Complete) - - // import scala.concurrent.duration._ - // val x = ask(FindInternal(0, _))(2.seconds) - -} -// : F[ProSubject[A, (A, M)]] - -// interface Middleware { -// fun dispatch(store: Store, next: (A) -> Unit, action: A) -// } - -trait Middleware[A, M] { - def dispatch[T]( - store: MonixProSubject[A, (A, M)], - cb: (A, M) => Task[T], - cb2: (A, M) => Observable[(A, M)], - cb3: Observable[(A, M)] => Observable[(A, M)] - ) = { - // store.fil - store.mapEval { - case (a, m) => IOUtils.toTask(cb(a, m)) - } - store.flatMap { - case (a, m) => cb2(a, m) - } - - cb3(store) - - def cb3impl(obs: Observable[(A, M)]) = - obs.doOnNext { - case (a, m) => IOUtils.toTask(Task(println("hello"))) - } - - def cb3impl2(obs: Observable[(A, M)]) = - obs.filter { - case (a, m) => m == "" - } - - cb3impl2(cb3impl(store)) - - val s = Seq(cb3impl _) - - val res = s.foldLeft(Observable.empty[(A, M)]) { - case (o1, o2) => o2(o1) - } - - val x = Iterant[Task].of(1, 2, 3) - - // x match { - // case Next(item, rest) => () - // case Halt(e) => () - // } - - } -} - -object Store { - type Reducer[A, M] = (M, A) => (M, Observable[A]) - type MonixProSubject[-I, +O] = Observable[O] with Observer[I] - // class MonixProSubject2[-I, +O] extends Subject[I, O] - object MonixProSubject { - def from[I, O]( - observer: Observer[I], - observable: Observable[O] - ): MonixProSubject[I, O] = - new Observable[O] with Observer[I] { - override def onNext(elem: I): Future[Ack] = observer.onNext(elem) - override def onError(ex: Throwable): Unit = observer.onError(ex) - override def onComplete(): Unit = observer.onComplete() - override def unsafeSubscribeFn(subscriber: Subscriber[O]): Cancelable = - observable.unsafeSubscribeFn(subscriber) - } - } - - def createL[A, M]( - initialAction: A, - initialState: M, - reducer: Reducer[A, M], - overflowStrategy: OverflowStrategy.Synchronous[A] = - OverflowStrategy.DropOld(50) - ) = - 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) - - effects.subscribe(subject.onNext _) - - action -> newState - } - } - - MonixProSubject.from( - subject, - subject - .scan[(A, M)](initialAction -> initialState)(fold) - .behavior(initialAction -> initialState) - .refCount - ) - } - } - - def create[F[_], A, 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] - - val fold: ((A, M), A) => (A, M) = { - case ((_, state), action) => { - val (newState, effects) = reducer(state, action) - - effects.subscribe(subject.onNext _) - - action -> newState - } - } - - subject - .scan[(A, M)](initialAction -> initialState)(fold) - .behavior(initialAction -> initialState) - .refCount - } - -} - -// object TodoListComponent { -// sealed trait Complete -// object Complete extends Complete - -// sealed trait Command - -// class Props( -// val todoListView: TodoListView, -// val fxScheduler: Scheduler, -// val channel: ConcurrentChannel[ -// Task, -// TodoListComponent.Complete, -// TodoListComponent.Command -// ] -// ) { -// def create = Task(new TodoListComponent(this)) -// } -// } - -// class TodoListComponent(props: TodoListComponent.Props) { -// import props._ -// import TodoListComponent._ -// def init = -// for { -// todoListOps <- new TodoListOps.Props(todoListView, fxScheduler).create -// consumer = channel.consume.use(ref => todoConsumer(ref, todoListOps)) -// _ <- consumer.startAndForget -// } yield () - -// def send(command: Command) = channel.push(command) - -// def todoConsumer( -// consumer: ConsumerF[Task, Complete, Command], -// ops: TodoListOps -// ) = -// consumer.pull.flatMap { -// case Left(value) => Task.unit -// case Right(value) => Task.unit -// } -// } - -// def askHandler( -// channel: ConcurrentChannel[ -// Task, -// TodoListComponent.Complete, -// TodoListComponent.Command -// ], -// consumer: ConsumerF[Task, Complete, Command], -// ops: TodoListOps -// ) = -// consumer.pull.flatMap { -// case Left(complete) => Task.unit -// case Right(command) => -// command match { -// case a: Ask => -// a match { -// case Find(id) => -// for { -// p <- Deferred[Task, Todo] -// _ <- channel.push(FindInternal(id, p)) -// res <- p.get -// } yield (res) -// case FindInternal(id, result) => -// for { -// mb <- ops.find(id) -// } yield result.complete(mb.get) -// case _ => Task.unit -// } -// case _ => Task.unit -// } -// } diff --git a/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListComponentOld.scala b/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListComponentOld.scala new file mode 100644 index 0000000..ab5667f --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListComponentOld.scala @@ -0,0 +1,378 @@ +package nova.monadic_sfx.ui.components.todo + +import scala.concurrent.duration.FiniteDuration + +import cats.effect.concurrent.Deferred +import io.odin.Logger +import monix.bio.Task +import monix.catnap.ConcurrentChannel +import monix.catnap.ConsumerF +import monix.execution.Scheduler +import monix.reactive.Observable +import monix.reactive.Observer +import nova.monadic_sfx.implicits.FontIcon +import nova.monadic_sfx.implicits.IconLiteral +import nova.monadic_sfx.implicits.JFXListView +import nova.monadic_sfx.implicits.JavaFXMonixObservables._ +import scalafx.Includes._ +import scalafx.beans.property.StringProperty +import scalafx.collections.ObservableBuffer +import scalafx.scene.control.ContextMenu +import scalafx.scene.control.ListCell +import scalafx.scene.control.MenuItem +import scalafx.scene.control.SelectionMode +import scalafx.scene.layout.HBox +import scalafx.scene.text.Text +import nova.monadic_sfx.util.reactive._ + +class TodoListViewOld( + val listView: JFXListView[Todo] = TodoListViewOld.defaultListView, + val lvObs: ObservableBuffer[Todo] = ObservableBuffer.empty +) { + listView.items = lvObs +} + +object TodoListViewOld { + def defaultListView = + new JFXListView[Todo] { + contextMenu = new ContextMenu { + items ++= Seq( + new MenuItem { + text = "delete" + }, + new MenuItem { + text = "edit" + } + ) + } + } + + implicit class Operations[A](val sink: Observer[A]) extends AnyVal {} + + def defaultListView2( + store: MonixProSubject[ + TodoListComponentOld.Command, + (TodoListComponentOld.Command, Vector[Todo]) + ] + ): Task[JFXListView[Todo]] = + Task.deferAction(implicit s => + Task { + val todos = + store.map { case (_, items) => items } + + val listView = new JFXListView[Todo] { lv => + def selectedItems = lv.selectionModel().selectedItems.view + // items = todos + items <-- todos + // .map(ObservableBuffer.from(_)) + cellFactory = _ => + new ListCell[Todo] { + val _text = StringProperty("") + val _graphic = new HBox { + children = Seq( + new FontIcon { + iconSize = 10 + iconLiteral = IconLiteral.Gmi10k + }, + new Text { + text <== _text + } + ) + } + item.onChange((_, _, todo) => { + println("called") + if (todo != null) { + _text() = s"${todo.id} - ${todo.content}" + graphic = _graphic + } else { + _text() = "" + graphic = null + } + }) + + } + selectionModel().selectionMode = SelectionMode.Multiple + contextMenu = new ContextMenu { + items ++= Seq( + new MenuItem { + text = "Add" + onAction = _ => + store.sink + .onNext(TodoListComponentOld.Add(Todo(1, "blah3"))) + }, + new MenuItem { + text = "Delete" + // onAction = _ => + // for { + // items <- Option(lv.selectionModel().selectedItems) + // _ <- Some(items.foreach(item => deleteSub.onNext(item))) + // } yield () + onAction = _ => + selectedItems + .map(todo => TodoListComponentOld.Delete(todo.id)) + .foreach(store.sink.onNext) + }, + new MenuItem { + text = "Edit" + // onAction = _ => + // Option(lv.selectionModel().selectedItems).foreach(items => + // items.foreach(item => editSub.onNext(item)) + // ) + } + ) + } + } + + listView + } + ) + +} + +private[todo] class TodoListComponentImpure( + todoListView: TodoListViewOld +) { + def add(todo: Todo) = todoListView.lvObs += todo + def find(id: Int) = todoListView.lvObs.find(_.id == id) + def edit(id: Int, content: String) = + find(id) + .map(todo => + todoListView.lvObs.replaceAll( + todo, + Todo(id, content) + ) + ) + .getOrElse(false) +} + +class TodoListOps private ( + props: TodoListOps.Props +) { + import props._ +// lazy val internal = new TodoListComponentImpure(todoListView) + +// def add(todo: Todo) = Task(internal.add(todo)) + def add(todo: Todo) = Task(todoListView.lvObs += todo).executeOn(fxScheduler) + def find(id: Int) = + Task(todoListView.lvObs.find(_.id == id)).executeOn(fxScheduler) + def delete(id: Int) = + (for { + mbTodo <- find(id) + _ <- logger.debug(mbTodo.toString()) + res <- Task( + mbTodo.map(todo => todoListView.lvObs.removeAll(todo)) + ) + _ <- logger.debug(todoListView.lvObs.toString()) + } yield res.getOrElse(false)).executeOn(fxScheduler) + def edit(id: Int, content: String) = + (for { + mbTodo <- find(id) + res <- Task( + mbTodo.map(todo => + todoListView.lvObs.replaceAll( + todo, + Todo(id, content) + ) + ) + ) + } yield res.getOrElse(false)).executeOn(fxScheduler) +} + +object TodoListOps { + class Props( + val todoListView: TodoListViewOld, + val fxScheduler: Scheduler, + val logger: Logger[Task] + ) { + def create = Task(new TodoListOps(this)) + } +} + +object TodoListComponentOld { + sealed trait Complete + object Complete extends Complete + + sealed trait Command + // sealed trait Tell extends Command + // sealed abstract class Ask extends Command + + case class Add(todo: Todo) extends Command + case class Find(id: Int, result: Deferred[Task, Option[Todo]]) extends Command + case class Edit(id: Int, content: String) extends Command + case class Delete(id: Int) extends Command + // private case class FindInternal(id: Int, result: Deferred[Task, Todo]) + // extends Ask + + def reducer( + state: Vector[Todo], + action: TodoListComponentOld.Command + ) = + action match { + case Add(todo) => state :+ todo + // case Find(id, result) => + case Edit(id, content) => state + case Delete(id) => + state.filterNot(_.id == id) + case _ => state + } + + val store = + Store + .createL[TodoListComponentOld.Command, Vector[Todo]]( + TodoListComponentOld.Delete(0), + Vector.empty[Todo], + (s: Vector[Todo], a: TodoListComponentOld.Command) => + reducer(s, a) -> Observable.empty + ) + + class Props( + val todoListView: TodoListViewOld, + val fxScheduler: Scheduler, + val channel: ConcurrentChannel[ + Task, + TodoListComponentOld.Complete, + TodoListComponentOld.Command + ], + val logger: Logger[Task] + ) { + + def create = + for { + todoListOps <- + new TodoListOps.Props(todoListView, fxScheduler, logger).create + consumer = channel.consume.use(ref => todoConsumer(ref, todoListOps)) + _ <- consumer.startAndForget + } yield (new TodoListComponentOld(this)) + + private def todoConsumer( + consumer: ConsumerF[Task, Complete, Command], + ops: TodoListOps + ): Task[Unit] = + consumer.pull + .flatMap { + case Left(complete) => logger.info("Received `Complete` event") + case Right(command) => + logger.debug(s"Received command $command") >> + (command match { + // case t: Tell => + // t match { + // case Add(todo) => ops.add(todo) + // case _ => Task.unit + // } + case Add(todo) => ops.add(todo) + // case Find(id) => + // for { + // p <- Deferred[Task, Todo] + // _ <- channel.push(FindInternal(id, p)) + // res <- p.get + // } yield (res) + case Find(id, result) => + for { + mbTodo <- ops.find(id) + } yield result.complete(mbTodo) + // case _ => Task.unit + + case Delete(id) => ops.delete(id) + + case Edit(id, content) => ops.edit(id, content) + }) + } + .flatMap(_ => todoConsumer(consumer, ops)) + + } + +} + +class TodoListComponentOld(props: TodoListComponentOld.Props) { + import props._ + import TodoListComponentOld._ + + def send(command: Command) = channel.push(command) + + def ask[T]( + commandBuilder: Deferred[Task, T] => Command + )(implicit timeout: FiniteDuration) = + for { + p <- Deferred[Task, T] + _ <- channel.push(commandBuilder(p)) + res <- p.get.timeout(timeout) + } yield res + + def stop = channel.halt(Complete) + + // import scala.concurrent.duration._ + // val x = ask(FindInternal(0, _))(2.seconds) + +} + +// object TodoListComponent { +// sealed trait Complete +// object Complete extends Complete + +// sealed trait Command + +// class Props( +// val todoListView: TodoListView, +// val fxScheduler: Scheduler, +// val channel: ConcurrentChannel[ +// Task, +// TodoListComponent.Complete, +// TodoListComponent.Command +// ] +// ) { +// def create = Task(new TodoListComponent(this)) +// } +// } + +// class TodoListComponent(props: TodoListComponent.Props) { +// import props._ +// import TodoListComponent._ +// def init = +// for { +// todoListOps <- new TodoListOps.Props(todoListView, fxScheduler).create +// consumer = channel.consume.use(ref => todoConsumer(ref, todoListOps)) +// _ <- consumer.startAndForget +// } yield () + +// def send(command: Command) = channel.push(command) + +// def todoConsumer( +// consumer: ConsumerF[Task, Complete, Command], +// ops: TodoListOps +// ) = +// consumer.pull.flatMap { +// case Left(value) => Task.unit +// case Right(value) => Task.unit +// } +// } + +// def askHandler( +// channel: ConcurrentChannel[ +// Task, +// TodoListComponent.Complete, +// TodoListComponent.Command +// ], +// consumer: ConsumerF[Task, Complete, Command], +// ops: TodoListOps +// ) = +// consumer.pull.flatMap { +// case Left(complete) => Task.unit +// case Right(command) => +// command match { +// case a: Ask => +// a match { +// case Find(id) => +// for { +// p <- Deferred[Task, Todo] +// _ <- channel.push(FindInternal(id, p)) +// res <- p.get +// } yield (res) +// case FindInternal(id, result) => +// for { +// mb <- ops.find(id) +// } yield result.complete(mb.get) +// case _ => Task.unit +// } +// case _ => Task.unit +// } +// } 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 new file mode 100644 index 0000000..23b9675 --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListStore.scala @@ -0,0 +1,67 @@ +package nova.monadic_sfx.ui.components.todo + +import nova.monadic_sfx.util.reactive.Store +import nova.monadic_sfx.util.reactive.Reducer +import nova.monadic_sfx.util.reactive.Middlewares.actionLoggerMiddleware +import io.odin.Logger +import monix.bio.Task +import io.odin._ +import io.odin.syntax._ +import io.circe.generic.JsonCodec +import com.softwaremill.quicklens._ + +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 + + case class State(todos: Vector[Todo], counter: Int) + + def reducer( + state: State, + action: Command + ) = + action match { + case Init => state + case Add(content) => + state.copy( + todos = state.todos :+ Todo(state.counter, content), + counter = state.counter + 1 + ) + case Edit(id, content) => + state + .modify(_.todos.eachWhere(_.id == id)) + .using(_.copy(content = content)) + case Delete(id) => + state.copy(state.todos.filterNot(_.id == id)) + } + + def updateTodo(id: Int, content: String, todos: Vector[Todo]) = + todos.view.zipWithIndex + .find { + case (todo, index) => todo.id == id + } + .map { + case (todo, index) => todo.copy(content = content) -> index + } + .map { + case (todo, index) => todos.updated(index, todo) + } + + val middlewareLogger = consoleLogger[Task]().withAsync() + + def apply(logger: Logger[Task]) = + Store + .createL[Command, State]( + Init, + State(Vector.empty[Todo], 0), + Reducer(reducer _), + Seq(actionLoggerMiddleware(logger)) + ) +} 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 new file mode 100644 index 0000000..de367fd --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListView.scala @@ -0,0 +1,91 @@ +package nova.monadic_sfx.ui.components.todo + +import monix.bio.Task +import nova.monadic_sfx.implicits.FontIcon +import nova.monadic_sfx.implicits.IconLiteral +import nova.monadic_sfx.implicits.JFXListView +import nova.monadic_sfx.implicits.JavaFXMonixObservables._ +import scalafx.Includes._ +import scalafx.beans.property.StringProperty +import scalafx.scene.control.ContextMenu +import scalafx.scene.control.ListCell +import scalafx.scene.control.SelectionMode +import scalafx.scene.layout.HBox +import scalafx.scene.text.Text +import nova.monadic_sfx.util.reactive._ +import org.gerweck.scalafx.util._ +import scalafx.beans.property.ObjectProperty +import nova.monadic_sfx.implicits.MenuItem +import monix.execution.cancelables.CompositeCancelable + +object TodoListView { + def apply( + store: MonixProSubject[ + TodoListStore.Command, + (TodoListStore.Command, TodoListStore.State) + ] + ): Task[JFXListView[Todo]] = + Task.deferAction(implicit s => + Task { + val cc = CompositeCancelable() + val todos = + store.map { case (_, state) => state.todos } + + new JFXListView[Todo] { + def selectedItems = selectionModel().selectedItems.view + + cc += items <-- todos + + val emptyCell = ObjectProperty(new HBox) + cellFactory = _ => + new ListCell[Todo] { + val _text = StringProperty("") + val _graphic = ObjectProperty( + new HBox { + children = Seq( + new FontIcon { + iconSize = 10 + iconLiteral = IconLiteral.Gmi10k + }, + new Text { + text <== _text + } + ) + } + ) + + item.asOption.map( + _.fold("")(todo => s"${todo.id} - ${todo.content}") + ) --> _text + + graphic <== item.asOption.flatMap( + _.fold(emptyCell)(_ => _graphic) + ) + + } + + selectionModel().selectionMode = SelectionMode.Multiple + + contextMenu = new ContextMenu { + items ++= Seq( + new MenuItem { + text = "Add" + obsAction.useLazy(TodoListStore.Add("blah3")) --> store + }, + new MenuItem { + text = "Delete" + obsAction.useIterable(_ => + selectedItems + .map(todo => TodoListStore.Delete(todo.id)) + .toList + ) --> store + }, + new MenuItem { + text = "Edit" + } + ) + } + } + } + ) +} diff --git a/src/main/scala/nova/monadic_sfx/ui/controller/TodoController.scala b/src/main/scala/nova/monadic_sfx/ui/controller/TodoController.scala index a8c2695..7caaf9b 100644 --- a/src/main/scala/nova/monadic_sfx/ui/controller/TodoController.scala +++ b/src/main/scala/nova/monadic_sfx/ui/controller/TodoController.scala @@ -12,13 +12,13 @@ import nova.monadic_sfx.implicits.JFXButton import nova.monadic_sfx.implicits.JFXListView import nova.monadic_sfx.implicits.JFXTextArea import nova.monadic_sfx.implicits.JFXTextField -import nova.monadic_sfx.ui.components.todo.TodoListComponent import scalafx.collections.ObservableBuffer import scalafx.scene.control.Label import scalafx.scene.layout.HBox import scalafx.scene.paint.Color +import nova.monadic_sfx.ui.components.todo.TodoListComponentOld -class TodoController(todoListComponent: TodoListComponent) { +class TodoController(todoListComponent: TodoListComponentOld) { import AnimFX._ def root = new HBox { diff --git a/src/main/scala/nova/monadic_sfx/util/reactive/Middlewares.scala b/src/main/scala/nova/monadic_sfx/util/reactive/Middlewares.scala new file mode 100644 index 0000000..6c1a482 --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/util/reactive/Middlewares.scala @@ -0,0 +1,27 @@ +package nova.monadic_sfx.util.reactive + +import io.odin.Logger +import monix.reactive.Observable +import monix.bio.Task +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 +// } +object Middlewares { + 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")) + } + + 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 ")) + } +} diff --git a/src/main/scala/nova/monadic_sfx/util/reactive/MonixProSubject.scala b/src/main/scala/nova/monadic_sfx/util/reactive/MonixProSubject.scala new file mode 100644 index 0000000..6627235 --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/util/reactive/MonixProSubject.scala @@ -0,0 +1,22 @@ +package nova.monadic_sfx.util.reactive + +import monix.execution.Cancelable +import monix.reactive.Observable +import monix.reactive.Observer +import scala.concurrent.Future +import monix.reactive.observers.Subscriber +import monix.execution.Ack + +object MonixProSubject { + def from[I, O]( + observer: Observer[I], + observable: Observable[O] + ): MonixProSubject[I, O] = + new Observable[O] with Observer[I] { + override def onNext(elem: I): Future[Ack] = observer.onNext(elem) + override def onError(ex: Throwable): Unit = observer.onError(ex) + override def onComplete(): Unit = observer.onComplete() + override def unsafeSubscribeFn(subscriber: Subscriber[O]): Cancelable = + observable.unsafeSubscribeFn(subscriber) + } +} diff --git a/src/main/scala/nova/monadic_sfx/util/reactive/Reducer.scala b/src/main/scala/nova/monadic_sfx/util/reactive/Reducer.scala new file mode 100644 index 0000000..73eb18c --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/util/reactive/Reducer.scala @@ -0,0 +1,39 @@ +package nova.monadic_sfx.util.reactive + +import monix.reactive.ObservableLike +import cats.implicits._ +import monix.reactive.Observable + +object Reducer { + + /** + * Creates a Reducer which yields a new State, as-well as an Observable of Effects + * Effects are Actions which will be executed after the Action that caused them to occur. + * This is accomplished by subscribing to the Effects Observable within the stores scan loop. + * + * CAUTION: There is currently a bug which causes the Effect-States to emit, + * before the State of the action that caused the effects is emitted. + * However, this only effects immediate emissions of the Effects Observable, delayed emissions should be fine. + * @param f The Reducing Function returning the (Model, Effects) tuple. + */ + def withEffects[F[_]: ObservableLike, A, M]( + f: (M, A) => (M, F[A]) + ): Reducer[A, M] = (s: M, a: A) => f(s, a).map(ObservableLike[F].apply) + + /** + * Creates a reducer which just transforms the state, without additional effects. + */ + def apply[A, M](f: (M, A) => M): Reducer[A, M] = + (s: M, a: A) => f(s, a) -> Observable.empty + + /** + * Creates a Reducer with an optional effect. + */ + def withOptionalEffects[F[_]: ObservableLike, A, M]( + f: (M, A) => (M, Option[F[A]]) + ): Reducer[A, M] = + (s: M, a: A) => + f(s, a).map( + _.fold[Observable[A]](Observable.empty)(ObservableLike[F].apply) + ) +} diff --git a/src/main/scala/nova/monadic_sfx/util/reactive/Store.scala b/src/main/scala/nova/monadic_sfx/util/reactive/Store.scala new file mode 100644 index 0000000..1a34b04 --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/util/reactive/Store.scala @@ -0,0 +1,73 @@ +package nova.monadic_sfx.util.reactive + +import monix.reactive.OverflowStrategy +import monix.reactive.subjects.ConcurrentSubject +import monix.reactive.Observable +import monix.bio.Task +import cats.effect.Sync +import monix.execution.Scheduler + +object Store { + def createL[A, M]( + initialAction: A, + initialState: M, + reducer: Reducer[A, M], + middlewares: Seq[Middleware[A, M]] = Seq.empty, + overflowStrategy: OverflowStrategy.Synchronous[A] = + OverflowStrategy.DropOld(50) + ) = + 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) + + effects.subscribe(subject.onNext _) + + action -> newState + } + } + + val obs = subject + .scan[(A, M)](initialAction -> initialState)(fold) + .behavior(initialAction -> initialState) + .refCount + + val res = middlewares.view.reverse.foldLeft(obs) { + case (obs, middleware) => middleware(obs) + } + + MonixProSubject.from( + subject, + res + ) + } + } + + def create[F[_], A, 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] + + val fold: ((A, M), A) => (A, M) = { + case ((_, state), action) => { + val (newState, effects) = reducer(state, action) + + effects.subscribe(subject.onNext _) + + action -> newState + } + } + + subject + .scan[(A, M)](initialAction -> initialState)(fold) + .behavior(initialAction -> initialState) + .refCount + } + +} diff --git a/src/main/scala/nova/monadic_sfx/util/reactive/package.scala b/src/main/scala/nova/monadic_sfx/util/reactive/package.scala new file mode 100644 index 0000000..6199457 --- /dev/null +++ b/src/main/scala/nova/monadic_sfx/util/reactive/package.scala @@ -0,0 +1,17 @@ +package nova.monadic_sfx.util + +import monix.reactive.Observable +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)] + + /** + * A Function that applies an Action onto the Stores current state. + * @param reducer The reducing function + * @tparam A The Action Type + * @tparam M The Model Type + */ + type Reducer[A, M] = (M, A) => (M, Observable[A]) +}