Rohan Sircar
3 years ago
17 changed files with 939 additions and 632 deletions
-
1build.sbt
-
8src/main/scala/nova/monadic_sfx/Main.scala
-
106src/main/scala/nova/monadic_sfx/MainApp.scala
-
37src/main/scala/nova/monadic_sfx/implicits/ActionObservable.scala
-
3src/main/scala/nova/monadic_sfx/implicits/JFXButton.scala
-
133src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala
-
9src/main/scala/nova/monadic_sfx/implicits/MenuItem.scala
-
556src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListComponent.scala
-
378src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListComponentOld.scala
-
67src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListStore.scala
-
91src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListView.scala
-
4src/main/scala/nova/monadic_sfx/ui/controller/TodoController.scala
-
27src/main/scala/nova/monadic_sfx/util/reactive/Middlewares.scala
-
22src/main/scala/nova/monadic_sfx/util/reactive/MonixProSubject.scala
-
39src/main/scala/nova/monadic_sfx/util/reactive/Reducer.scala
-
73src/main/scala/nova/monadic_sfx/util/reactive/Store.scala
-
17src/main/scala/nova/monadic_sfx/util/reactive/package.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)))) |
||||
|
} |
@ -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()) |
||||
|
} |
@ -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 |
|
||||
// } |
|
||||
// } |
|
@ -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 |
||||
|
// } |
||||
|
// } |
@ -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)) |
||||
|
) |
||||
|
} |
@ -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" |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
) |
||||
|
} |
@ -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 ")) |
||||
|
} |
||||
|
} |
@ -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) |
||||
|
} |
||||
|
} |
@ -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) |
||||
|
) |
||||
|
} |
@ -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 |
||||
|
} |
||||
|
|
||||
|
} |
@ -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]) |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue