Skip to content

ZIO / FS2 / etc. integration examples #140

@raquo

Description

@raquo

Laminar does not and will not have built-in integrations with ZIO / FS2 / etc., but anyone who actually uses these libraries on the frontend can easily publish their own packages or even just share a gist. If you need help developing such a package, feel free to ask in our Discord – although I personally don't use ZIO / FS2 / etc. especially on the frontend, a few other people do.

I'm opening this ticket as a reference for any such integration examples until I have a more permanent place to put them. Please feel free to comment with more example code or links.

To start off, here is an example ZIO integration from @sherpal that he posted on discord:

package ziojs
import com.raquo.laminar.api.L.*
import zio.{Unsafe, ZIO}

package object laminarinterrop {

  /** Executes asynchronously the effect when the element is mounted. */
  def onMountZIO[El <: Element](zio: ZIO[services.GlobalEnv, Nothing, Unit]): Modifier[El] =
    onMountZIOWithContext(_ => zio)

  def onUnmountZIO[El <: Element](effect: ZIO[services.GlobalEnv, Nothing, Unit]): Modifier[El] =
    onUnmountCallback(_ =>
      Unsafe.unsafe { implicit unsafe =>
        services.runtime.unsafe.runToFuture(effect)
        ()
      }
    )

  /** Executes asynchronously the effect when the element is mounted. */
  def onMountZIOWithContext[El <: Element](
      effect: MountContext[El] => ZIO[services.GlobalEnv, Nothing, Unit]
  ): Modifier[El] =
    onMountCallback[El](ctx =>
      Unsafe.unsafe { implicit unsafe: Unsafe =>
        services.runtime.unsafe.runToFuture(effect(ctx))
        ()
      }
    )

  def onClickZIO[El <: Element](zio: ZIO[services.GlobalEnv, Throwable, Unit]): Binder[El] = {
    import Implicits.ObserverEnhanced
    onClick.mapTo(()) --> Observer.zio(zio)
  }

}

and

package ziojs.laminarinterrop

import com.raquo.laminar.api.A.*
import zio.stream.*
import zio.{CancelableFuture, URIO, Unsafe, ZIO}
import services.GlobalEnv

import scala.concurrent.Future

object Implicits {

  import services.runtime

  type InnerForGlobalEnv[A] = URIO[GlobalEnv, A]

  implicit val zioFlattenStrategy: FlattenStrategy[Observable, InnerForGlobalEnv, EventStream] =
    new FlattenStrategy[Observable, InnerForGlobalEnv, EventStream] {
      def flatten[A](parent: Observable[InnerForGlobalEnv[A]]): EventStream[A] =
        parent.flatMap(task =>
          EventStream.fromFuture(
            Unsafe.unsafe { implicit unsafe =>
              services.runtime.unsafe.runToFuture(task)
            },
            emitFutureIfCompleted = true
          )
        )
    }

  implicit class EventStreamObjEnhanced(es: EventStream.type) {

    /** Retrieve the result of the zio effect and send it through the laminar stream */
    def fromZIOEffect[A](effect: ZIO[GlobalEnv, Throwable, A]): EventStream[A] =
      EventStream.fromFuture(
        Unsafe.unsafe { implicit unsafe =>
          runtime.unsafe.runToFuture(effect)
        }: Future[A],
        emitFutureIfCompleted = true
      )

    /** Passes the outputs of the incoming [[zio.stream.ZStream]] into a laminar stream. /!\ The
      * ZStream will continue to run, even after the laminar stream is over with it. The returned
      * [[zio.CancelableFuture]] allows you to cancel it.
      *
      * I think this is not fantastic. We should do it in another way, probably.
      */
    def fromZStream[A](ztream: Stream[Nothing, A]): (CancelableFuture[Unit], EventStream[A]) = {
      val bus = new EventBus[A]

      val f: zio.CancelableFuture[Unit] = Unsafe.unsafe { implicit unsafe =>
        zio.Runtime.default.unsafe
          .runToFuture(ztream.foreach(elem => ZIO.attempt(bus.writer.onNext(elem))))
      }

      f -> bus.events
    }

  }

  implicit class EventStreamEnhanced[A](es: EventStream[A]) {
    def flatMapZIO[B](effect: ZIO[GlobalEnv, Nothing, B]): EventStream[B] =
      es.flatMap(_ => effect)
  }

  implicit class ObserverEnhanced(val observer: Observer.type) extends AnyVal {
    def zio(effect: ZIO[GlobalEnv, Throwable, Unit]): Observer[Any] = observer.apply[Any] { _ =>
      Unsafe.unsafe { implicit unsafe =>
        runtime.unsafe.runToFuture(effect)
        ()
      }
    }

    def zioFromFunction[A](effect: A => ZIO[GlobalEnv, Throwable, Unit]): Observer[A] =
      observer[A] { elem =>
        Unsafe.unsafe { implicit unsafe =>
          runtime.unsafe.runToFuture(effect(elem))
          ()
        }
      }
  }

  implicit class RichLaminarZIO[R, E, A](val zio: ZIO[R, E, A]) extends AnyVal {

    /** Takes the output of this ZIO effect and pipe it to the [[Var]], keeping the value. */
    def setToVar(laminarVar: Var[A]): ZIO[R, E, A] = zio.tap(a => ZIO.succeed(laminarVar.set(a)))
  }

  implicit class RichVar[A](val theVar: Var[A]) extends AnyVal {
    def setZIO(a: => A): ZIO[Any, Nothing, Unit] = ZIO.succeed(theVar.set(a))
  }

}

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions