diff --git a/.projectile b/.projectile new file mode 100644 index 0000000..900fdcc --- /dev/null +++ b/.projectile @@ -0,0 +1 @@ +-/app/.yarn diff --git a/.projectile b/.projectile new file mode 100644 index 0000000..900fdcc --- /dev/null +++ b/.projectile @@ -0,0 +1 @@ +-/app/.yarn diff --git a/app/src/main/scala/mdr/pdb/app/Api.scala b/app/src/main/scala/mdr/pdb/app/Api.scala index ea46468..cdfc970 100644 --- a/app/src/main/scala/mdr/pdb/app/Api.scala +++ b/app/src/main/scala/mdr/pdb/app/Api.scala @@ -6,12 +6,14 @@ import sttp.tapir.DecodeResult import org.scalajs.dom import scala.concurrent.Future +import scala.concurrent.ExecutionContext trait Api: - def alive(): Future[DecodeResult[Either[Unit, String]]] + def isAlive(): Task[Boolean] object ApiLive: - def layer(base: Option[String]): ULayer[Api] = ZLayer.succeed(ApiLive(base)) + val layer: URLayer[AppConfig, Api] = + ((conf: AppConfig) => ApiLive(Some(conf.baseUrl + "api/"))).toLayer class ApiLive(base: Option[String]) extends Api with CustomTapir: private val backend = FetchBackend( @@ -22,5 +24,13 @@ ) private val baseUri = base.map(b => uri"${b}") private val aliveClient = toClient(Endpoints.alive, baseUri, backend) - override def alive(): Future[DecodeResult[Either[Unit, String]]] = - aliveClient(()) + override def isAlive(): Task[Boolean] = + ZIO.fromFuture(ec => + given ExecutionContext = ec + aliveClient(()).map { + case DecodeResult.Value(Right("ok")) => true + case _ => false + } recover { case _ => + false + } + ) diff --git a/.projectile b/.projectile new file mode 100644 index 0000000..900fdcc --- /dev/null +++ b/.projectile @@ -0,0 +1 @@ +-/app/.yarn diff --git a/app/src/main/scala/mdr/pdb/app/Api.scala b/app/src/main/scala/mdr/pdb/app/Api.scala index ea46468..cdfc970 100644 --- a/app/src/main/scala/mdr/pdb/app/Api.scala +++ b/app/src/main/scala/mdr/pdb/app/Api.scala @@ -6,12 +6,14 @@ import sttp.tapir.DecodeResult import org.scalajs.dom import scala.concurrent.Future +import scala.concurrent.ExecutionContext trait Api: - def alive(): Future[DecodeResult[Either[Unit, String]]] + def isAlive(): Task[Boolean] object ApiLive: - def layer(base: Option[String]): ULayer[Api] = ZLayer.succeed(ApiLive(base)) + val layer: URLayer[AppConfig, Api] = + ((conf: AppConfig) => ApiLive(Some(conf.baseUrl + "api/"))).toLayer class ApiLive(base: Option[String]) extends Api with CustomTapir: private val backend = FetchBackend( @@ -22,5 +24,13 @@ ) private val baseUri = base.map(b => uri"${b}") private val aliveClient = toClient(Endpoints.alive, baseUri, backend) - override def alive(): Future[DecodeResult[Either[Unit, String]]] = - aliveClient(()) + override def isAlive(): Task[Boolean] = + ZIO.fromFuture(ec => + given ExecutionContext = ec + aliveClient(()).map { + case DecodeResult.Value(Right("ok")) => true + case _ => false + } recover { case _ => + false + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/AppConfig.scala b/app/src/main/scala/mdr/pdb/app/AppConfig.scala new file mode 100644 index 0000000..7d0f86b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/AppConfig.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app + +import zio.* +import scala.scalajs.js +import scala.scalajs.js.annotation.JSImport + +@js.native +trait JSAppConfig extends js.Object { + def baseUrl: String = js.native + def onlineCheckMs: js.UndefOr[Double] = js.native +} + +case class AppConfig( + baseUrl: String, + onlineCheckMs: Option[Int] +) + +object AppConfig { + @js.native + @JSImport("website-config/conf.js", JSImport.Default) + object config extends JSAppConfig + + val layer: ULayer[AppConfig] = + ZLayer.succeed( + AppConfig( + baseUrl = config.baseUrl, + onlineCheckMs = config.onlineCheckMs.toOption.map(_.toInt) + ) + ) +} diff --git a/.projectile b/.projectile new file mode 100644 index 0000000..900fdcc --- /dev/null +++ b/.projectile @@ -0,0 +1 @@ +-/app/.yarn diff --git a/app/src/main/scala/mdr/pdb/app/Api.scala b/app/src/main/scala/mdr/pdb/app/Api.scala index ea46468..cdfc970 100644 --- a/app/src/main/scala/mdr/pdb/app/Api.scala +++ b/app/src/main/scala/mdr/pdb/app/Api.scala @@ -6,12 +6,14 @@ import sttp.tapir.DecodeResult import org.scalajs.dom import scala.concurrent.Future +import scala.concurrent.ExecutionContext trait Api: - def alive(): Future[DecodeResult[Either[Unit, String]]] + def isAlive(): Task[Boolean] object ApiLive: - def layer(base: Option[String]): ULayer[Api] = ZLayer.succeed(ApiLive(base)) + val layer: URLayer[AppConfig, Api] = + ((conf: AppConfig) => ApiLive(Some(conf.baseUrl + "api/"))).toLayer class ApiLive(base: Option[String]) extends Api with CustomTapir: private val backend = FetchBackend( @@ -22,5 +24,13 @@ ) private val baseUri = base.map(b => uri"${b}") private val aliveClient = toClient(Endpoints.alive, baseUri, backend) - override def alive(): Future[DecodeResult[Either[Unit, String]]] = - aliveClient(()) + override def isAlive(): Task[Boolean] = + ZIO.fromFuture(ec => + given ExecutionContext = ec + aliveClient(()).map { + case DecodeResult.Value(Right("ok")) => true + case _ => false + } recover { case _ => + false + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/AppConfig.scala b/app/src/main/scala/mdr/pdb/app/AppConfig.scala new file mode 100644 index 0000000..7d0f86b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/AppConfig.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app + +import zio.* +import scala.scalajs.js +import scala.scalajs.js.annotation.JSImport + +@js.native +trait JSAppConfig extends js.Object { + def baseUrl: String = js.native + def onlineCheckMs: js.UndefOr[Double] = js.native +} + +case class AppConfig( + baseUrl: String, + onlineCheckMs: Option[Int] +) + +object AppConfig { + @js.native + @JSImport("website-config/conf.js", JSImport.Default) + object config extends JSAppConfig + + val layer: ULayer[AppConfig] = + ZLayer.succeed( + AppConfig( + baseUrl = config.baseUrl, + onlineCheckMs = config.onlineCheckMs.toOption.map(_.toInt) + ) + ) +} diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index 733278c..d919ea0 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -31,17 +31,14 @@ @JSExportTopLevel("app") object Main extends ZIOApp: - override type Environment = ZEnv & Router[Page] & AppState & Api & LaminarApp + override type Environment = ZEnv & AppConfig & Router[Page] & AppState & Api & + LaminarApp override val tag: EnvironmentTag[Environment] = EnvironmentTag[Environment] // TODO: config override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live ++ (Routes.router >+> ApiLive.layer( - Some("/mdr/pdb/api") - ) >+> state.AppStateLive.layer( - unsafeWindowOwner - ) >+> LaminarAppLive.layer) + ZEnv.live >+> AppConfig.layer >+> Routes.router >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for @@ -49,6 +46,8 @@ documentEvents.onDomContentLoaded .foreach(_ => cb(program))(unsafeWindowOwner) ) + // Keep running forever, otherwise the resources are released after the run finishes + _ <- ZIO.never yield () private def program: RIO[LaminarApp, Unit] = diff --git a/.projectile b/.projectile new file mode 100644 index 0000000..900fdcc --- /dev/null +++ b/.projectile @@ -0,0 +1 @@ +-/app/.yarn diff --git a/app/src/main/scala/mdr/pdb/app/Api.scala b/app/src/main/scala/mdr/pdb/app/Api.scala index ea46468..cdfc970 100644 --- a/app/src/main/scala/mdr/pdb/app/Api.scala +++ b/app/src/main/scala/mdr/pdb/app/Api.scala @@ -6,12 +6,14 @@ import sttp.tapir.DecodeResult import org.scalajs.dom import scala.concurrent.Future +import scala.concurrent.ExecutionContext trait Api: - def alive(): Future[DecodeResult[Either[Unit, String]]] + def isAlive(): Task[Boolean] object ApiLive: - def layer(base: Option[String]): ULayer[Api] = ZLayer.succeed(ApiLive(base)) + val layer: URLayer[AppConfig, Api] = + ((conf: AppConfig) => ApiLive(Some(conf.baseUrl + "api/"))).toLayer class ApiLive(base: Option[String]) extends Api with CustomTapir: private val backend = FetchBackend( @@ -22,5 +24,13 @@ ) private val baseUri = base.map(b => uri"${b}") private val aliveClient = toClient(Endpoints.alive, baseUri, backend) - override def alive(): Future[DecodeResult[Either[Unit, String]]] = - aliveClient(()) + override def isAlive(): Task[Boolean] = + ZIO.fromFuture(ec => + given ExecutionContext = ec + aliveClient(()).map { + case DecodeResult.Value(Right("ok")) => true + case _ => false + } recover { case _ => + false + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/AppConfig.scala b/app/src/main/scala/mdr/pdb/app/AppConfig.scala new file mode 100644 index 0000000..7d0f86b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/AppConfig.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app + +import zio.* +import scala.scalajs.js +import scala.scalajs.js.annotation.JSImport + +@js.native +trait JSAppConfig extends js.Object { + def baseUrl: String = js.native + def onlineCheckMs: js.UndefOr[Double] = js.native +} + +case class AppConfig( + baseUrl: String, + onlineCheckMs: Option[Int] +) + +object AppConfig { + @js.native + @JSImport("website-config/conf.js", JSImport.Default) + object config extends JSAppConfig + + val layer: ULayer[AppConfig] = + ZLayer.succeed( + AppConfig( + baseUrl = config.baseUrl, + onlineCheckMs = config.onlineCheckMs.toOption.map(_.toInt) + ) + ) +} diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index 733278c..d919ea0 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -31,17 +31,14 @@ @JSExportTopLevel("app") object Main extends ZIOApp: - override type Environment = ZEnv & Router[Page] & AppState & Api & LaminarApp + override type Environment = ZEnv & AppConfig & Router[Page] & AppState & Api & + LaminarApp override val tag: EnvironmentTag[Environment] = EnvironmentTag[Environment] // TODO: config override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live ++ (Routes.router >+> ApiLive.layer( - Some("/mdr/pdb/api") - ) >+> state.AppStateLive.layer( - unsafeWindowOwner - ) >+> LaminarAppLive.layer) + ZEnv.live >+> AppConfig.layer >+> Routes.router >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for @@ -49,6 +46,8 @@ documentEvents.onDomContentLoaded .foreach(_ => cb(program))(unsafeWindowOwner) ) + // Keep running forever, otherwise the resources are released after the run finishes + _ <- ZIO.never yield () private def program: RIO[LaminarApp, Unit] = diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala index 46db170..02a58db 100644 --- a/app/src/main/scala/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -10,11 +10,11 @@ object Routes: - val layer: ULayer[Routes] = ZLayer.succeed(Routes()) + val layer: URLayer[AppConfig, Routes] = (Routes(_)).toLayer - val router: ULayer[Router[Page]] = layer.project(_.router) + val router: URLayer[AppConfig, Router[Page]] = layer.project(_.router) -class Routes(): +class Routes(appConfig: AppConfig): import Page.* import Routes.* @@ -27,9 +27,7 @@ given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" + val base = appConfig.baseUrl + "app" given router: Router[Page] = Router[Page]( routes = List( diff --git a/.projectile b/.projectile new file mode 100644 index 0000000..900fdcc --- /dev/null +++ b/.projectile @@ -0,0 +1 @@ +-/app/.yarn diff --git a/app/src/main/scala/mdr/pdb/app/Api.scala b/app/src/main/scala/mdr/pdb/app/Api.scala index ea46468..cdfc970 100644 --- a/app/src/main/scala/mdr/pdb/app/Api.scala +++ b/app/src/main/scala/mdr/pdb/app/Api.scala @@ -6,12 +6,14 @@ import sttp.tapir.DecodeResult import org.scalajs.dom import scala.concurrent.Future +import scala.concurrent.ExecutionContext trait Api: - def alive(): Future[DecodeResult[Either[Unit, String]]] + def isAlive(): Task[Boolean] object ApiLive: - def layer(base: Option[String]): ULayer[Api] = ZLayer.succeed(ApiLive(base)) + val layer: URLayer[AppConfig, Api] = + ((conf: AppConfig) => ApiLive(Some(conf.baseUrl + "api/"))).toLayer class ApiLive(base: Option[String]) extends Api with CustomTapir: private val backend = FetchBackend( @@ -22,5 +24,13 @@ ) private val baseUri = base.map(b => uri"${b}") private val aliveClient = toClient(Endpoints.alive, baseUri, backend) - override def alive(): Future[DecodeResult[Either[Unit, String]]] = - aliveClient(()) + override def isAlive(): Task[Boolean] = + ZIO.fromFuture(ec => + given ExecutionContext = ec + aliveClient(()).map { + case DecodeResult.Value(Right("ok")) => true + case _ => false + } recover { case _ => + false + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/AppConfig.scala b/app/src/main/scala/mdr/pdb/app/AppConfig.scala new file mode 100644 index 0000000..7d0f86b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/AppConfig.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app + +import zio.* +import scala.scalajs.js +import scala.scalajs.js.annotation.JSImport + +@js.native +trait JSAppConfig extends js.Object { + def baseUrl: String = js.native + def onlineCheckMs: js.UndefOr[Double] = js.native +} + +case class AppConfig( + baseUrl: String, + onlineCheckMs: Option[Int] +) + +object AppConfig { + @js.native + @JSImport("website-config/conf.js", JSImport.Default) + object config extends JSAppConfig + + val layer: ULayer[AppConfig] = + ZLayer.succeed( + AppConfig( + baseUrl = config.baseUrl, + onlineCheckMs = config.onlineCheckMs.toOption.map(_.toInt) + ) + ) +} diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index 733278c..d919ea0 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -31,17 +31,14 @@ @JSExportTopLevel("app") object Main extends ZIOApp: - override type Environment = ZEnv & Router[Page] & AppState & Api & LaminarApp + override type Environment = ZEnv & AppConfig & Router[Page] & AppState & Api & + LaminarApp override val tag: EnvironmentTag[Environment] = EnvironmentTag[Environment] // TODO: config override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live ++ (Routes.router >+> ApiLive.layer( - Some("/mdr/pdb/api") - ) >+> state.AppStateLive.layer( - unsafeWindowOwner - ) >+> LaminarAppLive.layer) + ZEnv.live >+> AppConfig.layer >+> Routes.router >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for @@ -49,6 +46,8 @@ documentEvents.onDomContentLoaded .foreach(_ => cb(program))(unsafeWindowOwner) ) + // Keep running forever, otherwise the resources are released after the run finishes + _ <- ZIO.never yield () private def program: RIO[LaminarApp, Unit] = diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala index 46db170..02a58db 100644 --- a/app/src/main/scala/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -10,11 +10,11 @@ object Routes: - val layer: ULayer[Routes] = ZLayer.succeed(Routes()) + val layer: URLayer[AppConfig, Routes] = (Routes(_)).toLayer - val router: ULayer[Router[Page]] = layer.project(_.router) + val router: URLayer[AppConfig, Router[Page]] = layer.project(_.router) -class Routes(): +class Routes(appConfig: AppConfig): import Page.* import Routes.* @@ -27,9 +27,7 @@ given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" + val base = appConfig.baseUrl + "app" given router: Router[Page] = Router[Page]( routes = List( diff --git a/app/src/main/scala/mdr/pdb/app/ZIOOwner.scala b/app/src/main/scala/mdr/pdb/app/ZIOOwner.scala new file mode 100644 index 0000000..4ca634b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/ZIOOwner.scala @@ -0,0 +1,15 @@ +package mdr.pdb.app + +import zio.* +import com.raquo.airstream.ownership.Owner +import scala.annotation.unused +import com.raquo.airstream.ownership.Subscription + +object ZIOOwner: + def acquire: UIO[ZIOOwner] = ZIO.succeed(new ZIOOwner) + + val layer = ZLayer.fromAcquireRelease(ZIOOwner.acquire)(_.release) + +class ZIOOwner extends Owner: + def release: UIO[Unit] = + Task.attempt(this.killSubscriptions()).ignore diff --git a/.projectile b/.projectile new file mode 100644 index 0000000..900fdcc --- /dev/null +++ b/.projectile @@ -0,0 +1 @@ +-/app/.yarn diff --git a/app/src/main/scala/mdr/pdb/app/Api.scala b/app/src/main/scala/mdr/pdb/app/Api.scala index ea46468..cdfc970 100644 --- a/app/src/main/scala/mdr/pdb/app/Api.scala +++ b/app/src/main/scala/mdr/pdb/app/Api.scala @@ -6,12 +6,14 @@ import sttp.tapir.DecodeResult import org.scalajs.dom import scala.concurrent.Future +import scala.concurrent.ExecutionContext trait Api: - def alive(): Future[DecodeResult[Either[Unit, String]]] + def isAlive(): Task[Boolean] object ApiLive: - def layer(base: Option[String]): ULayer[Api] = ZLayer.succeed(ApiLive(base)) + val layer: URLayer[AppConfig, Api] = + ((conf: AppConfig) => ApiLive(Some(conf.baseUrl + "api/"))).toLayer class ApiLive(base: Option[String]) extends Api with CustomTapir: private val backend = FetchBackend( @@ -22,5 +24,13 @@ ) private val baseUri = base.map(b => uri"${b}") private val aliveClient = toClient(Endpoints.alive, baseUri, backend) - override def alive(): Future[DecodeResult[Either[Unit, String]]] = - aliveClient(()) + override def isAlive(): Task[Boolean] = + ZIO.fromFuture(ec => + given ExecutionContext = ec + aliveClient(()).map { + case DecodeResult.Value(Right("ok")) => true + case _ => false + } recover { case _ => + false + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/AppConfig.scala b/app/src/main/scala/mdr/pdb/app/AppConfig.scala new file mode 100644 index 0000000..7d0f86b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/AppConfig.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app + +import zio.* +import scala.scalajs.js +import scala.scalajs.js.annotation.JSImport + +@js.native +trait JSAppConfig extends js.Object { + def baseUrl: String = js.native + def onlineCheckMs: js.UndefOr[Double] = js.native +} + +case class AppConfig( + baseUrl: String, + onlineCheckMs: Option[Int] +) + +object AppConfig { + @js.native + @JSImport("website-config/conf.js", JSImport.Default) + object config extends JSAppConfig + + val layer: ULayer[AppConfig] = + ZLayer.succeed( + AppConfig( + baseUrl = config.baseUrl, + onlineCheckMs = config.onlineCheckMs.toOption.map(_.toInt) + ) + ) +} diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index 733278c..d919ea0 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -31,17 +31,14 @@ @JSExportTopLevel("app") object Main extends ZIOApp: - override type Environment = ZEnv & Router[Page] & AppState & Api & LaminarApp + override type Environment = ZEnv & AppConfig & Router[Page] & AppState & Api & + LaminarApp override val tag: EnvironmentTag[Environment] = EnvironmentTag[Environment] // TODO: config override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live ++ (Routes.router >+> ApiLive.layer( - Some("/mdr/pdb/api") - ) >+> state.AppStateLive.layer( - unsafeWindowOwner - ) >+> LaminarAppLive.layer) + ZEnv.live >+> AppConfig.layer >+> Routes.router >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for @@ -49,6 +46,8 @@ documentEvents.onDomContentLoaded .foreach(_ => cb(program))(unsafeWindowOwner) ) + // Keep running forever, otherwise the resources are released after the run finishes + _ <- ZIO.never yield () private def program: RIO[LaminarApp, Unit] = diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala index 46db170..02a58db 100644 --- a/app/src/main/scala/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -10,11 +10,11 @@ object Routes: - val layer: ULayer[Routes] = ZLayer.succeed(Routes()) + val layer: URLayer[AppConfig, Routes] = (Routes(_)).toLayer - val router: ULayer[Router[Page]] = layer.project(_.router) + val router: URLayer[AppConfig, Router[Page]] = layer.project(_.router) -class Routes(): +class Routes(appConfig: AppConfig): import Page.* import Routes.* @@ -27,9 +27,7 @@ given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" + val base = appConfig.baseUrl + "app" given router: Router[Page] = Router[Page]( routes = List( diff --git a/app/src/main/scala/mdr/pdb/app/ZIOOwner.scala b/app/src/main/scala/mdr/pdb/app/ZIOOwner.scala new file mode 100644 index 0000000..4ca634b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/ZIOOwner.scala @@ -0,0 +1,15 @@ +package mdr.pdb.app + +import zio.* +import com.raquo.airstream.ownership.Owner +import scala.annotation.unused +import com.raquo.airstream.ownership.Subscription + +object ZIOOwner: + def acquire: UIO[ZIOOwner] = ZIO.succeed(new ZIOOwner) + + val layer = ZLayer.fromAcquireRelease(ZIOOwner.acquire)(_.release) + +class ZIOOwner extends Owner: + def release: UIO[Unit] = + Task.attempt(this.killSubscriptions()).ignore diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index d078b3a..6d6f50b 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action +case object CheckOnlineState extends Action case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action diff --git a/.projectile b/.projectile new file mode 100644 index 0000000..900fdcc --- /dev/null +++ b/.projectile @@ -0,0 +1 @@ +-/app/.yarn diff --git a/app/src/main/scala/mdr/pdb/app/Api.scala b/app/src/main/scala/mdr/pdb/app/Api.scala index ea46468..cdfc970 100644 --- a/app/src/main/scala/mdr/pdb/app/Api.scala +++ b/app/src/main/scala/mdr/pdb/app/Api.scala @@ -6,12 +6,14 @@ import sttp.tapir.DecodeResult import org.scalajs.dom import scala.concurrent.Future +import scala.concurrent.ExecutionContext trait Api: - def alive(): Future[DecodeResult[Either[Unit, String]]] + def isAlive(): Task[Boolean] object ApiLive: - def layer(base: Option[String]): ULayer[Api] = ZLayer.succeed(ApiLive(base)) + val layer: URLayer[AppConfig, Api] = + ((conf: AppConfig) => ApiLive(Some(conf.baseUrl + "api/"))).toLayer class ApiLive(base: Option[String]) extends Api with CustomTapir: private val backend = FetchBackend( @@ -22,5 +24,13 @@ ) private val baseUri = base.map(b => uri"${b}") private val aliveClient = toClient(Endpoints.alive, baseUri, backend) - override def alive(): Future[DecodeResult[Either[Unit, String]]] = - aliveClient(()) + override def isAlive(): Task[Boolean] = + ZIO.fromFuture(ec => + given ExecutionContext = ec + aliveClient(()).map { + case DecodeResult.Value(Right("ok")) => true + case _ => false + } recover { case _ => + false + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/AppConfig.scala b/app/src/main/scala/mdr/pdb/app/AppConfig.scala new file mode 100644 index 0000000..7d0f86b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/AppConfig.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app + +import zio.* +import scala.scalajs.js +import scala.scalajs.js.annotation.JSImport + +@js.native +trait JSAppConfig extends js.Object { + def baseUrl: String = js.native + def onlineCheckMs: js.UndefOr[Double] = js.native +} + +case class AppConfig( + baseUrl: String, + onlineCheckMs: Option[Int] +) + +object AppConfig { + @js.native + @JSImport("website-config/conf.js", JSImport.Default) + object config extends JSAppConfig + + val layer: ULayer[AppConfig] = + ZLayer.succeed( + AppConfig( + baseUrl = config.baseUrl, + onlineCheckMs = config.onlineCheckMs.toOption.map(_.toInt) + ) + ) +} diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index 733278c..d919ea0 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -31,17 +31,14 @@ @JSExportTopLevel("app") object Main extends ZIOApp: - override type Environment = ZEnv & Router[Page] & AppState & Api & LaminarApp + override type Environment = ZEnv & AppConfig & Router[Page] & AppState & Api & + LaminarApp override val tag: EnvironmentTag[Environment] = EnvironmentTag[Environment] // TODO: config override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live ++ (Routes.router >+> ApiLive.layer( - Some("/mdr/pdb/api") - ) >+> state.AppStateLive.layer( - unsafeWindowOwner - ) >+> LaminarAppLive.layer) + ZEnv.live >+> AppConfig.layer >+> Routes.router >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for @@ -49,6 +46,8 @@ documentEvents.onDomContentLoaded .foreach(_ => cb(program))(unsafeWindowOwner) ) + // Keep running forever, otherwise the resources are released after the run finishes + _ <- ZIO.never yield () private def program: RIO[LaminarApp, Unit] = diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala index 46db170..02a58db 100644 --- a/app/src/main/scala/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -10,11 +10,11 @@ object Routes: - val layer: ULayer[Routes] = ZLayer.succeed(Routes()) + val layer: URLayer[AppConfig, Routes] = (Routes(_)).toLayer - val router: ULayer[Router[Page]] = layer.project(_.router) + val router: URLayer[AppConfig, Router[Page]] = layer.project(_.router) -class Routes(): +class Routes(appConfig: AppConfig): import Page.* import Routes.* @@ -27,9 +27,7 @@ given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" + val base = appConfig.baseUrl + "app" given router: Router[Page] = Router[Page]( routes = List( diff --git a/app/src/main/scala/mdr/pdb/app/ZIOOwner.scala b/app/src/main/scala/mdr/pdb/app/ZIOOwner.scala new file mode 100644 index 0000000..4ca634b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/ZIOOwner.scala @@ -0,0 +1,15 @@ +package mdr.pdb.app + +import zio.* +import com.raquo.airstream.ownership.Owner +import scala.annotation.unused +import com.raquo.airstream.ownership.Subscription + +object ZIOOwner: + def acquire: UIO[ZIOOwner] = ZIO.succeed(new ZIOOwner) + + val layer = ZLayer.fromAcquireRelease(ZIOOwner.acquire)(_.release) + +class ZIOOwner extends Owner: + def release: UIO[Unit] = + Task.attempt(this.killSubscriptions()).ignore diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index d078b3a..6d6f50b 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action +case object CheckOnlineState extends Action case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 4d82d99..02a6eeb 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -18,6 +18,9 @@ import mdr.pdb.UserContract import fiftyforms.services.files.File import sttp.tapir.DecodeResult +import com.raquo.airstream.ownership.OneTimeOwner +import scala.annotation.unused +import com.raquo.airstream.ownership.Subscription trait AppState extends components.AppPage.AppState @@ -34,11 +37,26 @@ def actionBus: Observer[Action] object AppStateLive: - def layer(owner: Owner): URLayer[Api & Router[Page], AppState] = - (AppStateLive(owner, _, _)).toLayer[AppState] + def layer: URLayer[ZEnv & AppConfig & Api & Router[Page], AppState] = { + (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( + ( + appConfig: AppConfig, + api: Api, + router: Router[Page], + runtime: Runtime[ZEnv], + owner: Owner + ) => AppStateLive(appConfig, api, router, runtime)(using owner) + ).toLayer[AppState] + } -class AppStateLive(owner: Owner, api: Api, router: Router[Page]) - extends AppState: +class AppStateLive( + appConfig: AppConfig, + api: Api, + router: Router[Page], + runtime: Runtime[ZEnv] +)(using + owner: Owner +) extends AppState: given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen @@ -55,7 +73,7 @@ EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - private val isOnline = Var(false) + private val isOnline = Var(true) private val mockData: List[UserInfo] = mockUsers @@ -76,53 +94,65 @@ .collect { case Right(p) => p } .toList - EventStream - .periodic(1000, false, false) - .flatMap(_ => - EventStream - .fromFuture(api.alive()) - .map { - case DecodeResult.Value(_) => true - case _ => false - } + private def scheduleOnlineCheck(): Unit = + appConfig.onlineCheckMs.foreach(d => + actions.writer.delay(d).onNext(CheckOnlineState) ) - .foreach(isOnline.set)(using owner) // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) + private val handler: Action => Task[Unit] = + case CheckOnlineState => + for + o <- api.isAlive() + _ <- Task.attempt { + isOnline.set(o) + scheduleOnlineCheck() + } + yield () + case FetchDirectory => Task.attempt(pushUsers(mockData)) case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) + Task.attempt { + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } } case FetchParameters(osc) => - pushParameters(mockParameters) + Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) + Task.attempt { + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) + } case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) + Task.attempt { + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) + } + case NavigateTo(page) => Task.attempt { router.pushState(page) } case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") + Task.attempt { + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) ) - ) - }(using owner) + } + + actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) + + scheduleOnlineCheck() override def online: Signal[Boolean] = isOnline.signal override def users: EventStream[List[UserInfo]] = diff --git a/.projectile b/.projectile new file mode 100644 index 0000000..900fdcc --- /dev/null +++ b/.projectile @@ -0,0 +1 @@ +-/app/.yarn diff --git a/app/src/main/scala/mdr/pdb/app/Api.scala b/app/src/main/scala/mdr/pdb/app/Api.scala index ea46468..cdfc970 100644 --- a/app/src/main/scala/mdr/pdb/app/Api.scala +++ b/app/src/main/scala/mdr/pdb/app/Api.scala @@ -6,12 +6,14 @@ import sttp.tapir.DecodeResult import org.scalajs.dom import scala.concurrent.Future +import scala.concurrent.ExecutionContext trait Api: - def alive(): Future[DecodeResult[Either[Unit, String]]] + def isAlive(): Task[Boolean] object ApiLive: - def layer(base: Option[String]): ULayer[Api] = ZLayer.succeed(ApiLive(base)) + val layer: URLayer[AppConfig, Api] = + ((conf: AppConfig) => ApiLive(Some(conf.baseUrl + "api/"))).toLayer class ApiLive(base: Option[String]) extends Api with CustomTapir: private val backend = FetchBackend( @@ -22,5 +24,13 @@ ) private val baseUri = base.map(b => uri"${b}") private val aliveClient = toClient(Endpoints.alive, baseUri, backend) - override def alive(): Future[DecodeResult[Either[Unit, String]]] = - aliveClient(()) + override def isAlive(): Task[Boolean] = + ZIO.fromFuture(ec => + given ExecutionContext = ec + aliveClient(()).map { + case DecodeResult.Value(Right("ok")) => true + case _ => false + } recover { case _ => + false + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/AppConfig.scala b/app/src/main/scala/mdr/pdb/app/AppConfig.scala new file mode 100644 index 0000000..7d0f86b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/AppConfig.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app + +import zio.* +import scala.scalajs.js +import scala.scalajs.js.annotation.JSImport + +@js.native +trait JSAppConfig extends js.Object { + def baseUrl: String = js.native + def onlineCheckMs: js.UndefOr[Double] = js.native +} + +case class AppConfig( + baseUrl: String, + onlineCheckMs: Option[Int] +) + +object AppConfig { + @js.native + @JSImport("website-config/conf.js", JSImport.Default) + object config extends JSAppConfig + + val layer: ULayer[AppConfig] = + ZLayer.succeed( + AppConfig( + baseUrl = config.baseUrl, + onlineCheckMs = config.onlineCheckMs.toOption.map(_.toInt) + ) + ) +} diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index 733278c..d919ea0 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -31,17 +31,14 @@ @JSExportTopLevel("app") object Main extends ZIOApp: - override type Environment = ZEnv & Router[Page] & AppState & Api & LaminarApp + override type Environment = ZEnv & AppConfig & Router[Page] & AppState & Api & + LaminarApp override val tag: EnvironmentTag[Environment] = EnvironmentTag[Environment] // TODO: config override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live ++ (Routes.router >+> ApiLive.layer( - Some("/mdr/pdb/api") - ) >+> state.AppStateLive.layer( - unsafeWindowOwner - ) >+> LaminarAppLive.layer) + ZEnv.live >+> AppConfig.layer >+> Routes.router >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for @@ -49,6 +46,8 @@ documentEvents.onDomContentLoaded .foreach(_ => cb(program))(unsafeWindowOwner) ) + // Keep running forever, otherwise the resources are released after the run finishes + _ <- ZIO.never yield () private def program: RIO[LaminarApp, Unit] = diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala index 46db170..02a58db 100644 --- a/app/src/main/scala/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -10,11 +10,11 @@ object Routes: - val layer: ULayer[Routes] = ZLayer.succeed(Routes()) + val layer: URLayer[AppConfig, Routes] = (Routes(_)).toLayer - val router: ULayer[Router[Page]] = layer.project(_.router) + val router: URLayer[AppConfig, Router[Page]] = layer.project(_.router) -class Routes(): +class Routes(appConfig: AppConfig): import Page.* import Routes.* @@ -27,9 +27,7 @@ given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" + val base = appConfig.baseUrl + "app" given router: Router[Page] = Router[Page]( routes = List( diff --git a/app/src/main/scala/mdr/pdb/app/ZIOOwner.scala b/app/src/main/scala/mdr/pdb/app/ZIOOwner.scala new file mode 100644 index 0000000..4ca634b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/ZIOOwner.scala @@ -0,0 +1,15 @@ +package mdr.pdb.app + +import zio.* +import com.raquo.airstream.ownership.Owner +import scala.annotation.unused +import com.raquo.airstream.ownership.Subscription + +object ZIOOwner: + def acquire: UIO[ZIOOwner] = ZIO.succeed(new ZIOOwner) + + val layer = ZLayer.fromAcquireRelease(ZIOOwner.acquire)(_.release) + +class ZIOOwner extends Owner: + def release: UIO[Unit] = + Task.attempt(this.killSubscriptions()).ignore diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index d078b3a..6d6f50b 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action +case object CheckOnlineState extends Action case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 4d82d99..02a6eeb 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -18,6 +18,9 @@ import mdr.pdb.UserContract import fiftyforms.services.files.File import sttp.tapir.DecodeResult +import com.raquo.airstream.ownership.OneTimeOwner +import scala.annotation.unused +import com.raquo.airstream.ownership.Subscription trait AppState extends components.AppPage.AppState @@ -34,11 +37,26 @@ def actionBus: Observer[Action] object AppStateLive: - def layer(owner: Owner): URLayer[Api & Router[Page], AppState] = - (AppStateLive(owner, _, _)).toLayer[AppState] + def layer: URLayer[ZEnv & AppConfig & Api & Router[Page], AppState] = { + (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( + ( + appConfig: AppConfig, + api: Api, + router: Router[Page], + runtime: Runtime[ZEnv], + owner: Owner + ) => AppStateLive(appConfig, api, router, runtime)(using owner) + ).toLayer[AppState] + } -class AppStateLive(owner: Owner, api: Api, router: Router[Page]) - extends AppState: +class AppStateLive( + appConfig: AppConfig, + api: Api, + router: Router[Page], + runtime: Runtime[ZEnv] +)(using + owner: Owner +) extends AppState: given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen @@ -55,7 +73,7 @@ EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - private val isOnline = Var(false) + private val isOnline = Var(true) private val mockData: List[UserInfo] = mockUsers @@ -76,53 +94,65 @@ .collect { case Right(p) => p } .toList - EventStream - .periodic(1000, false, false) - .flatMap(_ => - EventStream - .fromFuture(api.alive()) - .map { - case DecodeResult.Value(_) => true - case _ => false - } + private def scheduleOnlineCheck(): Unit = + appConfig.onlineCheckMs.foreach(d => + actions.writer.delay(d).onNext(CheckOnlineState) ) - .foreach(isOnline.set)(using owner) // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) + private val handler: Action => Task[Unit] = + case CheckOnlineState => + for + o <- api.isAlive() + _ <- Task.attempt { + isOnline.set(o) + scheduleOnlineCheck() + } + yield () + case FetchDirectory => Task.attempt(pushUsers(mockData)) case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) + Task.attempt { + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } } case FetchParameters(osc) => - pushParameters(mockParameters) + Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) + Task.attempt { + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) + } case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) + Task.attempt { + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) + } + case NavigateTo(page) => Task.attempt { router.pushState(page) } case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") + Task.attempt { + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) ) - ) - }(using owner) + } + + actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) + + scheduleOnlineCheck() override def online: Signal[Boolean] = isOnline.signal override def users: EventStream[List[UserInfo]] = diff --git a/app/vite.config.js b/app/vite.config.js index aa57b89..398db7a 100644 --- a/app/vite.config.js +++ b/app/vite.config.js @@ -28,12 +28,10 @@ alias: { 'stylesheets': path.resolve(__dirname, './src/main/static/stylesheets'), 'data': path.resolve(__dirname, appInfo.mockDataDir), - 'params': path.resolve(__dirname, './pdb-params') - /* + 'params': path.resolve(__dirname, './pdb-params'), 'website-config': mode === 'production' ? - resolve(__dirname, '../website-config/prod') : - resolve(__dirname, '../website-config/dev') - */ + path.resolve(__dirname, './website-config/prod') : + path.resolve(__dirname, './website-config/dev') } }, base: '/mdr/pdb/', diff --git a/.projectile b/.projectile new file mode 100644 index 0000000..900fdcc --- /dev/null +++ b/.projectile @@ -0,0 +1 @@ +-/app/.yarn diff --git a/app/src/main/scala/mdr/pdb/app/Api.scala b/app/src/main/scala/mdr/pdb/app/Api.scala index ea46468..cdfc970 100644 --- a/app/src/main/scala/mdr/pdb/app/Api.scala +++ b/app/src/main/scala/mdr/pdb/app/Api.scala @@ -6,12 +6,14 @@ import sttp.tapir.DecodeResult import org.scalajs.dom import scala.concurrent.Future +import scala.concurrent.ExecutionContext trait Api: - def alive(): Future[DecodeResult[Either[Unit, String]]] + def isAlive(): Task[Boolean] object ApiLive: - def layer(base: Option[String]): ULayer[Api] = ZLayer.succeed(ApiLive(base)) + val layer: URLayer[AppConfig, Api] = + ((conf: AppConfig) => ApiLive(Some(conf.baseUrl + "api/"))).toLayer class ApiLive(base: Option[String]) extends Api with CustomTapir: private val backend = FetchBackend( @@ -22,5 +24,13 @@ ) private val baseUri = base.map(b => uri"${b}") private val aliveClient = toClient(Endpoints.alive, baseUri, backend) - override def alive(): Future[DecodeResult[Either[Unit, String]]] = - aliveClient(()) + override def isAlive(): Task[Boolean] = + ZIO.fromFuture(ec => + given ExecutionContext = ec + aliveClient(()).map { + case DecodeResult.Value(Right("ok")) => true + case _ => false + } recover { case _ => + false + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/AppConfig.scala b/app/src/main/scala/mdr/pdb/app/AppConfig.scala new file mode 100644 index 0000000..7d0f86b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/AppConfig.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app + +import zio.* +import scala.scalajs.js +import scala.scalajs.js.annotation.JSImport + +@js.native +trait JSAppConfig extends js.Object { + def baseUrl: String = js.native + def onlineCheckMs: js.UndefOr[Double] = js.native +} + +case class AppConfig( + baseUrl: String, + onlineCheckMs: Option[Int] +) + +object AppConfig { + @js.native + @JSImport("website-config/conf.js", JSImport.Default) + object config extends JSAppConfig + + val layer: ULayer[AppConfig] = + ZLayer.succeed( + AppConfig( + baseUrl = config.baseUrl, + onlineCheckMs = config.onlineCheckMs.toOption.map(_.toInt) + ) + ) +} diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index 733278c..d919ea0 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -31,17 +31,14 @@ @JSExportTopLevel("app") object Main extends ZIOApp: - override type Environment = ZEnv & Router[Page] & AppState & Api & LaminarApp + override type Environment = ZEnv & AppConfig & Router[Page] & AppState & Api & + LaminarApp override val tag: EnvironmentTag[Environment] = EnvironmentTag[Environment] // TODO: config override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live ++ (Routes.router >+> ApiLive.layer( - Some("/mdr/pdb/api") - ) >+> state.AppStateLive.layer( - unsafeWindowOwner - ) >+> LaminarAppLive.layer) + ZEnv.live >+> AppConfig.layer >+> Routes.router >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for @@ -49,6 +46,8 @@ documentEvents.onDomContentLoaded .foreach(_ => cb(program))(unsafeWindowOwner) ) + // Keep running forever, otherwise the resources are released after the run finishes + _ <- ZIO.never yield () private def program: RIO[LaminarApp, Unit] = diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala index 46db170..02a58db 100644 --- a/app/src/main/scala/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -10,11 +10,11 @@ object Routes: - val layer: ULayer[Routes] = ZLayer.succeed(Routes()) + val layer: URLayer[AppConfig, Routes] = (Routes(_)).toLayer - val router: ULayer[Router[Page]] = layer.project(_.router) + val router: URLayer[AppConfig, Router[Page]] = layer.project(_.router) -class Routes(): +class Routes(appConfig: AppConfig): import Page.* import Routes.* @@ -27,9 +27,7 @@ given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" + val base = appConfig.baseUrl + "app" given router: Router[Page] = Router[Page]( routes = List( diff --git a/app/src/main/scala/mdr/pdb/app/ZIOOwner.scala b/app/src/main/scala/mdr/pdb/app/ZIOOwner.scala new file mode 100644 index 0000000..4ca634b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/ZIOOwner.scala @@ -0,0 +1,15 @@ +package mdr.pdb.app + +import zio.* +import com.raquo.airstream.ownership.Owner +import scala.annotation.unused +import com.raquo.airstream.ownership.Subscription + +object ZIOOwner: + def acquire: UIO[ZIOOwner] = ZIO.succeed(new ZIOOwner) + + val layer = ZLayer.fromAcquireRelease(ZIOOwner.acquire)(_.release) + +class ZIOOwner extends Owner: + def release: UIO[Unit] = + Task.attempt(this.killSubscriptions()).ignore diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index d078b3a..6d6f50b 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action +case object CheckOnlineState extends Action case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 4d82d99..02a6eeb 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -18,6 +18,9 @@ import mdr.pdb.UserContract import fiftyforms.services.files.File import sttp.tapir.DecodeResult +import com.raquo.airstream.ownership.OneTimeOwner +import scala.annotation.unused +import com.raquo.airstream.ownership.Subscription trait AppState extends components.AppPage.AppState @@ -34,11 +37,26 @@ def actionBus: Observer[Action] object AppStateLive: - def layer(owner: Owner): URLayer[Api & Router[Page], AppState] = - (AppStateLive(owner, _, _)).toLayer[AppState] + def layer: URLayer[ZEnv & AppConfig & Api & Router[Page], AppState] = { + (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( + ( + appConfig: AppConfig, + api: Api, + router: Router[Page], + runtime: Runtime[ZEnv], + owner: Owner + ) => AppStateLive(appConfig, api, router, runtime)(using owner) + ).toLayer[AppState] + } -class AppStateLive(owner: Owner, api: Api, router: Router[Page]) - extends AppState: +class AppStateLive( + appConfig: AppConfig, + api: Api, + router: Router[Page], + runtime: Runtime[ZEnv] +)(using + owner: Owner +) extends AppState: given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen @@ -55,7 +73,7 @@ EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - private val isOnline = Var(false) + private val isOnline = Var(true) private val mockData: List[UserInfo] = mockUsers @@ -76,53 +94,65 @@ .collect { case Right(p) => p } .toList - EventStream - .periodic(1000, false, false) - .flatMap(_ => - EventStream - .fromFuture(api.alive()) - .map { - case DecodeResult.Value(_) => true - case _ => false - } + private def scheduleOnlineCheck(): Unit = + appConfig.onlineCheckMs.foreach(d => + actions.writer.delay(d).onNext(CheckOnlineState) ) - .foreach(isOnline.set)(using owner) // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) + private val handler: Action => Task[Unit] = + case CheckOnlineState => + for + o <- api.isAlive() + _ <- Task.attempt { + isOnline.set(o) + scheduleOnlineCheck() + } + yield () + case FetchDirectory => Task.attempt(pushUsers(mockData)) case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) + Task.attempt { + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } } case FetchParameters(osc) => - pushParameters(mockParameters) + Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) + Task.attempt { + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) + } case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) + Task.attempt { + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) + } + case NavigateTo(page) => Task.attempt { router.pushState(page) } case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") + Task.attempt { + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) ) - ) - }(using owner) + } + + actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) + + scheduleOnlineCheck() override def online: Signal[Boolean] = isOnline.signal override def users: EventStream[List[UserInfo]] = diff --git a/app/vite.config.js b/app/vite.config.js index aa57b89..398db7a 100644 --- a/app/vite.config.js +++ b/app/vite.config.js @@ -28,12 +28,10 @@ alias: { 'stylesheets': path.resolve(__dirname, './src/main/static/stylesheets'), 'data': path.resolve(__dirname, appInfo.mockDataDir), - 'params': path.resolve(__dirname, './pdb-params') - /* + 'params': path.resolve(__dirname, './pdb-params'), 'website-config': mode === 'production' ? - resolve(__dirname, '../website-config/prod') : - resolve(__dirname, '../website-config/dev') - */ + path.resolve(__dirname, './website-config/prod') : + path.resolve(__dirname, './website-config/dev') } }, base: '/mdr/pdb/', diff --git a/app/website-config/dev/conf.js b/app/website-config/dev/conf.js new file mode 100644 index 0000000..e52ff61 --- /dev/null +++ b/app/website-config/dev/conf.js @@ -0,0 +1,4 @@ +export default { + baseUrl: import.meta.env.BASE_URL, + onlineCheckMs: 5000 +} diff --git a/.projectile b/.projectile new file mode 100644 index 0000000..900fdcc --- /dev/null +++ b/.projectile @@ -0,0 +1 @@ +-/app/.yarn diff --git a/app/src/main/scala/mdr/pdb/app/Api.scala b/app/src/main/scala/mdr/pdb/app/Api.scala index ea46468..cdfc970 100644 --- a/app/src/main/scala/mdr/pdb/app/Api.scala +++ b/app/src/main/scala/mdr/pdb/app/Api.scala @@ -6,12 +6,14 @@ import sttp.tapir.DecodeResult import org.scalajs.dom import scala.concurrent.Future +import scala.concurrent.ExecutionContext trait Api: - def alive(): Future[DecodeResult[Either[Unit, String]]] + def isAlive(): Task[Boolean] object ApiLive: - def layer(base: Option[String]): ULayer[Api] = ZLayer.succeed(ApiLive(base)) + val layer: URLayer[AppConfig, Api] = + ((conf: AppConfig) => ApiLive(Some(conf.baseUrl + "api/"))).toLayer class ApiLive(base: Option[String]) extends Api with CustomTapir: private val backend = FetchBackend( @@ -22,5 +24,13 @@ ) private val baseUri = base.map(b => uri"${b}") private val aliveClient = toClient(Endpoints.alive, baseUri, backend) - override def alive(): Future[DecodeResult[Either[Unit, String]]] = - aliveClient(()) + override def isAlive(): Task[Boolean] = + ZIO.fromFuture(ec => + given ExecutionContext = ec + aliveClient(()).map { + case DecodeResult.Value(Right("ok")) => true + case _ => false + } recover { case _ => + false + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/AppConfig.scala b/app/src/main/scala/mdr/pdb/app/AppConfig.scala new file mode 100644 index 0000000..7d0f86b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/AppConfig.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app + +import zio.* +import scala.scalajs.js +import scala.scalajs.js.annotation.JSImport + +@js.native +trait JSAppConfig extends js.Object { + def baseUrl: String = js.native + def onlineCheckMs: js.UndefOr[Double] = js.native +} + +case class AppConfig( + baseUrl: String, + onlineCheckMs: Option[Int] +) + +object AppConfig { + @js.native + @JSImport("website-config/conf.js", JSImport.Default) + object config extends JSAppConfig + + val layer: ULayer[AppConfig] = + ZLayer.succeed( + AppConfig( + baseUrl = config.baseUrl, + onlineCheckMs = config.onlineCheckMs.toOption.map(_.toInt) + ) + ) +} diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index 733278c..d919ea0 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -31,17 +31,14 @@ @JSExportTopLevel("app") object Main extends ZIOApp: - override type Environment = ZEnv & Router[Page] & AppState & Api & LaminarApp + override type Environment = ZEnv & AppConfig & Router[Page] & AppState & Api & + LaminarApp override val tag: EnvironmentTag[Environment] = EnvironmentTag[Environment] // TODO: config override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live ++ (Routes.router >+> ApiLive.layer( - Some("/mdr/pdb/api") - ) >+> state.AppStateLive.layer( - unsafeWindowOwner - ) >+> LaminarAppLive.layer) + ZEnv.live >+> AppConfig.layer >+> Routes.router >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for @@ -49,6 +46,8 @@ documentEvents.onDomContentLoaded .foreach(_ => cb(program))(unsafeWindowOwner) ) + // Keep running forever, otherwise the resources are released after the run finishes + _ <- ZIO.never yield () private def program: RIO[LaminarApp, Unit] = diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala index 46db170..02a58db 100644 --- a/app/src/main/scala/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -10,11 +10,11 @@ object Routes: - val layer: ULayer[Routes] = ZLayer.succeed(Routes()) + val layer: URLayer[AppConfig, Routes] = (Routes(_)).toLayer - val router: ULayer[Router[Page]] = layer.project(_.router) + val router: URLayer[AppConfig, Router[Page]] = layer.project(_.router) -class Routes(): +class Routes(appConfig: AppConfig): import Page.* import Routes.* @@ -27,9 +27,7 @@ given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" + val base = appConfig.baseUrl + "app" given router: Router[Page] = Router[Page]( routes = List( diff --git a/app/src/main/scala/mdr/pdb/app/ZIOOwner.scala b/app/src/main/scala/mdr/pdb/app/ZIOOwner.scala new file mode 100644 index 0000000..4ca634b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/ZIOOwner.scala @@ -0,0 +1,15 @@ +package mdr.pdb.app + +import zio.* +import com.raquo.airstream.ownership.Owner +import scala.annotation.unused +import com.raquo.airstream.ownership.Subscription + +object ZIOOwner: + def acquire: UIO[ZIOOwner] = ZIO.succeed(new ZIOOwner) + + val layer = ZLayer.fromAcquireRelease(ZIOOwner.acquire)(_.release) + +class ZIOOwner extends Owner: + def release: UIO[Unit] = + Task.attempt(this.killSubscriptions()).ignore diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index d078b3a..6d6f50b 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action +case object CheckOnlineState extends Action case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 4d82d99..02a6eeb 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -18,6 +18,9 @@ import mdr.pdb.UserContract import fiftyforms.services.files.File import sttp.tapir.DecodeResult +import com.raquo.airstream.ownership.OneTimeOwner +import scala.annotation.unused +import com.raquo.airstream.ownership.Subscription trait AppState extends components.AppPage.AppState @@ -34,11 +37,26 @@ def actionBus: Observer[Action] object AppStateLive: - def layer(owner: Owner): URLayer[Api & Router[Page], AppState] = - (AppStateLive(owner, _, _)).toLayer[AppState] + def layer: URLayer[ZEnv & AppConfig & Api & Router[Page], AppState] = { + (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( + ( + appConfig: AppConfig, + api: Api, + router: Router[Page], + runtime: Runtime[ZEnv], + owner: Owner + ) => AppStateLive(appConfig, api, router, runtime)(using owner) + ).toLayer[AppState] + } -class AppStateLive(owner: Owner, api: Api, router: Router[Page]) - extends AppState: +class AppStateLive( + appConfig: AppConfig, + api: Api, + router: Router[Page], + runtime: Runtime[ZEnv] +)(using + owner: Owner +) extends AppState: given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen @@ -55,7 +73,7 @@ EventStream.withCallback[List[UserInfo]] private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - private val isOnline = Var(false) + private val isOnline = Var(true) private val mockData: List[UserInfo] = mockUsers @@ -76,53 +94,65 @@ .collect { case Right(p) => p } .toList - EventStream - .periodic(1000, false, false) - .flatMap(_ => - EventStream - .fromFuture(api.alive()) - .map { - case DecodeResult.Value(_) => true - case _ => false - } + private def scheduleOnlineCheck(): Unit = + appConfig.onlineCheckMs.foreach(d => + actions.writer.delay(d).onNext(CheckOnlineState) ) - .foreach(isOnline.set)(using owner) // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) + private val handler: Action => Task[Unit] = + case CheckOnlineState => + for + o <- api.isAlive() + _ <- Task.attempt { + isOnline.set(o) + scheduleOnlineCheck() + } + yield () + case FetchDirectory => Task.attempt(pushUsers(mockData)) case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) + Task.attempt { + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } } case FetchParameters(osc) => - pushParameters(mockParameters) + Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) + Task.attempt { + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) + } case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) + Task.attempt { + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) + } + case NavigateTo(page) => Task.attempt { router.pushState(page) } case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") + Task.attempt { + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) ) - ) - }(using owner) + } + + actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) + + scheduleOnlineCheck() override def online: Signal[Boolean] = isOnline.signal override def users: EventStream[List[UserInfo]] = diff --git a/app/vite.config.js b/app/vite.config.js index aa57b89..398db7a 100644 --- a/app/vite.config.js +++ b/app/vite.config.js @@ -28,12 +28,10 @@ alias: { 'stylesheets': path.resolve(__dirname, './src/main/static/stylesheets'), 'data': path.resolve(__dirname, appInfo.mockDataDir), - 'params': path.resolve(__dirname, './pdb-params') - /* + 'params': path.resolve(__dirname, './pdb-params'), 'website-config': mode === 'production' ? - resolve(__dirname, '../website-config/prod') : - resolve(__dirname, '../website-config/dev') - */ + path.resolve(__dirname, './website-config/prod') : + path.resolve(__dirname, './website-config/dev') } }, base: '/mdr/pdb/', diff --git a/app/website-config/dev/conf.js b/app/website-config/dev/conf.js new file mode 100644 index 0000000..e52ff61 --- /dev/null +++ b/app/website-config/dev/conf.js @@ -0,0 +1,4 @@ +export default { + baseUrl: import.meta.env.BASE_URL, + onlineCheckMs: 5000 +} diff --git a/app/website-config/prod/conf.js b/app/website-config/prod/conf.js new file mode 100644 index 0000000..171ab49 --- /dev/null +++ b/app/website-config/prod/conf.js @@ -0,0 +1,3 @@ +export default { + baseUrl: import.meta.env.BASE_URL +}