diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LaminarExtensions.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LaminarExtensions.scala index 71e8529..81a64d1 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LaminarExtensions.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LaminarExtensions.scala @@ -8,8 +8,13 @@ import com.raquo.laminar.nodes.ChildNode.Base import works.iterative.ui.components.ComponentContext import works.iterative.ui.components.laminar.HtmlRenderable +import com.raquo.airstream.custom.CustomStreamSource +import com.raquo.airstream.custom.CustomSource +import zio.IsSubtypeOfError -object LaminarExtensions: +object LaminarExtensions extends I18NExtensions with ZIOInteropExtensions + +trait I18NExtensions: extension (msg: UserMessage) inline def asElement(using ctx: ComponentContext[_]): HtmlElement = span(msg.asMod) @@ -31,3 +36,52 @@ given (using ComponentContext[_]): HtmlRenderable[UserMessage] with def toHtml(msg: UserMessage): Modifier[HtmlElement] = msg.asElement + +trait ZIOInteropExtensions: + import zio.{ZIO, Runtime, Unsafe, Fiber, Cause} + + extension [R, E, O](effect: ZIO[R, E, O]) + def toEventStream(using runtime: Runtime[R])(using + ev: E IsSubtypeOfError Throwable + ): EventStream[O] = + var fiberRuntime: Fiber.Runtime[E, O] = null + EventStream.fromCustomSource( + start = (fireValue, fireError, getStartIndex, getIsStarted) => + Unsafe.unsafe { implicit unsafe => + fiberRuntime = runtime.unsafe.fork(effect) + fiberRuntime.unsafe.addObserver(exit => + exit.foldExit(cause => fireError(cause.squash), fireValue) + ) + }, + stop = _ => + if fiberRuntime != null then + Unsafe.unsafe { implicit unsafe => + runtime.unsafe.run(fiberRuntime.interrupt).ignore + } + fiberRuntime = null + else () + ) + + def toEventStreamWith(mapError: E => Throwable)(using + runtime: Runtime[R] + ): EventStream[O] = + var fiberRuntime: Fiber.Runtime[E, O] = null + EventStream.fromCustomSource( + start = (fireValue, fireError, getStartIndex, getIsStarted) => + Unsafe.unsafe { implicit unsafe => + fiberRuntime = runtime.unsafe.fork(effect) + fiberRuntime.unsafe.addObserver(exit => + exit.foldExit( + cause => fireError(cause.squashWith(mapError)), + fireValue + ) + ) + }, + stop = _ => + if fiberRuntime != null then + Unsafe.unsafe { implicit unsafe => + runtime.unsafe.run(fiberRuntime.interrupt).ignore + } + fiberRuntime = null + else () + ) diff --git a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LaminarExtensions.scala b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LaminarExtensions.scala index 71e8529..81a64d1 100644 --- a/ui/js/src/main/scala/works/iterative/ui/components/laminar/LaminarExtensions.scala +++ b/ui/js/src/main/scala/works/iterative/ui/components/laminar/LaminarExtensions.scala @@ -8,8 +8,13 @@ import com.raquo.laminar.nodes.ChildNode.Base import works.iterative.ui.components.ComponentContext import works.iterative.ui.components.laminar.HtmlRenderable +import com.raquo.airstream.custom.CustomStreamSource +import com.raquo.airstream.custom.CustomSource +import zio.IsSubtypeOfError -object LaminarExtensions: +object LaminarExtensions extends I18NExtensions with ZIOInteropExtensions + +trait I18NExtensions: extension (msg: UserMessage) inline def asElement(using ctx: ComponentContext[_]): HtmlElement = span(msg.asMod) @@ -31,3 +36,52 @@ given (using ComponentContext[_]): HtmlRenderable[UserMessage] with def toHtml(msg: UserMessage): Modifier[HtmlElement] = msg.asElement + +trait ZIOInteropExtensions: + import zio.{ZIO, Runtime, Unsafe, Fiber, Cause} + + extension [R, E, O](effect: ZIO[R, E, O]) + def toEventStream(using runtime: Runtime[R])(using + ev: E IsSubtypeOfError Throwable + ): EventStream[O] = + var fiberRuntime: Fiber.Runtime[E, O] = null + EventStream.fromCustomSource( + start = (fireValue, fireError, getStartIndex, getIsStarted) => + Unsafe.unsafe { implicit unsafe => + fiberRuntime = runtime.unsafe.fork(effect) + fiberRuntime.unsafe.addObserver(exit => + exit.foldExit(cause => fireError(cause.squash), fireValue) + ) + }, + stop = _ => + if fiberRuntime != null then + Unsafe.unsafe { implicit unsafe => + runtime.unsafe.run(fiberRuntime.interrupt).ignore + } + fiberRuntime = null + else () + ) + + def toEventStreamWith(mapError: E => Throwable)(using + runtime: Runtime[R] + ): EventStream[O] = + var fiberRuntime: Fiber.Runtime[E, O] = null + EventStream.fromCustomSource( + start = (fireValue, fireError, getStartIndex, getIsStarted) => + Unsafe.unsafe { implicit unsafe => + fiberRuntime = runtime.unsafe.fork(effect) + fiberRuntime.unsafe.addObserver(exit => + exit.foldExit( + cause => fireError(cause.squashWith(mapError)), + fireValue + ) + ) + }, + stop = _ => + if fiberRuntime != null then + Unsafe.unsafe { implicit unsafe => + runtime.unsafe.run(fiberRuntime.interrupt).ignore + } + fiberRuntime = null + else () + ) diff --git a/ui/js/src/test/scala/works/iterative/ui/components/laminar/ZIOLaminarInteropSpec.scala b/ui/js/src/test/scala/works/iterative/ui/components/laminar/ZIOLaminarInteropSpec.scala new file mode 100644 index 0000000..8bde2d2 --- /dev/null +++ b/ui/js/src/test/scala/works/iterative/ui/components/laminar/ZIOLaminarInteropSpec.scala @@ -0,0 +1,25 @@ +package works.iterative.ui.components.laminar + +import zio.* +import zio.test.* +import com.raquo.airstream.core.EventStream +import com.raquo.airstream.core.Observer +import com.raquo.airstream.ownership.Owner + +object ZIOLaminarInteropSpec extends ZIOSpecDefault: + override def spec: Spec[TestEnvironment & Scope, Any] = + suite("ZIO-Laminar interop should")( + test("run a ZIO effect to EventStream") { + import LaminarExtensions.* + + given runtime: Runtime[Any] = Runtime.default + given owner: Owner = new Owner { + def killAll(): Unit = this.killSubscriptions() + } + val ev: EventStream[String] = ZIO.succeed("Hello").toEventStream + val buffer = collection.mutable.Buffer[String]() + val subscription = ev.foreach(buffer += _) + subscription.kill() + assertTrue(buffer.size == 1, buffer.head == "Hello") + } + )