Modularized code

Moved all resource creation code to modules (thin cake pattern)
Cleanup
Added test actor running on fx thread
This commit is contained in:
Rohan Sircar 2020-09-01 20:26:12 +05:30
parent 02c4e2f2f3
commit b6823aebfa
19 changed files with 467 additions and 375 deletions

View File

@ -1,5 +1,5 @@
javafx-dispatcher {
type = "Dispatcher"
executor = "akka.dispatch.gui.JavaFXEventThreadExecutorServiceConfigurator"
executor = "nova.monadic_sfx.executors.JavaFXEventThreadExecutorServiceConfigurator"
throughput = 1
}

View File

@ -1,60 +1,22 @@
package nova.monadic_sfx
import scalafx.application.JFXApp
import scalafx.application.JFXApp.PrimaryStage
import monix.eval.Task
import monix.execution.Scheduler
// import sttp.client.asynchttpclient.monix.AsyncHttpClientMonixBackend
// import sttp.client._
// import sttp.client.circe._
// import io.circe.generic.auto._
import scala.util.Failure
import scala.util.Success
import akka.{actor => classic}
import nova.monadic_sfx.executors._
import cats.effect.Resource
import nova.monadic_sfx.models._
import nova.monadic_sfx.ui.DefaultUI
import nova.monadic_sfx.http.Backend
import nova.monadic_sfx.modules.MainModule
import scalafx.stage.Stage
import scalafx.scene.layout.FlowPane
import nova.monadic_sfx.pages.LoginPage
import scala.concurrent.duration._
import sttp.client.asynchttpclient.monix.AsyncHttpClientMonixBackend
import scalafx.Includes._
import scala.concurrent.Await
import akka.actor.typed.scaladsl.adapter._
import scalafx.application.Platform
import io.odin.syntax._
// import io.odin._
import io.odin.monix._
import nova.monadic_sfx.pages.HomePage
import monix.execution.Callback
import monix.eval.TaskApp
import cats.effect.ExitCode
import scalafx.scene.layout.HBox
import scalafx.scene.Scene
import cats.implicits._
import cats.effect.Clock
import cats.effect.Sync
object Main extends TaskApp {
object Main extends MainModule with TaskApp {
// lazy val schedulers: Schedulers = new Schedulers()
// override implicit def scheduler: Scheduler = schedulers.fx
override def run(args: List[String]): Task[ExitCode] = {
// val logger = consoleLogger().withAsyncUnsafe()
// lazy val backendTask = AsyncHttpClientMonixBackend()
// lazy val actorSystemTask = Task {
// classic.ActorSystem(
// name = "FXActorSystem"
// )
// }
// implicit lazy val FSync = Sync[Task]
// implicit lazy val FClock = Clock[Task]
// val startTime = Task.clock
// .monotonic(scala.concurrent.duration.MILLISECONDS)
// .map(Duration.fromNanos(_))
@ -62,108 +24,13 @@ object Main extends TaskApp {
// clock <- Resource.liftF(Task(Task.clock))
logger <- consoleLogger().withAsync()
backend <- AsyncHttpClientMonixBackend.resource()
actorSystem <-
Resource.make(logger.info("Creating Actor System") >> Task {
classic.ActorSystem(
name = "FXActorSystem"
)
})(sys =>
logger.info("Shutting down actor system") >> Task.fromFuture(
sys.terminate()
) >> logger.info("Actor System terminated")
)
// _ <- Resource.liftF(logger.info(Thread.currentThread().getName()))
fxApp <- Resource.make(logger.info("Creating FX Application") >> Task {
// val appStage = makePrimaryStage(backend, actorSystem)
// stage = appStage
// val stage2 = new PrimaryStage {
// scene = new Scene(new HBox())
// }
val app: JFXApp = new JFXApp {
lazy val schedulers: Schedulers = new Schedulers()
implicit lazy val defaultScheduler: Scheduler = schedulers.fx
val application =
for {
appStage <- logger.info("Inside JFX application stage") >> Task(
makePrimaryStage(backend, actorSystem)
)
_ <- Task { stage = appStage }
_ <- Task.sleep(2.seconds)
loginScene <- LoginPage(appStage, backend, actorSystem)
_ <- Task {
// appStage.maximized = true
appStage.height = 800
appStage.width = 800
appStage
.scene()
.setRoot(
loginScene
)
}
} yield ()
application.timed.runAsync(
new Callback[Throwable, (FiniteDuration, Unit)] {
override def onSuccess(value: (FiniteDuration, Unit)): Unit = {
val (duration, _) = value
println(
s"Application started successfully in ${duration.toSeconds} seconds"
)
}
override def onError(e: Throwable): Unit = {
println("Application start failed. Reason -")
e.printStackTrace()
}
}
)
override def stopApp() = {
Platform.exit()
// System.exit(0)
}
}
app
})(app => logger.info("Stopping FX Application") >> Task(app.stopApp()))
// _ <- Resource.liftF(Task.unit.executeOn(defaultScheduler))
_ <- Resource.liftF(logger.info(Thread.currentThread().getName()))
actorSystem <- actorResource(logger)
reqs <- Resource.liftF(Task(requesters(backend, actorSystem)))
schedulers <- Resource.liftF(Task(new Schedulers()))
fxApp <- fxAppResource(logger, backend, actorSystem, reqs, schedulers)
} yield (fxApp)
// >> logger.info("test")
appResource
.use(fxApp => Task(fxApp.main(args.toArray)))
.as(ExitCode.Success)
}
def makePrimaryStage(
backend: AppTypes.HttpBackend,
actorSystem: classic.ActorSystem
) = {
new PrimaryStage {
scene = new DefaultUI().scene
// onCloseRequest = () => {
// val f2 = actorSystem.terminate()
// val f1 = backend.close().runToFuture
// println("Closing backend")
// Await.result(f1, 3.seconds)
// println("Closing actor system")
// println(Thread.currentThread().getName())
// Await.result(f2, 3.seconds)
// println("Actor system closed")
// Platform.exit()
// System.exit(0)
// }
}
}
}
// class MyFxApp extends javafx.application.Application {
// override def start(stage: javafx.stage.Stage): Unit = {
// stage.show()
// }
// }

View File

@ -0,0 +1,7 @@
package nova.monadic_sfx
import nova.monadic_sfx.actors.ActorModule
import nova.monadic_sfx.ui.UiModule
import nova.monadic_sfx.http.HttpModule
trait MainModule extends ActorModule with UiModule with HttpModule

View File

@ -0,0 +1,49 @@
package nova.monadic_sfx.actors
import io.odin.Logger
import monix.eval.Task
import cats.effect.Resource
import akka.actor._
import akka.actor.typed.scaladsl.adapter._
import akka.actor.typed.scaladsl.Behaviors
import com.softwaremill.macwire._
import akka.actor.typed.Behavior
import akka.actor.typed.DispatcherSelector
trait ActorModule {
def actorResource(logger: Logger[Task]): Resource[Task, ActorSystem] =
Resource.make(logger.info("Creating Actor System") >> Task {
ActorSystem(
name = "FXActorSystem"
)
})(sys =>
logger.info("Shutting down actor system") >> Task.fromFuture(
sys.terminate()
) >> logger.info("Actor System terminated")
)
def testActor(
system: ActorSystem
): akka.actor.typed.ActorRef[Counter.Command] = {
val behaviour: Behavior[Counter.Command] =
Behaviors.setup(context => wire[Counter])
system.spawn(
behaviour,
"CounterActor",
DispatcherSelector.fromConfig("javafx-dispatcher")
)
}
def testActorL(
system: ActorSystem
): Task[akka.actor.typed.ActorRef[Counter.Command]] =
Task {
val behaviour: Behavior[Counter.Command] =
Behaviors.setup(context => wire[Counter])
system.spawn(
behaviour,
"CounterActor",
DispatcherSelector.fromConfig("javafx-dispatcher")
)
}
}

View File

@ -0,0 +1,34 @@
package nova.monadic_sfx.actors
import akka.actor.typed._
import akka.actor.typed.scaladsl._
object Counter {
sealed trait Command
case object Increment extends Command
final case class GetValue(replyTo: ActorRef[Value]) extends Command
final case class Value(n: Int)
def apply(): Behavior[Command] = {
Behaviors.setup(context => new Counter(context))
}
}
class Counter(context: ActorContext[Counter.Command])
extends AbstractBehavior[Counter.Command](context) {
import Counter._
private var n = 0
override def onMessage(msg: Command): Behavior[Counter.Command] = {
msg match {
case Increment =>
n += 1
context.log.debug("Incremented counter to [{}]", n)
this
case GetValue(replyTo) =>
replyTo ! Value(n)
Behaviors.stopped
}
}
}

View File

@ -0,0 +1,5 @@
package nova.monadic_sfx.executors
trait ExecutorsModule {
lazy val schedulers = new Schedulers()
}

View File

@ -20,7 +20,7 @@ import scala.concurrent.ExecutionContext
import java.util.concurrent.Executor
// First we wrap invokeLater/runLater as an ExecutorService
abstract class GUIExecutorService extends AbstractExecutorService {
trait GUIExecutorService extends AbstractExecutorService {
def execute(command: Runnable): Unit
def shutdown(): Unit = ()

View File

@ -1,10 +0,0 @@
package nova.monadic_sfx.http
import sttp.client.asynchttpclient.monix.AsyncHttpClientMonixBackend
import nova.monadic_sfx.AppTypes
object Backend {
val backend = AsyncHttpClientMonixBackend
.resource()
def apply() = { backend }
}

View File

@ -0,0 +1,28 @@
package nova.monadic_sfx.http
import nova.monadic_sfx.http.requests.DummyRequest
import nova.monadic_sfx.AppTypes
trait HttpModule {
def requesters(
backend: AppTypes.HttpBackend,
system: akka.actor.ActorSystem
): Requesters = {
import com.softwaremill.macwire._
val dummyRequester = wire[DummyRequest]
wire[Requesters]
}
}
class Requesters(val dummyRequester: DummyRequest)
// object Requesters {
// def apply(
// backend: AppTypes.HttpBackend,
// system: akka.actor.ActorSystem
// ): Requesters = {
// import com.softwaremill.macwire._
// val dummyRequester = wire[DummyRequest]
// wire[Requesters]
// }
// }

View File

@ -8,20 +8,36 @@ import sttp.client._
import sttp.client.circe._
import io.circe.generic.auto._
import nova.monadic_sfx.models._
import cats.data.EitherT
class DummyRequest(backend: HttpBackend) extends AppTypes {
private implicit val _backend = backend
def send() = {
Task
.suspend(
(for {
.suspend {
for {
req <-
basicRequest
.get(uri"https://httpbin.org/get")
.response(asJson[HttpBinResponse])
.send()
} yield println(req)) >>
Task(println(Thread.currentThread().getName()))
} yield (req)
}
}
def test() = {
for {
res <- send()
res3 <- Task { res.body }
res2 <- Task {
res3.fold(
err => {
err.toString()
},
value => value.toString()
)
}
} yield (res3)
}
}

View File

@ -1,79 +0,0 @@
package nova.monadic_sfx.modules
import nova.monadic_sfx.executors.Schedulers
import monix.execution.Scheduler
import nova.monadic_sfx.http.requests.DummyRequest
import monix.eval.Task
import akka.{actor => classic}
import cats.effect.Resource
import nova.monadic_sfx.AppTypes
trait MainModule {
import com.softwaremill.macwire._
// val schedulers: Schedulers = new Schedulers()
// implicit val defaultScheduler: Scheduler = schedulers.fx
// val program =
// for {
// backend <- Backend()
// fxActorSystem <-
// Resource
// .make(Task {
// classic.ActorSystem(
// name = "FXActorSystem"
// )
// })(sys => Task(sys.terminate()))
// } yield {
// val dummyRequester = wire[DummyRequest]
// dummyRequester.send()
// }
// val program = Backend().use(backend =>
// Resource
// .make(Task {
// classic.ActorSystem(
// name = "FXActorSystem"
// )
// })(sys => Task(sys.terminate()))
// .use { implicit system =>
// // system.spa
// // system.typed
// // val javaFxActor = system.actorOf(
// // Props[JavaFxActor]().withDispatcher("javafx-dispatcher"),
// // "javaFxActor"
// // )
// // val swingActor = system.actorOf(
// // Props[SwingActor]().withDispatcher("swing-dispatcher"),
// // "swingActor"
// // )
// Task(new DummyRequest(backend)) >>
// Task.unit
// }
// )
def schedulers: Schedulers
def defaultScheduler: Scheduler
def backendTask: Task[AppTypes.HttpBackend]
def actorSystemTask: Task[classic.ActorSystem]
def deps =
for {
backend <- backendTask
actorSystem <- actorSystemTask
dummyRequesterTask <- Task {
wireDeps(backend, actorSystem)
}
} yield dummyRequesterTask
def wireDeps(
backend: AppTypes.HttpBackend,
system: classic.ActorSystem
): DummyRequest = {
wire[DummyRequest]
// new DummyRequest(backend)
}
}

View File

@ -1,39 +0,0 @@
package nova.monadic_sfx.pages
import nova.monadic_sfx.AppTypes
import scalafx.scene.control.TextField
import scalafx.scene.control._
import scalafx.scene.layout.VBox
import scalafx.scene.Node
import scalafx.Includes._
import scalafx.scene.layout.HBox
import scalafx.scene.text.Text
import scalafx.scene.Parent
import scalafx.application.JFXApp.PrimaryStage
class HomePage(
backend: AppTypes.HttpBackend,
system: akka.actor.ActorSystem,
onLogout: () => Unit
) {
private lazy val root = new HBox {
children = List(
new Text {
text = "hello"
},
new Button {
text = "logout"
onAction = () => onLogout()
}
)
}
def render = root
}
object HomePage {
def apply(
backend: AppTypes.HttpBackend,
system: akka.actor.ActorSystem,
onLogout: () => Unit
): Parent =
new HomePage(backend, system, onLogout).render
}

View File

@ -1,102 +0,0 @@
package nova.monadic_sfx.pages
import nova.monadic_sfx.AppTypes
import scalafx.scene.control.TextField
import scalafx.scene.control._
import scalafx.scene.layout.VBox
import scalafx.scene.Node
import scalafx.Includes._
import scalafx.scene.Parent
import scalafx.application.JFXApp.PrimaryStage
import nova.monadic_sfx.http.requests.DummyRequest
import monix.eval.Task
import monix.execution.Scheduler
// import io.odin.syntax._
// import _root_.monix.eval.Task
// import io.odin.monix._
// import javafx.beans.property.ObjectProperty
// import javafx.event.{ActionEvent, EventHandler}
class LoginPage(
appStage: PrimaryStage,
backend: AppTypes.HttpBackend,
system: akka.actor.ActorSystem
) {
val dummyRequester = new DummyRequest(backend)
//pure function callbacks, but with side effects still
private def onLogout(stage: PrimaryStage) =
for {
_ <- Task { println("logging out") }
root <- render
_ <- Task(stage.scene().setRoot(root))
} yield ()
private def onLogin(stage: PrimaryStage) =
Task.deferAction { implicit Scheduler =>
for {
_ <- Task(println("logging in"))
root <- Task {
stage
.scene()
.setRoot(
HomePage(
backend,
system,
() => runFxTask(onLogout(appStage))
)
)
}
} yield ()
}
private lazy val root = Task.deferAction(implicit scheduler =>
Task {
new VBox {
children = Seq(
new Label {
text = "username"
},
new TextField(),
new Label {
text = "password"
},
new TextField(),
new Button {
text = "Login"
onAction = () =>
runFxTask {
Task
.parZip2(
dummyRequester
.send(),
// .executeOn(Scheduler.global)
onLogin(appStage)
)
}
}
)
}
}
)
def render: Task[Parent] = root
/**
* Implicitly runs monix task as fire and forget. \
* For use in ScalaFX callbacks.
*
* @param task
* @param s
*/
def runFxTask[T](task: => Task[T])(implicit s: Scheduler) = {
task.runAsyncAndForget
}
}
object LoginPage {
def apply(
appStage: PrimaryStage,
backend: AppTypes.HttpBackend,
system: akka.actor.ActorSystem
) = new LoginPage(appStage, backend, system).render
}

View File

@ -0,0 +1,90 @@
package nova.monadic_sfx.ui
import scalafx.application.JFXApp
import nova.monadic_sfx.executors.Schedulers
import monix.execution.Scheduler
import monix.eval.Task
import nova.monadic_sfx.screens.LoginScreen
import nova.monadic_sfx.AppTypes
import scalafx.application.Platform
import scala.concurrent.duration._
import io.odin.Logger
import monix.execution.Callback
import com.softwaremill.macwire._
import nova.monadic_sfx.http.Requesters
import akka.actor._
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.adapter._
import akka.actor.typed.scaladsl.Behaviors
import nova.monadic_sfx.actors.Counter
import akka.actor.typed.DispatcherSelector
class MyFxApp(
logger: Logger[Task],
backend: AppTypes.HttpBackend,
actorSystem: akka.actor.ActorSystem,
requesters: Requesters,
schedulers: Schedulers
) extends JFXApp {
implicit lazy val defaultScheduler: Scheduler = schedulers.fx
lazy val application =
for {
appStage <- Task(
UiModule.makePrimaryStage(backend, actorSystem)
)
// _ <- Task {
// val counterActor = testActor(actorSystem)
// counterActor ! (Counter.Increment)
// }
_ <- Task { stage = appStage }
_ <- Task.sleep(2.seconds)
loginScene <- wire[LoginScreen].render
_ <- Task {
// appStage.maximized = true
appStage.height = 800
appStage.width = 800
appStage
.scene()
.setRoot(
loginScene
)
}
} yield ()
def testActor(
system: ActorSystem
): akka.actor.typed.ActorRef[Counter.Command] = {
val behaviour: Behavior[Counter.Command] =
Behaviors.setup(context => wire[Counter])
system.spawn(
behaviour,
"CounterActor",
DispatcherSelector.fromConfig("javafx-dispatcher")
)
}
application.timed.runAsync(
new Callback[Throwable, (FiniteDuration, Unit)] {
override def onSuccess(value: (FiniteDuration, Unit)): Unit = {
val (duration, _) = value
println(
s"Application started successfully in ${duration.toSeconds} seconds"
)
}
override def onError(e: Throwable): Unit = {
println("Application start failed. Reason -")
e.printStackTrace()
}
}
)
override def stopApp() = {
Platform.exit()
}
}

View File

@ -0,0 +1,37 @@
package nova.monadic_sfx.ui
import scalafx.application.JFXApp
import monix.eval.Task
import nova.monadic_sfx.AppTypes
import scalafx.application.JFXApp.PrimaryStage
import io.odin.Logger
import cats.effect.Resource
import com.softwaremill.macwire._
import nova.monadic_sfx.http.Requesters
import nova.monadic_sfx.executors.Schedulers
trait UiModule {
def fxAppResource(
logger: Logger[Task],
backend: AppTypes.HttpBackend,
actorSystem: akka.actor.ActorSystem,
requesters: Requesters,
schedulers: Schedulers
): Resource[Task, JFXApp] =
Resource.make(logger.info("Creating FX Application") >> Task {
val app: JFXApp = wire[MyFxApp]
app
})(app => logger.info("Stopping FX Application") >> Task(app.stopApp()))
}
object UiModule {
def makePrimaryStage(
backend: AppTypes.HttpBackend,
actorSystem: akka.actor.ActorSystem
) = {
new PrimaryStage {
scene = new DefaultUI().scene
}
}
}

View File

@ -0,0 +1,45 @@
package nova.monadic_sfx.screens
import nova.monadic_sfx.AppTypes
import scalafx.scene.control.TextField
import scalafx.scene.control._
import scalafx.scene.layout.VBox
import scalafx.scene.Node
import scalafx.Includes._
import scalafx.scene.layout.HBox
import scalafx.scene.text.Text
import scalafx.scene.Parent
import scalafx.application.JFXApp.PrimaryStage
import monix.eval.Task
import nova.monadic_sfx.util.Action
class HomeScreen(
backend: AppTypes.HttpBackend,
system: akka.actor.ActorSystem,
onLogout: () => Task[Unit]
) {
private lazy val root = Task.deferAction { implicit s =>
Task {
new HBox {
children = List(
new Text {
text = "hello"
},
new Button {
text = "logout"
onAction = () => Action.asyncT(onLogout())
}
)
}
}
}
def render = root
}
object HomeScreen {
def apply(
backend: AppTypes.HttpBackend,
system: akka.actor.ActorSystem,
onLogout: () => Task[Unit]
): Task[Parent] =
new HomeScreen(backend, system, onLogout).render
}

View File

@ -0,0 +1,108 @@
package nova.monadic_sfx.screens
import nova.monadic_sfx.AppTypes
import scalafx.scene.control.TextField
import scalafx.scene.control._
import scalafx.scene.layout.VBox
import scalafx.scene.Node
import scalafx.Includes._
import scalafx.scene.Parent
import scalafx.application.JFXApp.PrimaryStage
import nova.monadic_sfx.http.requests.DummyRequest
import monix.eval.Task
import monix.execution.Scheduler
import cats.effect.Effect
import cats.effect.implicits._
import nova.monadic_sfx.util.Action
import io.odin.Logger
import nova.monadic_sfx.http.Requesters
import sttp.client.Response
import nova.monadic_sfx.models.HttpBinResponse
import sttp.client.ResponseError
import nova.monadic_sfx.executors.Schedulers
import nova.monadic_sfx.ui.screens.Screen
// import io.odin.syntax._
// import _root_.monix.eval.Task
// import io.odin.monix._
// import javafx.beans.property.ObjectProperty
// import javafx.event.{ActionEvent, EventHandler}
class LoginScreen(
override protected val appStage: PrimaryStage,
logger: Logger[Task],
backend: AppTypes.HttpBackend,
system: akka.actor.ActorSystem,
requesters: Requesters,
schedulers: Schedulers
) extends Screen {
val dummyRequester: DummyRequest = requesters.dummyRequester
//pure function callbacks, but with side effects still
// lazy val hs = {
// import com.softwaremill.macwire._
// lazy val action = () => onLogout(appStage)
// wire[HomeScreen]
// }
private def onLogout(stage: PrimaryStage) =
for {
_ <- logger.info("Logging out")
root <- render
_ <- changeRootL(root)
} yield ()
private def onLogin(stage: PrimaryStage) =
for {
_ <- logger.info("Logging in")
homeScreen <- HomeScreen(
backend,
system,
() => onLogout(appStage)
)
_ <- Task { stage.scene().setRoot(homeScreen) }
} yield ()
private lazy val root = Task.deferAction(implicit scheduler =>
Task {
new VBox {
children = Seq(
new Label {
text = "username"
},
new TextField(),
new Label {
text = "password"
},
new TextField(),
new Button {
text = "Login"
onAction = () =>
Action.asyncT {
Task
.parSequence(
List(
testRequest,
// .executeOn(Scheduler.global)
onLogin(appStage)
)
)
}
}
)
}
}
)
def render: Task[Parent] = root
val testRequest = for {
res <- dummyRequester.send()
_ <- logger.info(res.body.toString())
} yield ()
}
// object LoginScreen {
// def apply(
// appStage: PrimaryStage,
// backend: AppTypes.HttpBackend,
// system: akka.actor.ActorSystem
// ) = new LoginScreen(appStage, backend, system).render
// }

View File

@ -0,0 +1,13 @@
package nova.monadic_sfx.ui.screens
import scalafx.application.JFXApp.PrimaryStage
import scalafx.scene.Parent
import monix.eval.Task
trait Screen {
protected def appStage: PrimaryStage
def changeRoot(root: Parent): Unit = {
appStage.scene().setRoot(root)
}
def changeRootL(root: Parent): Task[Unit] = Task(changeRoot(root))
}

View File

@ -0,0 +1,23 @@
package nova.monadic_sfx.util
import monix.eval.Task
import monix.execution.Scheduler
import cats.effect.Effect
import cats.effect.implicits._
object Action {
/**
* Implicitly runs monix task as fire and forget. \
* For use in ScalaFX callbacks.
*
* @param task
* @param s
*/
def asyncT[T](task: => Task[T])(implicit s: Scheduler): Unit = {
task.runAsyncAndForget
}
def asyncF[F[_]: Effect, T](cb: => F[T]): Unit =
cb.toIO.unsafeRunAsyncAndForget()
}