Browse Source

Cleanup and refactoring

master
Rohan Sircar 3 years ago
parent
commit
8f1fe0cc84
  1. 1
      build.sbt
  2. 8
      src/main/scala/nova/monadic_sfx/Main.scala
  3. 106
      src/main/scala/nova/monadic_sfx/MainApp.scala
  4. 37
      src/main/scala/nova/monadic_sfx/implicits/ActionObservable.scala
  5. 3
      src/main/scala/nova/monadic_sfx/implicits/JFXButton.scala
  6. 133
      src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala
  7. 9
      src/main/scala/nova/monadic_sfx/implicits/MenuItem.scala
  8. 556
      src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListComponent.scala
  9. 378
      src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListComponentOld.scala
  10. 67
      src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListStore.scala
  11. 91
      src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListView.scala
  12. 4
      src/main/scala/nova/monadic_sfx/ui/controller/TodoController.scala
  13. 27
      src/main/scala/nova/monadic_sfx/util/reactive/Middlewares.scala
  14. 22
      src/main/scala/nova/monadic_sfx/util/reactive/MonixProSubject.scala
  15. 39
      src/main/scala/nova/monadic_sfx/util/reactive/Reducer.scala
  16. 73
      src/main/scala/nova/monadic_sfx/util/reactive/Store.scala
  17. 17
      src/main/scala/nova/monadic_sfx/util/reactive/package.scala

1
build.sbt

@ -25,6 +25,7 @@ libraryDependencies ++= Seq(
"com.softwaremill.sttp.client" %% "circe" % "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" %% "async-http-client-backend-monix" % "2.2.9",
"com.softwaremill.sttp.client" %% "httpclient-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.github.valskalla" %% "odin-monix" % "0.8.1",
"com.typesafe.akka" %% "akka-actor-typed" % "2.6.8", "com.typesafe.akka" %% "akka-actor-typed" % "2.6.8",
"com.softwaremill.macwire" %% "util" % "2.3.7", "com.softwaremill.macwire" %% "util" % "2.3.7",

8
src/main/scala/nova/monadic_sfx/Main.scala

@ -23,10 +23,10 @@ object Main extends MainModule with BIOApp {
"application.log" "application.log"
).withAsync() ).withAsync()
schedulers = new Schedulers() 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) _ <- Resource.liftF(wire[MainApp].program)
} yield () } yield ()

106
src/main/scala/nova/monadic_sfx/MainApp.scala

@ -3,15 +3,11 @@ package nova.monadic_sfx
import com.softwaremill.macwire._ import com.softwaremill.macwire._
import io.odin.Logger import io.odin.Logger
import monix.bio.Task import monix.bio.Task
import monix.catnap.ConcurrentChannel
import nova.monadic_sfx.executors.Schedulers import nova.monadic_sfx.executors.Schedulers
import nova.monadic_sfx.implicits.JFXButton import nova.monadic_sfx.implicits.JFXButton
import nova.monadic_sfx.implicits.JavaFXMonixObservables._ import nova.monadic_sfx.implicits.JavaFXMonixObservables._
import nova.monadic_sfx.ui.MyFxApp 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.ui.components.todo.TodoListView
import nova.monadic_sfx.util.IOUtils._
import org.gerweck.scalafx.util._ import org.gerweck.scalafx.util._
import scalafx.Includes._ import scalafx.Includes._
import scalafx.application.JFXApp.PrimaryStage import scalafx.application.JFXApp.PrimaryStage
@ -25,6 +21,7 @@ import scalafx.scene.control.TableView
import scalafx.scene.layout.HBox import scalafx.scene.layout.HBox
import scalafx.scene.paint.Color import scalafx.scene.paint.Color
import scalafx.scene.shape.Rectangle import scalafx.scene.shape.Rectangle
import nova.monadic_sfx.ui.components.todo.TodoListStore
class MainApp( class MainApp(
// spawnProtocol: ActorSystem[SpawnProtocol.Command], // spawnProtocol: ActorSystem[SpawnProtocol.Command],
@ -38,7 +35,7 @@ class MainApp(
lazy val addTodoObs = addTodoButton.observableAction() lazy val addTodoObs = addTodoButton.observableAction()
lazy val todoListView = TodoListView.defaultListView
// lazy val todoListView = TodoListView.defaultListView
lazy val _scene = new Scene { lazy val _scene = new Scene {
root = new HBox { root = new HBox {
@ -77,59 +74,66 @@ class MainApp(
// _ <- Task(fxApp.stage = stage) // _ <- Task(fxApp.stage = stage)
// .executeOn(schedulers.fx) // .executeOn(schedulers.fx)
// .delayExecution(2000.millis) // .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( _ <- logger.info(
s"Application started in ${(System.currentTimeMillis() - startTime) / 1000f} seconds" s"Application started in ${(System.currentTimeMillis() - startTime) / 1000f} seconds"
) )
_ <- fxAppFib.join _ <- fxAppFib.join
} yield () } 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 { 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 ()
} }

37
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))))
}

3
src/main/scala/nova/monadic_sfx/implicits/JFXButton.scala

@ -39,4 +39,7 @@ class JFXButton(
ripplerFill() = b ripplerFill() = b
} }
def obsAction =
new ActionObservableBuilder(this.observableAction())
} }

133
src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala

@ -19,6 +19,12 @@ import scalafx.beans.value.ObservableValue
import scalafx.collections.ObservableBuffer import scalafx.collections.ObservableBuffer
import scalafx.scene.Scene import scalafx.scene.Scene
import scalafx.scene.control.ButtonBase 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 { 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]) implicit final class BindObs2[A](private val prop: ObjectProperty[A])
extends AnyVal { extends AnyVal {
@ -107,6 +119,10 @@ object JavaFXMonixObservables {
// op(prop.observableChange().map(_._3)).runToFuture // 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) = { def <--(obs: Observable[A])(implicit s: Scheduler) = {
obs.doOnNext(v => me.Task(prop() = v)).subscribe() 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]( implicit final class ObjectPropertyObservableListExt[A](
private val prop: ObjectProperty[ObservableList[A]] private val prop: ObjectProperty[ObservableList[A]]
) extends AnyVal { ) 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( implicit final class OnActionObservable(
private val button: ButtonBase private val button: ButtonBase
) extends AnyVal { ) 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
}
}
}
} }

9
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())
}

556
src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListComponent.scala

@ -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<S, A> {
// fun dispatch(store: Store<S, A>, 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
// }
// }

378
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
// }
// }

67
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))
)
}

91
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"
}
)
}
}
}
)
}

4
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.JFXListView
import nova.monadic_sfx.implicits.JFXTextArea import nova.monadic_sfx.implicits.JFXTextArea
import nova.monadic_sfx.implicits.JFXTextField import nova.monadic_sfx.implicits.JFXTextField
import nova.monadic_sfx.ui.components.todo.TodoListComponent
import scalafx.collections.ObservableBuffer import scalafx.collections.ObservableBuffer
import scalafx.scene.control.Label import scalafx.scene.control.Label
import scalafx.scene.layout.HBox import scalafx.scene.layout.HBox
import scalafx.scene.paint.Color import scalafx.scene.paint.Color
import nova.monadic_sfx.ui.components.todo.TodoListComponentOld
class TodoController(todoListComponent: TodoListComponent) {
class TodoController(todoListComponent: TodoListComponentOld) {
import AnimFX._ import AnimFX._
def root = def root =
new HBox { new HBox {

27
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 "))
}
}

22
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)
}
}

39
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)
)
}

73
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
}
}

17
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])
}
Loading…
Cancel
Save