Cleanup and refactoring

This commit is contained in:
Rohan Sircar 2020-12-16 17:32:19 +05:30
parent b988ad267e
commit 8f1fe0cc84
17 changed files with 939 additions and 632 deletions

View File

@ -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",

View File

@ -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 ()

View File

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

View File

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

View File

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

View File

@ -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 observableChange[J1 >: J]()
// : Observable[(ObservableValue[T, J], J1, J1)] = {
// import monix.execution.cancelables.SingleAssignCancelable
// Observable.create(OverflowStrategy.Unbounded) { sub =>
// val c = SingleAssignCancelable()
def <--(obs: Observable[T])(implicit s: Scheduler) = {
obs.doOnNext(v => me.Task(prop.value = v)).subscribe()
}
// val canc = prop.onChange((a, b, c) => sub.onNext((a, b, c)))
def asOption = prop.map(Option(_))
// c := Cancelable(() => canc.cancel())
// c
// }
// }
// }
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 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
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

@ -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])
}