Skip to content

Commit

Permalink
Merge pull request #192 from armanbilge/update/cats-effect-3.5.0-RC1
Browse files Browse the repository at this point in the history
Update to Cats Effect v3.5.0-RC1
  • Loading branch information
armanbilge authored Feb 14, 2023
2 parents fe7f4de + 4230c11 commit 5799248
Show file tree
Hide file tree
Showing 12 changed files with 49 additions and 120 deletions.
5 changes: 3 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ ThisBuild / githubWorkflowJavaVersions := Seq(JavaSpec.temurin("17"))
ThisBuild / tlJdkRelease := Some(8)

val CatsVersion = "2.9.0"
val CatsEffectVersion = "3.4.6"
val CatsEffectVersion = "3.5.0-RC1"
val Fs2Version = "3.6.1"
val Fs2DomVersion = "0.2.0-RC1"
val MonocleVersion = "3.2.0"
Expand Down Expand Up @@ -128,7 +128,8 @@ lazy val unidocs = project
.enablePlugins(ScalaJSPlugin, TypelevelUnidocPlugin)
.settings(
name := "calico-docs",
ScalaUnidoc / unidoc / unidocProjectFilter := inProjects(frp.js, calico, router)
ScalaUnidoc / unidoc / unidocProjectFilter := inProjects(frp.js, calico, router),
ScalaUnidoc / unidoc / fullClasspath := (todoMvc / Compile / fullClasspath).value
)

lazy val jsdocs = project.dependsOn(calico, router).enablePlugins(ScalaJSPlugin)
Expand Down
16 changes: 7 additions & 9 deletions calico/src/main/scala/calico/html/Children.scala
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ private trait ChildrenModifiers[F[_]](using F: Async[F]):
n.appendChild(dom.document.createComment(""))
}
}
_ <- tail
update = tail
.foreach { children =>
hs.swap(children) { (prev, next) =>
F.delay {
Expand All @@ -104,7 +104,7 @@ private trait ChildrenModifiers[F[_]](using F: Async[F]):
}
.compile
.drain
.cedeBackground
_ <- (F.cede *> update).background
yield ()

final class KeyedChildren[F[_], K] private[calico] (f: K => Resource[F, fs2.dom.Node[F]]):
Expand All @@ -128,7 +128,7 @@ private trait KeyedChildrenModifiers[F[_]](using F: Async[F]):
val n = _n.asInstanceOf[dom.Node]
inline def build(k: K) = m.build(k).asInstanceOf[Resource[F, dom.Node]]
for
(head, tail) <- m.keys.getAndDiscreteUpdates
(head, tail) <- m.keys.changes.getAndDiscreteUpdates
active <- Resource.makeFull[F, Ref[F, mutable.Map[K, (dom.Node, F[Unit])]]] { poll =>
def go(keys: List[K], active: mutable.Map[K, (dom.Node, F[Unit])]): F[Unit] =
if keys.isEmpty then F.unit
Expand All @@ -143,12 +143,10 @@ private trait KeyedChildrenModifiers[F[_]](using F: Async[F]):
.flatTap(active => go(head, active).onCancel(traverse_(active.values)(_._2)))
.flatMap(F.ref(_))
}(
_.get.flatMap(ns => traverse_(ns.values)(_._2)).evalOn(unsafe.MacrotaskExecutor)
F.cede *> _.get.flatMap(ns => traverse_(ns.values)(_._2))
)
sentinel <- Resource.eval(F.delay(n.appendChild(dom.document.createComment(""))))
_ <- tail
.dropWhile(_ === head)
.changes
update = tail
.foreach { keys =>
F.uncancelable { poll =>
active.get.flatMap { currentNodes =>
Expand All @@ -173,13 +171,13 @@ private trait KeyedChildrenModifiers[F[_]](using F: Async[F]):
}

(active.set(nextNodes) *> acquireNewNodes *> renderNextNodes).guarantee(
releaseOldNodes.evalOn(unsafe.MacrotaskExecutor)
F.cede *> releaseOldNodes
)
}.flatten
}
}
}
.compile
.drain
.cedeBackground
_ <- (F.cede *> update).background
yield ()
11 changes: 5 additions & 6 deletions calico/src/main/scala/calico/html/DomHotswap.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@
package calico
package html

import cats.effect.kernel.Async
import cats.effect.kernel.Concurrent
import cats.effect.kernel.Resource
import cats.effect.syntax.all.*
import cats.syntax.all.*

private[calico] abstract class DomHotswap[F[_], A]:
private abstract class DomHotswap[F[_], A]:
def swap(next: Resource[F, A])(render: (A, A) => F[Unit]): F[Unit]

private[calico] object DomHotswap:
private object DomHotswap:
def apply[F[_], A](init: Resource[F, A])(
using F: Async[F]
using F: Concurrent[F]
): Resource[F, (DomHotswap[F, A], A)] =
Resource.make(init.allocated.flatMap(F.ref(_)))(_.get.flatMap(_._2)).evalMap { active =>
val hs = new DomHotswap[F, A]:
Expand All @@ -36,8 +36,7 @@ private[calico] object DomHotswap:
nextAllocated <- poll(next.allocated)
(oldA, oldFinalizer) <- active.getAndSet(nextAllocated)
newA = nextAllocated._1
_ <- render(oldA, newA)
_ <- oldFinalizer.evalOn(unsafe.MacrotaskExecutor)
_ <- render(oldA, newA) *> F.cede *> oldFinalizer
yield ()
}

Expand Down
20 changes: 9 additions & 11 deletions calico/src/main/scala/calico/html/Modifier.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,21 @@ object Modifier:
def contramap[A, B](fa: Modifier[Id, Any, A])(f: B => A) =
fa.contramap(f)

private[html] def forSignal[F[_]: Async, E, M, V](signal: M => Signal[F, V])(
mkModify: (M, E) => V => F[Unit]): Modifier[F, E, M] = (m, e) =>
private[html] def forSignal[F[_], E, M, V](signal: M => Signal[F, V])(
mkModify: (M, E) => V => F[Unit])(using F: Async[F]): Modifier[F, E, M] = (m, e) =>
signal(m).getAndDiscreteUpdates.flatMap { (head, tail) =>
val modify = mkModify(m, e)
Resource.eval(modify(head)) *>
tail.foreach(modify(_)).compile.drain.cedeBackground.void
(F.cede *> tail.foreach(modify(_)).compile.drain).background.void
}

private[html] def forSignalResource[F[_]: Async, E, M, V](
signal: M => Resource[F, Signal[F, V]])(
mkModify: (M, E) => V => F[Unit]): Modifier[F, E, M] = (m, e) =>
private[html] def forSignalResource[F[_], E, M, V](signal: M => Resource[F, Signal[F, V]])(
mkModify: (M, E) => V => F[Unit])(using F: Async[F]): Modifier[F, E, M] = (m, e) =>
signal(m).flatMap { sig =>
sig.getAndDiscreteUpdates.flatMap { (head, tail) =>
val modify = mkModify(m, e)
Resource.eval(modify(head)) *>
tail.foreach(modify(_)).compile.drain.cedeBackground.void
(F.cede *> tail.foreach(modify(_)).compile.drain).background.void
}
}

Expand Down Expand Up @@ -89,7 +88,7 @@ private trait Modifiers[F[_]](using F: Async[F]):
Resource
.eval(F.delay(e.appendChild(dom.document.createTextNode(head))))
.flatMap { n =>
tail.foreach(t => F.delay(n.textContent = t)).compile.drain.cedeBackground
(F.cede *> tail.foreach(t => F.delay(n.textContent = t)).compile.drain).background
}
.void
}
Expand Down Expand Up @@ -126,11 +125,10 @@ private trait Modifiers[F[_]](using F: Async[F]):
n2s.getAndDiscreteUpdates.flatMap { (head, tail) =>
DomHotswap(head).flatMap { (hs, n2) =>
F.delay(n.appendChild(n2)).toResource *>
tail
(F.cede *> tail
.foreach(hs.swap(_)((n2, n3) => F.delay(n.replaceChild(n3, n2))))
.compile
.drain
.cedeBackground
.drain).background
}.void
}

Expand Down
3 changes: 2 additions & 1 deletion calico/src/main/scala/calico/html/Prop.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import cats.FunctorFilter
import cats.Id
import cats.effect.kernel.Async
import cats.effect.kernel.Resource
import cats.effect.syntax.all.*
import cats.syntax.all.*
import fs2.Pipe
import fs2.Stream
Expand Down Expand Up @@ -178,7 +179,7 @@ private trait EventPropModifiers[F[_]](using F: Async[F]):
inline given forPipeEventProp[T <: fs2.dom.Node[F]]: Modifier[F, T, PipeModifier[F]] =
_forPipeEventProp.asInstanceOf[Modifier[F, T, PipeModifier[F]]]
private val _forPipeEventProp: Modifier[F, dom.EventTarget, PipeModifier[F]] =
(m, t) => fs2.dom.events(t, m.key).through(m.sink).compile.drain.cedeBackground.void
(m, t) => (F.cede *> fs2.dom.events(t, m.key).through(m.sink).compile.drain).background.void

final class ClassProp[F[_]] private[calico]
extends Prop[F, List[String], String](
Expand Down
16 changes: 0 additions & 16 deletions calico/src/main/scala/calico/syntax.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,6 @@ extension [F[_]](component: Resource[F, fs2.dom.Node[F]])
def renderInto(root: fs2.dom.Node[F])(using Functor[F], Dom[F]): Resource[F, Unit] =
component.flatMap { e => Resource.make(root.appendChild(e))(_ => root.removeChild(e)) }

extension [F[_], A](fa: F[A])
private[calico] def cedeBackground(
using F: Async[F]): Resource[F, F[Outcome[F, Throwable, A]]] =
F.executionContext.toResource.flatMap { ec =>
Resource
.make(F.deferred[Fiber[F, Throwable, A]])(_.get.flatMap(_.cancel))
.evalTap { deferred =>
fa.start
.flatMap(deferred.complete(_))
.evalOn(ec)
.startOn(unsafe.MacrotaskExecutor)
.start
}
.map(_.get.flatMap(_.join))
}

extension [F[_], A](sigRef: SignallingRef[F, A])
def zoom[B](lens: Lens[A, B])(using Functor[F]): SignallingRef[F, B] =
SignallingRef.lens[F, A, B](sigRef)(lens.get(_), a => b => lens.replace(b)(a))
19 changes: 0 additions & 19 deletions calico/src/main/scala/calico/unsafe/MacrotaskExecutor.scala

This file was deleted.

31 changes: 0 additions & 31 deletions calico/src/main/scala/calico/unsafe/MicrotaskExecutor.scala

This file was deleted.

17 changes: 9 additions & 8 deletions calico/src/main/scala/calico/unsafe/runtime.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ package calico.unsafe

import cats.effect.unsafe.IORuntime
import cats.effect.unsafe.IORuntimeConfig
import cats.effect.unsafe.Scheduler

given IORuntime = IORuntime(
MicrotaskExecutor,
MicrotaskExecutor,
Scheduler.createDefaultScheduler()._1,
() => (),
IORuntimeConfig()
)
given IORuntime = // never auto-cede, to prevent glitchy rendering
val ec = IORuntime.createBatchingMacrotaskExecutor(Int.MaxValue)
IORuntime(
ec,
ec,
IORuntime.defaultScheduler,
() => (),
IORuntimeConfig(1, Int.MaxValue)
)
18 changes: 7 additions & 11 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,19 +118,15 @@ A JavaScript webapp typically has a flow like:

Notice that this scheduling strategy guarantees glitch-free rendering. Because all tasks triggered by an event must complete before the view re-renders, the user will never see inconsistent state in the UI.

However, there are certain situations where running a task with high-priority may not be desirable and you would prefer that it runs in the "background" while your application continues to be responsive. This is relevant only if you are doing an expensive calculation or processing task; for example, there is no need to explicitly background I/O tasks since they operate via the event-driven flow described above.

In these cases, you should break that expensive task into smaller steps and schedule it as a so-called _macrotask_:
However, there are certain situations where you may want the browser to re-render in the middle of a task. In these cases, simply sequence an `IO.cede` operation. This will temporarily yield control flow back to the browser so that it may re-render the UI, before resuming the task.

```scala
import calico.unsafe.MacrotaskExecutor

val expensiveTask = IO(smallStep1()) *> IO(smallStep2()) *> IO(smallStep3()) *> ...
expensiveTask.evalOn(MacrotaskExecutor)
updateComponentA *> // doesn't render yet
updateComponentB *> // still didn't render
IO.cede *> // re-render now
doOtherStuff *> ... // do non-view-related work
```

The [`MacrotaskExecutor`](https://github.com/scala-js/scala-js-macrotask-executor) schedules macrotasks with equal priority to event processing and UI rendering. Conceptually, it is somewhat analogous to using `IO.blocking(...)` on the JVM, in that running these tasks on a separate `ExecutionContext` preserves fairness and enables your application to continue responding to incoming events. Conversely, forgetting to use `.evalOn(MacrotaskExecutor)` or `IO.blocking(...)` could cause your application to become unresponsive.

However, I suspect situations where you need to use the `MacrotaskExecutor` in webapp are rare. If you truly have a long-running, compute-intensive task that you do not want to compromise the responsiveness of your application, you should consider running it in a background thread via a [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) instead.
Explicitly inserting an `IO.cede` can be a useful strategy to improve your app’s UX, by re-rendering as soon as you are done updating the view, and deferring other work until after the re-render. This will make your UI more responsive.

To learn more about microtasks and macrotasks I recommend [this article about the JavaScript event loop](https://javascript.info/event-loop).
To learn more I recommend [this article about the JavaScript event loop](https://javascript.info/event-loop).
9 changes: 5 additions & 4 deletions router/src/main/scala/calico/router/Router.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

package calico.router

import cats.effect.kernel.Async
import cats.effect.kernel.Concurrent
import cats.effect.kernel.RefSink
import cats.effect.kernel.Resource
import cats.effect.kernel.Unique
Expand Down Expand Up @@ -80,7 +80,7 @@ abstract class Router[F[_]] private ():
Resource.eval(routes).flatMap(dispatch)

object Router:
def apply[F[_]: Dom](window: Window[F])(using F: Async[F]): F[Router[F]] =
def apply[F[_]: Dom](window: Window[F])(using F: Concurrent[F]): F[Router[F]] =
Topic[F, Uri].map { gps =>
val history = window.history[Unit]
new:
Expand Down Expand Up @@ -120,7 +120,8 @@ object Router:
F.uncancelable { _ =>
container.removeChild(oldChild) *>
currentRoute.set(None) *>
finalizer.evalOn(MacrotaskExecutor)
F.cede *>
finalizer
}
case (None, Some(route)) =>
F.uncancelable { poll =>
Expand All @@ -138,7 +139,7 @@ object Router:
case ((sink, child), newFinalizer) =>
container.replaceChild(child, oldChild) *>
currentRoute.set(Some((route.key, child, sink, newFinalizer)))
} *> oldFinalizer.evalOn(MacrotaskExecutor)
} *> F.cede *> oldFinalizer
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions todo-mvc/src/main/scala/todomvc/TodoMvc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,10 @@ object TodoStore:

_ <- mapRef
.discrete
.foreach(todos => window.localStorage.setItem(key, todos.asJson.noSpaces))
.foreach(todos => IO.cede *> window.localStorage.setItem(key, todos.asJson.noSpaces))
.compile
.drain
.backgroundOn(calico.unsafe.MacrotaskExecutor)
.background
yield new TodoStore(mapRef, IO.realTime.map(_.toMillis))

case class Todo(text: String, completed: Boolean) derives Codec.AsObject
Expand Down

0 comments on commit 5799248

Please sign in to comment.