diff --git a/ui/js/src/main/scala/works/iterative/ui/components/ReloadableComponent.scala b/ui/js/src/main/scala/works/iterative/ui/components/ReloadableComponent.scala index fed565e..174897d 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/ReloadableComponent.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/ReloadableComponent.scala @@ -10,29 +10,51 @@ case class ReloadableComponent[A, I]( fetch: I => IO[UserMessage, A], init: Option[I] = None, - loadSchedule: Schedule[Any, UserMessage, Any] = Schedule.stop + loadSchedule: Schedule[Any, Any, ?] = Schedule.stop )(using runtime: Runtime[Any]): private val computable: Var[Computable[A]] = Var(Computable.Uninitialized) private val memo: Var[Option[I]] = Var(init) val state: Signal[Computable[A]] = computable.signal + def now(): Option[A] = computable.now().toOption val update: Observer[I] = Observer { input => memo.update(_ => Some(input)) load(input) } - val reload: Observer[Unit] = Observer(_ => memo.now().foreach(load)) + val reload: Observer[ReloadableComponent.Reload[A]] = Observer { + case ReloadableComponent.Reload.Once => memo.now().foreach(load) + case ReloadableComponent.Reload.UntilChanged(original) => + memo.now().foreach(reloadUntilChanged(_, original)) + } - def initMod: HtmlMod = EventStream.fromValue(()) --> reload + def initMod: HtmlMod = + EventStream.fromValue(ReloadableComponent.Reload.Once) --> reload - def load(input: I): Unit = + def load(input: I): Unit = doLoad( + input, + fetch(_).retry(loadSchedule) + ) + + def reloadUntilChanged(input: I, original: A): Unit = + doLoad( + input, + fetch(_) + .repeat(loadSchedule && Schedule.recurWhile(_ == original)) + .map(_._2) + ) + + private def doLoad( + input: I, + q: I => IO[UserMessage, A] + ): Unit = computable.update(_.started) // TODO: do we need to manage the result of the run? val _ = Unsafe.unsafely { runtime.unsafe.runOrFork( - fetch(input).retry(loadSchedule).fold( + q(input).fold( msg => computable.update(_.fail(msg)), result => computable.update(_.update(result)) ) @@ -40,12 +62,18 @@ } object ReloadableComponent: + enum Reload[+A]: + case Once extends Reload[Nothing] + case UntilChanged(a: A) extends Reload[A] + def apply[A](fetch: IO[UserMessage, A])(using runtime: Runtime[Any] ): ReloadableComponent[A, Unit] = ReloadableComponent(_ => fetch, Some(())) - def apply[A, I](endpoint: PublicEndpoint[I, Unit, A, Any]): URIO[ClientEndpointFactory, ReloadableComponent[A, I]] = + def apply[A, I]( + endpoint: PublicEndpoint[I, Unit, A, Any] + ): URIO[ClientEndpointFactory, ReloadableComponent[A, I]] = for given Runtime[Any] <- ZIO.runtime[Any] factory <- ZIO.service[ClientEndpointFactory] diff --git a/ui/js/src/main/scala/works/iterative/ui/components/ReloadableComponent.scala b/ui/js/src/main/scala/works/iterative/ui/components/ReloadableComponent.scala index fed565e..174897d 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/ReloadableComponent.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/ReloadableComponent.scala @@ -10,29 +10,51 @@ case class ReloadableComponent[A, I]( fetch: I => IO[UserMessage, A], init: Option[I] = None, - loadSchedule: Schedule[Any, UserMessage, Any] = Schedule.stop + loadSchedule: Schedule[Any, Any, ?] = Schedule.stop )(using runtime: Runtime[Any]): private val computable: Var[Computable[A]] = Var(Computable.Uninitialized) private val memo: Var[Option[I]] = Var(init) val state: Signal[Computable[A]] = computable.signal + def now(): Option[A] = computable.now().toOption val update: Observer[I] = Observer { input => memo.update(_ => Some(input)) load(input) } - val reload: Observer[Unit] = Observer(_ => memo.now().foreach(load)) + val reload: Observer[ReloadableComponent.Reload[A]] = Observer { + case ReloadableComponent.Reload.Once => memo.now().foreach(load) + case ReloadableComponent.Reload.UntilChanged(original) => + memo.now().foreach(reloadUntilChanged(_, original)) + } - def initMod: HtmlMod = EventStream.fromValue(()) --> reload + def initMod: HtmlMod = + EventStream.fromValue(ReloadableComponent.Reload.Once) --> reload - def load(input: I): Unit = + def load(input: I): Unit = doLoad( + input, + fetch(_).retry(loadSchedule) + ) + + def reloadUntilChanged(input: I, original: A): Unit = + doLoad( + input, + fetch(_) + .repeat(loadSchedule && Schedule.recurWhile(_ == original)) + .map(_._2) + ) + + private def doLoad( + input: I, + q: I => IO[UserMessage, A] + ): Unit = computable.update(_.started) // TODO: do we need to manage the result of the run? val _ = Unsafe.unsafely { runtime.unsafe.runOrFork( - fetch(input).retry(loadSchedule).fold( + q(input).fold( msg => computable.update(_.fail(msg)), result => computable.update(_.update(result)) ) @@ -40,12 +62,18 @@ } object ReloadableComponent: + enum Reload[+A]: + case Once extends Reload[Nothing] + case UntilChanged(a: A) extends Reload[A] + def apply[A](fetch: IO[UserMessage, A])(using runtime: Runtime[Any] ): ReloadableComponent[A, Unit] = ReloadableComponent(_ => fetch, Some(())) - def apply[A, I](endpoint: PublicEndpoint[I, Unit, A, Any]): URIO[ClientEndpointFactory, ReloadableComponent[A, I]] = + def apply[A, I]( + endpoint: PublicEndpoint[I, Unit, A, Any] + ): URIO[ClientEndpointFactory, ReloadableComponent[A, I]] = for given Runtime[Any] <- ZIO.runtime[Any] factory <- ZIO.service[ClientEndpointFactory] diff --git a/ui/shared/src/main/scala/works/iterative/ui/model/HtmlUIBuilder.scala b/ui/shared/src/main/scala/works/iterative/ui/model/HtmlUIBuilder.scala index 9551d23..9686c74 100644 --- a/ui/shared/src/main/scala/works/iterative/ui/model/HtmlUIBuilder.scala +++ b/ui/shared/src/main/scala/works/iterative/ui/model/HtmlUIBuilder.scala @@ -2,6 +2,7 @@ import works.iterative.core.* import zio.prelude.* +import com.raquo.airstream.core.Signal trait HtmlUIBuilder[Node, Context]: type Ctx = Context @@ -46,11 +47,18 @@ type Id = String type Title = Output type Subtitle = Option[Output] - type Actions = List[Action] type Content = Reader[Any, Output] type Footer = Option[Output] type Status = Vector[Output] + enum Actions: + case Direct(actions: List[Action]) + case Deferred(actions: Signal[List[Action]]) + + object Actions: + given Conversion[List[Action], Actions] = Direct(_) + given Conversion[Signal[List[Action]], Actions] = Deferred(_) + trait UIInterpreter: def render(el: UIElement): Rendered def withConfig(config: UIConfig): UIInterpreter