Repository: 7mind/izumi
Pull request #841 (2020-01-19 04:06:37)

Update jawn-parser to 1.0.0
Updates [org.typelevel:jawn-parser](https://github.com/typelevel/jawn) [from 0.14.3 to 1.0.0](https://github.com/typelevel/jawn/compare/v0.14.3...v1.0.0).
[Release Notes/Changelog](https://github.com/typelevel/jawn/releases/tag/v1.0.0)

I'll automatically update this PR to resolve conflicts as long as you don't change it yourself.

If you'd like to skip this version, you can just close this PR. If you have any feedback, just mention me in the comments below.

Have a fantastic day writing Scala!

<details>
<summary>Ignore future updates</summary>

Add this to your `.scala-steward.conf` file to ignore future updates of this dependency:
```
updates.ignore = [ { groupId = "org.typelevel", artifactId = "jawn-parser" } ]
```
</details>

labels: library-update, semver-major


Patch:
diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml
index 1d3de543c..54f60df3b 100644
--- a/.azure-pipelines.yml
+++ b/.azure-pipelines.yml
@@ -7,6 +7,7 @@ trigger:
   branches:
     include:
       - develop
+      - series/*
 
 variables:
   - group: security-tokens
diff --git a/.build.sh b/.build.sh
index a410f50f4..85b08be76 100644
--- a/.build.sh
+++ b/.build.sh
@@ -66,7 +66,7 @@ function publishIDL {
     return 0
   fi
 
-  if [[ ! ("$CI_BRANCH" == "develop" || "$CI_TAG" =~ ^v.*$ ) ]] ; then
+  if [[ ! ("$CI_BRANCH" == "develop" || "$CI_BRANCH" == "zio-RC16" || "$CI_TAG" =~ ^v.*$ ) ]] ; then
     return 0
   fi
   #copypaste
@@ -90,13 +90,13 @@ function publishScala {
     return 0
   fi
 
-  if [[ ! ("$CI_BRANCH" == "develop" || "$CI_TAG" =~ ^v.*$ ) ]] ; then
+  if [[ ! ("$CI_BRANCH" == "develop" || "$CI_BRANCH" == "zio-RC16" || "$CI_TAG" =~ ^v.*$ ) ]] ; then
     return 0
   fi
 
   echo "PUBLISH SCALA LIBRARIES..."
 
-  if [[ "$CI_BRANCH" == "develop" ]] ; then
+  if [[ "$CI_BRANCH" == "develop" || "$CI_BRANCH" == "zio-RC16" ]] ; then
     csbt "'$VERSION_COMMAND clean'" "'$VERSION_COMMAND package'" "'$VERSION_COMMAND publishSigned'" || exit 1
   else
     csbt "'$VERSION_COMMAND clean'" "'$VERSION_COMMAND package'" "'$VERSION_COMMAND publishSigned'" sonatypeBundleRelease || exit 1
@@ -106,7 +106,7 @@ function publishScala {
 function init {
     echo "=== INIT ==="
     export LC_ALL="C.UTF-8"
-    
+
     if [[ "$SYSTEM_PULLREQUEST_PULLREQUESTNUMBER" == ""  ]] ; then
         export CI_PULL_REQUEST=false
     else
diff --git a/README.md b/README.md
index 68e6de401..8e99231ed 100644
--- a/README.md
+++ b/README.md
@@ -53,6 +53,7 @@ Example projects:
 Support Chats:
 
 * [Izumi on Gitter](https://gitter.im/7mind/izumi)
+* [Izumi User Group [EN] on Telegram](https://t.me/izumi_en)
 * [Izumi User Group [RU] on Telegram](https://t.me/izumi_ru)
 
 Slides:
diff --git a/build.sbt b/build.sbt
index 8ba7b2717..c8b941958 100644
--- a/build.sbt
+++ b/build.sbt
@@ -3458,7 +3458,7 @@ lazy val `izumi` = (project in file("."))
       "-deprecation",
       "-language:higherKinds",
       "-Ybackend-parallelism",
-      math.max(1, sys.runtime.availableProcessors() - 1).toString,
+      math.min(16, math.max(1, sys.runtime.availableProcessors() - 1)).toString,
       "-explaintypes",
       "-Ycache-plugin-class-loader:always",
       "-Ycache-macro-class-loader:last-modified"
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/AnyConstructorMacro.scala b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/AnyConstructorMacro.scala
index 8135cc76d..97dfa8d08 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/AnyConstructorMacro.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/AnyConstructorMacro.scala
@@ -91,7 +91,7 @@ object AnyConstructorMacro {
     val macroUniverse = StaticDIUniverse(c)
     val reflectionProvider = ReflectionProviderDefaultImpl(macroUniverse)
 
-    val targetType = ReflectionUtil.norm(c.universe: c.universe.type)(weakTypeOf[T])
+    val targetType = ReflectionUtil.norm(c.universe: c.universe.type)(weakTypeOf[T].dealias)
     requireConcreteTypeConstructor(c)("AnyConstructor", targetType)
 
     if (reflectionProvider.isConcrete(targetType)) {
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/ClassConstructorMacro.scala b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/ClassConstructorMacro.scala
index a5ed08aea..6254e8d7e 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/ClassConstructorMacro.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/ClassConstructorMacro.scala
@@ -16,7 +16,7 @@ object ClassConstructorMacro {
   def mkClassConstructor[T: c.WeakTypeTag](c: blackbox.Context): c.Expr[ClassConstructor[T]] = {
     import c.universe._
 
-    val targetType = ReflectionUtil.norm(c.universe: c.universe.type)(weakTypeOf[T])
+    val targetType = ReflectionUtil.norm(c.universe: c.universe.type)(weakTypeOf[T].dealias)
     requireConcreteTypeConstructor(c)("ClassConstructor", targetType)
 
     targetType match {
@@ -28,7 +28,7 @@ object ClassConstructorMacro {
           case _ => q"${t.termSymbol}"
         }
         c.Expr[ClassConstructor[T]] {
-          q"{ new ${weakTypeOf[ClassConstructor[T]]}($providerMagnet.pure[$targetType]($term)) }"
+          q"{ new ${weakTypeOf[ClassConstructor[T]]}($providerMagnet.singleton[$targetType]($term)) }"
         }
 
       case _ =>
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/FactoryConstructorMacro.scala b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/FactoryConstructorMacro.scala
index e4373bf0a..f21eca7a8 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/FactoryConstructorMacro.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/FactoryConstructorMacro.scala
@@ -25,7 +25,7 @@ object FactoryConstructorMacro {
     import macroUniverse.Wiring._
     import macroUniverse._
 
-    val targetType = ReflectionUtil.norm(c.universe: c.universe.type)(weakTypeOf[T])
+    val targetType = ReflectionUtil.norm(c.universe: c.universe.type)(weakTypeOf[T].dealias)
     requireConcreteTypeConstructor(c)("FactoryConstructor", targetType)
 
     val factory@Factory(factoryMethods, factoryTraitDependencies) = reflectionProvider.symbolToWiring(targetType)
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/TraitConstructorMacro.scala b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/TraitConstructorMacro.scala
index a25ceb812..d2ac0253d 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/TraitConstructorMacro.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/constructors/macros/TraitConstructorMacro.scala
@@ -16,7 +16,7 @@ object TraitConstructorMacro {
   def mkTraitConstructor[T: c.WeakTypeTag](c: blackbox.Context): c.Expr[TraitConstructor[T]] = {
     import c.universe._
 
-    val targetType = ReflectionUtil.norm(c.universe: c.universe.type)(weakTypeOf[T])
+    val targetType = ReflectionUtil.norm(c.universe: c.universe.type)(weakTypeOf[T].dealias)
     requireConcreteTypeConstructor(c)("FactoryConstructor", targetType)
 
     val macroUniverse = StaticDIUniverse(c)
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/definition/DIResource.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/definition/DIResource.scala
index 7d4229ab6..da20d9dd0 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/definition/DIResource.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/definition/DIResource.scala
@@ -497,7 +497,7 @@ object DIResource {
       new AdaptProvider[ZManaged[R, E, A]] {
         type Out = DIResource.FromZIO[R, E, A]
 
-        override def apply(a: ProviderMagnet[ZManaged[R, E, A]])(implicit tag: ResourceTag[Out]): ProviderMagnet[FromZIO[R, E, A]] = {
+        override def apply(a: ProviderMagnet[ZManaged[R, E, A]])(implicit tag: ResourceTag[DIResource.FromZIO[R, E, A]]): ProviderMagnet[FromZIO[R, E, A]] = {
           import tag.tagFull
           a.map(fromZIO)
         }
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/definition/ImplDef.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/definition/ImplDef.scala
index 530a71f5b..9e546f8d4 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/definition/ImplDef.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/definition/ImplDef.scala
@@ -7,7 +7,7 @@ sealed trait ImplDef {
   def implType: SafeType
 
   override final def toString: String = BindingFormatter(KeyFormatter.Full).formatImplDef(this)
-  override lazy val hashCode: Int = scala.util.hashing.MurmurHash3.productHash(this.asInstanceOf[Product])
+  override final lazy val hashCode: Int = scala.util.hashing.MurmurHash3.productHash(this.asInstanceOf[Product])
 }
 
 object ImplDef {
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/effect/DIEffect.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/effect/DIEffect.scala
index 6b6e7dd92..3e5c3d658 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/effect/DIEffect.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/effect/DIEffect.scala
@@ -24,11 +24,11 @@ trait DIEffect[F[_]] {
     * this will _also_ intercept Throwable defects in `ZIO`, not only typed errors */
   def definitelyRecover[A](action: => F[A])(recover: Throwable => F[A]): F[A]
 
-  /** `definitelyRecover`, but the Throwable is not guaranteed to be
-    * the original Throwable, it can be a Throwable enhanced with a
-    * Monad's own debugging information (e.g. in case of `ZIO` it's always `FiberFailure`)
+  /** `definitelyRecover`, but second Throwable is not the original Throwable,
+    * it can be a Throwable enhanced with an effect's own debugging information
+    * (e.g. in case of `ZIO` it's always `FiberFailure`)
     */
-  def definitelyRecoverCause[A](action: => F[A])(recoverCause: Throwable => F[A]): F[A]
+  def definitelyRecoverCause[A](action: => F[A])(recoverCause: (Throwable, Throwable) => F[A]): F[A]
 
   def fail[A](t: => Throwable): F[A]
 
@@ -74,8 +74,8 @@ object DIEffect extends LowPriorityDIEffectInstances {
         case t: Throwable => recover(t)
       }
     }
-    override def definitelyRecoverCause[A](action: => Identity[A])(recoverCause: Throwable => Identity[A]): Identity[A] = {
-      definitelyRecover(action)(recoverCause)
+    override def definitelyRecoverCause[A](action: => Identity[A])(recoverCause: (Throwable, Throwable) => Identity[A]): Identity[A] = {
+      definitelyRecover(action)(e => recoverCause(e, e))
     }
     override def bracket[A, B](acquire: => Identity[A])(release: A => Identity[Unit])(use: A => Identity[B]): Identity[B] = {
       val a = acquire
@@ -106,8 +106,8 @@ object DIEffect extends LowPriorityDIEffectInstances {
       override def definitelyRecover[A](action: => F[E, A])(recover: Throwable => F[E, A]): F[E, A] = {
         suspendF(action).sandbox.catchAll(recover apply _.toThrowable)
       }
-      override def definitelyRecoverCause[A](action: => F[Throwable, A])(recover: Throwable => F[Throwable, A]): F[Throwable, A] = {
-        suspendF(action).sandbox.catchAll(recover apply _.trace.toThrowable)
+      override def definitelyRecoverCause[A](action: => F[Throwable, A])(recover: (Throwable, Throwable) => F[Throwable, A]): F[Throwable, A] = {
+        suspendF(action).sandbox.catchAll(e => recover(e.toThrowable, e.trace.toThrowable))
       }
 
       override def fail[A](t: => Throwable): F[Throwable, A] = F.fail(t)
@@ -146,8 +146,8 @@ private[effect] sealed trait LowPriorityDIEffectInstances {
       override def definitelyRecover[A](action: => F[A])(recover: Throwable => F[A]): F[A] = {
         F.handleErrorWith(F.suspend(action))(recover)
       }
-      override def definitelyRecoverCause[A](action: => F[A])(recoverCause: Throwable => F[A]): F[A] = {
-        definitelyRecover(action)(recoverCause)
+      override def definitelyRecoverCause[A](action: => F[A])(recoverCause: (Throwable, Throwable) => F[A]): F[A] = {
+        definitelyRecover(action)(e => recoverCause(e, e))
       }
       override def fail[A](t: => Throwable): F[A] = F.suspend(F.raiseError(t))
       override def bracket[A, B](acquire: => F[A])(release: A => F[Unit])(use: A => F[B]): F[B] = {
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/effect/DIEffectAsync.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/effect/DIEffectAsync.scala
index 5cb27b728..5ad7e690f 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/effect/DIEffectAsync.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/effect/DIEffectAsync.scala
@@ -20,6 +20,8 @@ object DIEffectAsync extends LowPriorityDIEffectAsyncInstances {
 
   implicit val diEffectParIdentity: DIEffectAsync[Identity] = {
     new DIEffectAsync[Identity] {
+      final val DIEffectAsyncIdentityPool = ExecutionContext.fromExecutorService(null)
+
       override def parTraverse_[A](l: Iterable[A])(f: A => Unit): Unit = {
         parTraverse(l)(f)
         ()
@@ -29,9 +31,7 @@ object DIEffectAsync extends LowPriorityDIEffectAsyncInstances {
       }
 
       override def parTraverse[A, B](l: Iterable[A])(f: A => Identity[B]): Identity[List[B]] = {
-        implicit val ec = ExecutionContext.global
-        val future = Future.sequence(l.map(a => Future(f(a))))
-        Await.result(future, Duration.Inf).toList
+        parTraverseIdentity(DIEffectAsyncIdentityPool)(l)(f)
       }
     }
   }
@@ -50,6 +50,12 @@ object DIEffectAsync extends LowPriorityDIEffectAsyncInstances {
     }
   }
 
+  private[izumi] def parTraverseIdentity[A, B](ec0: ExecutionContext)(l: Iterable[A])(f: A => Identity[B]): Identity[List[B]] = {
+    implicit val ec = ec0
+    val future = Future.sequence(l.map(a => Future(scala.concurrent.blocking(f(a)))))
+    Await.result(future, Duration.Inf).toList
+  }
+
 }
 
 private[effect] sealed trait LowPriorityDIEffectAsyncInstances {
diff --git a/distage/distage-core-api/src/main/scala/izumi/distage/model/providers/ProviderMagnet.scala b/distage/distage-core-api/src/main/scala/izumi/distage/model/providers/ProviderMagnet.scala
index 432628463..afe280a31 100644
--- a/distage/distage-core-api/src/main/scala/izumi/distage/model/providers/ProviderMagnet.scala
+++ b/distage/distage-core-api/src/main/scala/izumi/distage/model/providers/ProviderMagnet.scala
@@ -2,8 +2,9 @@ package izumi.distage.model.providers
 
 import izumi.distage.model.exceptions.TODOBindingException
 import izumi.distage.model.reflection.macros.ProviderMagnetMacro
+import izumi.distage.model.reflection.universe.RuntimeDIUniverse.Association.Parameter
 import izumi.distage.model.reflection.universe.RuntimeDIUniverse._
-import izumi.fundamentals.platform.language.CodePositionMaterializer
+import izumi.fundamentals.platform.language.{CodePositionMaterializer, unused}
 import izumi.fundamentals.platform.language.Quirks._
 import izumi.fundamentals.reflection.Tags.Tag
 
@@ -89,7 +90,8 @@ final case class ProviderMagnet[+A](get: Provider) {
   }
 
   def zip[B: Tag](that: ProviderMagnet[B]): ProviderMagnet[(A, B)] = {
-    implicit val rTag: Tag[A] = Tag(that.get.ret.cls, that.get.ret.tag); rTag.discard() // scalac can't detect usage in TagMacro assembling Tag[(R, B)] below
+    implicit val rTag: Tag[A] = getRetTag
+    rTag.discard() // used for assembling Tag[(A, B)] below
     copy[(A, B)](get = get.unsafeZip(SafeType.get[(A, B)], that.get))
   }
 
@@ -97,18 +99,26 @@ final case class ProviderMagnet[+A](get: Provider) {
     zip(that).map[C](f.tupled)
   }
 
-  /** Applicative's `ap` method - can be used to chain transformations similarly to `flatMap` */
+  /** Applicative's `ap` method - can be used to chain transformations like `flatMap` */
   def flatAp[B: Tag](that: ProviderMagnet[A => B]): ProviderMagnet[B] = {
-    implicit val rTag: Tag[A] = Tag(that.get.ret.cls, that.get.ret.tag); rTag.discard() // scalac can't detect usage in TagMacro assembling Tag[(R, B)] below
-    map2(that) { case (a, f) => f(a) }
+    implicit val rTag: Tag[A] = getRetTag
+    rTag.discard() // used for assembling Tag[A => B] below
+    map2[A => B, B](that) { case (a, f) => f(a) }
   }
 
-  /** Add `B` as an unused dependency for this constructor */
+  /** Apply a function produced by `this` Provider to the argument produced by `that` Provider */
+  def ap[B, C](that: ProviderMagnet[B])(implicit @unused ev: A <:< (B => C), tag: Tag[C]): ProviderMagnet[C] = {
+    that.flatAp[C](this.asInstanceOf[ProviderMagnet[B => C]])
+  }
+
+  /** Add `B` as an unused dependency of this constructor */
   def addDependency[B: Tag]: ProviderMagnet[A] = addDependency(DIKey.get[B])
 
   def addDependency(key: DIKey): ProviderMagnet[A] = addDependencies(key :: Nil)
 
   def addDependencies(keys: Seq[DIKey]): ProviderMagnet[A] = copy[A](get = get.addUnused(keys))
+
+  @inline private[this] def getRetTag: Tag[A] = Tag(get.ret.cls, get.ret.tag)
 }
 
 object ProviderMagnet {
@@ -161,23 +171,53 @@ object ProviderMagnet {
     )
   }
 
-  def identityKey(key: DIKey): ProviderMagnet[_] = {
+  def single[A: Tag, B: Tag](f: A => B): ProviderMagnet[B] = {
+    val key = DIKey.get[A]
     val tpe = key.tpe
-    val symbolInfo = SymbolInfo(
-      name = "x$1",
-      finalResultType = tpe,
-      isByName = false,
-      wasGeneric = false
+    val retTpe = SafeType.get[B]
+    val symbolInfo = firstParamSymbolInfo(tpe)
+
+    new ProviderMagnet[B](
+      Provider.ProviderImpl(
+        parameters = Seq(Parameter(symbolInfo, key)),
+        ret = retTpe,
+        fun = (s: Seq[Any]) => f(s.head.asInstanceOf[A]),
+        isGenerated = false,
+      )
+    )
+  }
+
+  def singleton[A <: Singleton: Tag](a: A): ProviderMagnet[A] = {
+    new ProviderMagnet[A](
+      Provider.ProviderImpl[A](
+        parameters = Seq.empty,
+        ret = SafeType.get[A],
+        fun = (_: Seq[Any]) => a,
+        isGenerated = true,
+      )
     )
+  }
+
+  def identityKey(key: DIKey): ProviderMagnet[_] = {
+    val tpe = key.tpe
+    val symbolInfo = firstParamSymbolInfo(tpe)
 
     new ProviderMagnet(
       Provider.ProviderImpl(
-        parameters = Seq(Association.Parameter(symbolInfo, key)),
-        ret = key.tpe,
+        parameters = Seq(Parameter(symbolInfo, key)),
+        ret = tpe,
         fun = (_: Seq[Any]).head,
         isGenerated = false,
       )
     )
   }
 
+  @inline private[this] def firstParamSymbolInfo(tpe: SafeType): SymbolInfo = {
+    SymbolInfo(
+      name = "x$1",
+      finalResultType = tpe,
+      isByName = false,
+      wasGeneric = false
+    )
+  }
 }
diff --git a/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/PluginConfig.scala b/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/PluginConfig.scala
index 72004cd6e..f418ed868 100644
--- a/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/PluginConfig.scala
+++ b/distage/distage-extension-plugins/src/main/scala/izumi/distage/plugins/PluginConfig.scala
@@ -41,3 +41,5 @@ object PluginConfig {
       .asBoolean(true)
   }
 }
+
+
diff --git a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/DockerContainer.scala b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/DockerContainer.scala
index 233a19898..ac8da99e2 100644
--- a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/DockerContainer.scala
+++ b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/DockerContainer.scala
@@ -123,7 +123,7 @@ object DockerContainer {
         for {
           containers <- DIEffect[F]
             .maybeSuspend {
-              // FIXME: temporary hack to allow missing containers to skip tests
+              // FIXME: temporary hack to allow missing containers to skip tests (happens when both DockerWrapper & integration check that depends on Docker.Container are memoized)
               try {
                 client.listContainersCmd()
                   .withAncestorFilter(List(config.image).asJava)
diff --git a/distage/distage-framework/src/main/scala/izumi/distage/framework/services/RoleAppPlanner.scala b/distage/distage-framework/src/main/scala/izumi/distage/framework/services/RoleAppPlanner.scala
index 4c67f01f5..c2f8ae90f 100644
--- a/distage/distage-framework/src/main/scala/izumi/distage/framework/services/RoleAppPlanner.scala
+++ b/distage/distage-framework/src/main/scala/izumi/distage/framework/services/RoleAppPlanner.scala
@@ -26,7 +26,7 @@ object RoleAppPlanner {
                           bsModule: BootstrapModule,
                           logger: IzLogger,
                         ) extends RoleAppPlanner[F] { self =>
-    private[this] val injector = Injector.Standard(bsModule)
+    private[this] val injector = Injector(bsModule)
 
     override def reboot(bsOverride: BootstrapModule): RoleAppPlanner[F] = {
       new RoleAppPlanner.Impl[F](options, bsModule overridenBy bsOverride, logger)
diff --git a/doc/microsite/src/main/tut/distage/00_distage.md b/doc/microsite/src/main/tut/distage/00_distage.md
index a4858d5f5..5d48f90fc 100644
--- a/doc/microsite/src/main/tut/distage/00_distage.md
+++ b/doc/microsite/src/main/tut/distage/00_distage.md
@@ -5,11 +5,11 @@ out: index.html
 distage: Staged Dependency Injection
 ====================================
 
-`distage` is a pragmatic module system for functional Scala, it aims to combine simplicity and expressiveness of pure FP
+`distage` is a pragmatic module system for FP Scala & Scala.js. It combines simplicity and expressiveness of pure FP
 with the flexibility and extreme late-binding, traditionally associated with object-oriented dependency injection frameworks, such as Guice.
 
-`distage` is suitable for wiring applications in @ref[Tagless Final Style](basics.md#tagless-final-style),
-in @ref[ZIO Environment](basics.md#auto-traits) style, or in imperative Scala style.
+`distage` is suitable for wiring @ref[Tagless Final Style](basics.md#tagless-final-style),
+@ref[ZIO Environment](basics.md#auto-traits) style, and imperative Scala style applications.
 
 Why distage?
 ------------
diff --git a/doc/microsite/src/main/tut/distage/basics.md b/doc/microsite/src/main/tut/distage/basics.md
index 39e51a35e..ca5126f2f 100644
--- a/doc/microsite/src/main/tut/distage/basics.md
+++ b/doc/microsite/src/main/tut/distage/basics.md
@@ -719,7 +719,7 @@ First, the program we want to write:
 
 ```scala mdoc:reset:to-string
 import cats.Monad
-import cats.effect.{ExitCode, Sync, IO}
+import cats.effect.{Sync, IO}
 import cats.syntax.all._
 import distage.{GCMode, Module, ModuleDef, Injector, Tag, TagK, TagKK}
 
@@ -744,7 +744,7 @@ class TaglessProgram[F[_]: Monad: Validation: Interaction] {
   } yield ()
 }
 
-def ProgramModule[F[_]: TagK: Monad]: Module = new ModuleDef {
+def ProgramModule[F[_]: TagK: Monad] = new ModuleDef {
   make[TaglessProgram[F]]
   addImplicit[Monad[F]]
 }
@@ -851,45 +851,55 @@ consult @scaladoc[HKTag](izumi.fundamentals.reflection.WithTags#HKTag) docs for
 
 ### Cats & ZIO Integration
 
-Cats & ZIO instances and syntax are available automatically without imports, if `cats-core`, `cats-effect` or `zio` are
-already dependencies of your project. (Note: distage *won't* bring `cats` or `zio` as a dependency if you don't already use them.
-See [No More Orphans](https://blog.7mind.io/no-more-orphans.html) for description of the technique)
+Cats & ZIO instances and syntax are available automatically in `distage-core`, without wildcard imports, if your project depends on `cats-core`, `cats-effect` or `zio`.
+But distage *won't* bring in `cats` or `zio` as dependencies if you don't already depend on them.
+([No More Orphans](https://blog.7mind.io/no-more-orphans.html) blog post details how that works)
 
-@ref[Cats Resource Bindings](basics.md#resource-bindings-lifecycle) will also work out of the box without any magic imports.
+@ref[Cats Resource & ZIO ZManaged Bindings](basics.md#resource-bindings-lifecycle) also work out of the box without any magic imports.
 
 Example:
 
-```scala mdoc:invisible:to-string
+```scala mdoc:reset:invisible:to-string
+import distage.Module
 class DBConnection
 object DBConnection {
   def create[F[_]]: F[DBConnection] = ???
 }
+def ProgramModule[F[_]]: Module = Module.empty
+def SyncInterpreters[F[_]]: Module = Module.empty
 ```
 
 ```scala mdoc:to-string
-import cats.effect.IOApp
-import distage.DIKey
+import cats.syntax.semigroup._
+import cats.effect.{ExitCode, IO, IOApp}
+import distage.{DIKey, GCMode, Injector}
 
 trait AppEntrypoint {
   def run: IO[Unit]
 }
 
-object Main extends IOApp {
+object Main extends App {
   def run(args: List[String]): IO[ExitCode] = {
-    // ModuleDef has a Monoid instance
+    
+    // `distage.Module` has a Monoid instance
+
     val myModules = ProgramModule[IO] |+| SyncInterpreters[IO]
+
     val plan = Injector().plan(myModules, GCMode(DIKey.get[AppEntrypoint]))
 
     for {
       // resolveImportsF can effectfully add missing instances to an existing plan
       // (You can also create instances effectfully inside `ModuleDef` via `make[_].fromEffect` bindings)
+
       newPlan <- plan.resolveImportsF[IO] {
         case i if i.target == DIKey.get[DBConnection] =>
            DBConnection.create[IO]
-      } 
+      }
+
       // `produceF` specifies an Effect to run in.
       // Effects used in Resource and Effect Bindings 
       // should match the effect in `produceF`
+
       _ <- Injector().produceF[IO](newPlan).use {
         classes =>
           classes.get[AppEntrypoint].run
diff --git a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/HttpServer.scala b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/HttpServer.scala
index 19d5c9384..470353737 100644
--- a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/HttpServer.scala
+++ b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/HttpServer.scala
@@ -21,17 +21,16 @@ import org.http4s.server.websocket.WebSocketBuilder
 import org.http4s.websocket.WebSocketFrame
 import org.http4s.websocket.WebSocketFrame.{Binary, Close, Pong, Text}
 
-class HttpServer[C <: Http4sContext]
-(
-  val c: C#IMPL[C]
-, val muxer: IRTServerMultiplexor[C#BiIO, C#RequestContext, C#MethodContext]
-, val codec: IRTClientMultiplexor[C#BiIO]
-, val contextProvider: AuthMiddleware[C#MonoIO, C#RequestContext]
-, val wsContextProvider: WsContextProvider[C#BiIO, C#RequestContext, C#ClientId]
-, val wsSessionStorage: WsSessionsStorage[C#BiIO, C#ClientId, C#RequestContext]
-, val listeners: Seq[WsSessionListener[C#ClientId]]
-, logger: IzLogger
-, printer: Printer
+class HttpServer[C <: Http4sContext](
+  val c: C#IMPL[C],
+  val muxer: IRTServerMultiplexor[C#BiIO, C#RequestContext, C#MethodContext],
+  val codec: IRTClientMultiplexor[C#BiIO],
+  val contextProvider: AuthMiddleware[C#MonoIO, C#RequestContext],
+  val wsContextProvider: WsContextProvider[C#BiIO, C#RequestContext, C#ClientId],
+  val wsSessionStorage: WsSessionsStorage[C#BiIO, C#ClientId, C#RequestContext],
+  val listeners: Seq[WsSessionListener[C#BiIO, C#ClientId]],
+  logger: IzLogger,
+  printer: Printer
 ) {
 
   import c._
@@ -67,15 +66,15 @@ class HttpServer[C <: Http4sContext]
   }
 
   protected def handler(): PartialFunction[AuthedRequest[MonoIO, RequestContext], MonoIO[Response[MonoIO]]] = {
-    case request@GET -> Root / "ws" as ctx =>
+    case request @ GET -> Root / "ws" as ctx =>
       val result = setupWs(request, ctx)
       result
 
-    case request@GET -> Root / service / method as ctx =>
+    case request @ GET -> Root / service / method as ctx =>
       val methodId = IRTMethodId(IRTServiceId(service), IRTMethodName(method))
       run(new HttpRequestContext(request, ctx), body = "{}", methodId)
 
-    case request@POST -> Root / service / method as ctx =>
+    case request @ POST -> Root / service / method as ctx =>
       val methodId = IRTMethodId(IRTServiceId(service), IRTMethodName(method))
       request.req.decode[String] {
         body =>
@@ -83,62 +82,55 @@ class HttpServer[C <: Http4sContext]
       }
   }
 
-  protected def handleWsClose(context: WebsocketClientContext[C#BiIO, C#ClientId, C#RequestContext]): Unit = {
-    logger.debug(s"${context -> null}: Websocket client disconnected")
+  protected def handleWsClose(context: WebsocketClientContext[C#BiIO, C#ClientId, C#RequestContext]): MonoIO[Unit] = {
+    F.sync(logger.debug(s"${context -> null}: Websocket client disconnected")) *>
     context.finish()
   }
 
-  protected def onWsOpened(): Unit = {
-  }
+  protected def onWsOpened(): MonoIO[Unit] = F.unit
 
-  protected def onWsUpdate(maybeNewId: Option[C#ClientId], old: WsClientId[ClientId]): Unit = {
+  protected def onWsUpdate(maybeNewId: Option[C#ClientId], old: WsClientId[ClientId]): MonoIO[Unit] = F.sync {
     (maybeNewId, old).forget
   }
 
-  protected def onWsClosed(): Unit = {
-  }
+  protected def onWsClosed(): MonoIO[Unit] = F.unit
 
   protected def setupWs(request: AuthedRequest[MonoIO, RequestContext], initialContext: RequestContext): MonoIO[Response[MonoIO]] = {
     val context = new WebsocketClientContextImpl[C](c, request, initialContext, listeners, wsSessionStorage, logger) {
-
-      override def onWsSessionOpened(): Unit = {
-          onWsOpened()
-          super.onWsSessionOpened()
+      override def onWsSessionOpened(): C#MonoIO[Unit] = {
+        onWsOpened() *> super.onWsSessionOpened()
       }
-
-      override def onWsClientIdUpdate(maybeNewId: Option[C#ClientId], oldId: WsClientId[C#ClientId]): Unit = {
-        onWsUpdate(maybeNewId, oldId)
-        super.onWsClientIdUpdate(maybeNewId, oldId)
+      override def onWsClientIdUpdate(maybeNewId: Option[C#ClientId], oldId: WsClientId[C#ClientId]): C#MonoIO[Unit] = {
+        onWsUpdate(maybeNewId, oldId) *> super.onWsClientIdUpdate(maybeNewId, oldId)
       }
-
-      override def onWsSessionClosed(): Unit = {
-        onWsClosed()
-        super.onWsSessionClosed()
+      override def onWsSessionClosed(): C#MonoIO[Unit] = {
+        onWsClosed() *> super.onWsSessionClosed()
       }
     }
-    context.start()
-    logger.debug(s"${context -> null}: Websocket client connected")
-
-    context.queue.flatMap[Throwable, Response[MonoIO]] { q =>
-      val dequeueStream = q.dequeue.through {
-        stream =>
-          stream
-            .evalMap(handleWsMessage(context))
-            .collect { case Some(v) => WebSocketFrame.Text(v) }
+    for {
+      _ <- context.start()
+      _ <- F.sync(logger.debug(s"${context -> null}: Websocket client connected"))
+      response <- context.queue.flatMap[Throwable, Response[MonoIO]] {
+        q =>
+          val dequeueStream = q.dequeue.through {
+            stream =>
+              stream
+                .evalMap(handleWsMessage(context))
+                .collect { case Some(v) => WebSocketFrame.Text(v) }
+          }
+          val enqueueSink = q.enqueue
+          WebSocketBuilder[MonoIO].build(
+            send = dequeueStream.merge(context.outStream).merge(context.pingStream),
+            receive = enqueueSink,
+            onClose = handleWsClose(context)
+          )
       }
-      val enqueueSink = q.enqueue
-      WebSocketBuilder[MonoIO].build(
-        send = dequeueStream.merge(context.outStream).merge(context.pingStream)
-      , receive = enqueueSink
-      , onClose = F.syncThrowable(handleWsClose(context))
-      )
-    }
+    } yield response
   }
 
   protected def handleWsMessage(context: WebsocketClientContextImpl[C], requestTime: ZonedDateTime = IzTime.utcNow): WebSocketFrame => MonoIO[Option[String]] = {
     case Text(msg, _) =>
-      makeResponse(context, msg)
-        .sandboxBIOExit
+      makeResponse(context, msg).sandboxBIOExit
         .map(handleResult(context, _))
 
     case Close(_) =>
@@ -178,7 +170,7 @@ class HttpServer[C <: Http4sContext]
       parsed <- F.fromEither(parse(message))
       unmarshalled <- F.fromEither(parsed.as[RpcPacket])
       id <- wsContextProvider.toId(context.initialContext, context.id, unmarshalled)
-      _ <- F.syncThrowable(context.updateId(id))
+      _ <- context.updateId(id)
       response <- respond(context, unmarshalled).sandbox.catchAll {
         case BIOExit.Termination(exception, allExceptions, trace) =>
           logger.error(s"${context -> null}: WS processing terminated, $message, $exception, $allExceptions, $trace")
@@ -195,8 +187,7 @@ class HttpServer[C <: Http4sContext]
       case RpcPacket(RPCPacketKind.RpcRequest, None, _, _, _, _, _) =>
         wsContextProvider.handleEmptyBodyPacket(context.id, context.initialContext, input).flatMap {
           case (id, eff) =>
-          context.updateId(id)
-          eff
+            context.updateId(id) *> eff
         }
 
       case RpcPacket(RPCPacketKind.RpcRequest, Some(data), Some(id), _, Some(service), Some(method), _) =>
@@ -219,7 +210,7 @@ class HttpServer[C <: Http4sContext]
         context.requestState.handleResponse(id, data).as(None)
 
       case RpcPacket(RPCPacketKind.BuzzFailure, Some(data), _, Some(id), _, _, _) =>
-        context.requestState.respond(id, RawResponse.BadRawResponse())
+        F.sync(context.requestState.respond(id, RawResponse.BadRawResponse())) *>
         F.fail(new IRTGenericFailure(s"Buzzer has returned failure: $data"))
 
       case k =>
@@ -239,7 +230,6 @@ class HttpServer[C <: Http4sContext]
     }
   }
 
-
   protected def run(context: HttpRequestContext[MonoIO, RequestContext], body: String, method: IRTMethodId): MonoIO[Response[MonoIO]] = {
     val ioR = for {
       parsed <- F.fromEither(parse(body))
@@ -252,7 +242,11 @@ class HttpServer[C <: Http4sContext]
       .flatMap(handleResult(context, method, _))
   }
 
-  private def handleResult(context: HttpRequestContext[MonoIO, RequestContext], method: IRTMethodId, result: BIOExit[Throwable, Option[Json]]): BiIO[Throwable, Response[MonoIO]] = {
+  private def handleResult(
+    context: HttpRequestContext[MonoIO, RequestContext],
+    method: IRTMethodId,
+    result: BIOExit[Throwable, Option[Json]]
+  ): C#MonoIO[Response[MonoIO]] = {
     result match {
       case Success(v) =>
         v match {
diff --git a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/WithWebsocketClientContext.scala b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/WithWebsocketClientContext.scala
index e76085998..ac9c58690 100644
--- a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/WithWebsocketClientContext.scala
+++ b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/main/scala/izumi/idealingua/runtime/rpc/http4s/WithWebsocketClientContext.scala
@@ -4,74 +4,77 @@ import java.time.ZonedDateTime
 import java.util.concurrent.atomic.AtomicReference
 import java.util.concurrent.{ConcurrentHashMap, ConcurrentLinkedDeque, TimeUnit, TimeoutException}
 
+import fs2.concurrent.Queue
+import io.circe.Json
+import io.circe.syntax._
+import izumi.functional.bio.BIOApplicative
 import izumi.fundamentals.platform.language.Quirks
 import izumi.fundamentals.platform.time.IzTime
 import izumi.fundamentals.platform.uuid.UUIDGen
 import izumi.idealingua.runtime.rpc._
-import fs2.concurrent.Queue
-import io.circe.Json
-import io.circe.syntax._
 import logstage.IzLogger
 import org.http4s.AuthedRequest
 import org.http4s.websocket.WebSocketFrame
 import org.http4s.websocket.WebSocketFrame.{Ping, Text}
 
-import scala.jdk.CollectionConverters._
 import scala.concurrent.duration._
+import scala.jdk.CollectionConverters._
 
-trait WebsocketClientContext[B[+ _, + _], ClientId, Ctx] {
-  def requestState: RequestState[B]
+trait WebsocketClientContext[F[+_, +_], ClientId, Ctx] {
+  def requestState: RequestState[F]
 
   def id: WsClientId[ClientId]
 
-  def enqueue(method: IRTMethodId, data: Json): RpcPacketId
+  def enqueue(method: IRTMethodId, data: Json): F[Throwable, RpcPacketId]
 
-  def finish(): Unit
+  def finish(): F[Throwable, Unit]
 }
 
-trait WsSessionsStorage[B[+ _, + _], ClientId, Ctx] {
-  def addClient(id: WsSessionId, ctx: WebsocketClientContext[B, ClientId, Ctx]): WebsocketClientContext[B, ClientId, Ctx]
+trait WsSessionsStorage[F[+_, +_], ClientId, Ctx] {
+  def addClient(id: WsSessionId, ctx: WebsocketClientContext[F, ClientId, Ctx]): F[Throwable, WebsocketClientContext[F, ClientId, Ctx]]
 
-  def deleteClient(id: WsSessionId): WebsocketClientContext[B, ClientId, Ctx]
+  def deleteClient(id: WsSessionId): F[Throwable, WebsocketClientContext[F, ClientId, Ctx]]
 
-  def allClients(): Seq[WebsocketClientContext[B, ClientId, Ctx]]
+  def allClients(): F[Throwable, Seq[WebsocketClientContext[F, ClientId, Ctx]]]
 
-  def buzzersFor(clientId: ClientId): Option[IRTDispatcher[B]]
+  def buzzersFor(clientId: ClientId): F[Throwable, Option[IRTDispatcher[F]]]
 }
 
-trait WsSessionListener[ClientId] {
-  def onSessionOpened(context: WsClientId[ClientId]): Unit
+trait WsSessionListener[F[+_, +_], ClientId] {
+  def onSessionOpened(context: WsClientId[ClientId]): F[Throwable, Unit]
 
-  def onClientIdUpdate(context: WsClientId[ClientId], old: WsClientId[ClientId]): Unit
+  def onClientIdUpdate(context: WsClientId[ClientId], old: WsClientId[ClientId]): F[Throwable, Unit]
 
-  def onSessionClosed(context: WsClientId[ClientId]): Unit
+  def onSessionClosed(context: WsClientId[ClientId]): F[Throwable, Unit]
 }
 
 object WsSessionListener {
-  def empty[ClientId]: WsSessionListener[ClientId] = new WsSessionListener[ClientId] {
-    override def onSessionOpened(context: WsClientId[ClientId]): Unit = {}
-
-    override def onSessionClosed(context: WsClientId[ClientId]): Unit = {}
-
-    override def onClientIdUpdate(context: WsClientId[ClientId], old: WsClientId[ClientId]): Unit = {}
+  def empty[F[+_, +_]: BIOApplicative, ClientId]: WsSessionListener[F, ClientId] = new WsSessionListener[F, ClientId] {
+    import izumi.functional.bio.F
+    override def onSessionOpened(context: WsClientId[ClientId]): F[Throwable, Unit] = F.unit
+    override def onClientIdUpdate(context: WsClientId[ClientId], old: WsClientId[ClientId]): F[Throwable, Unit] = F.unit
+    override def onSessionClosed(context: WsClientId[ClientId]): F[Throwable, Unit] = F.unit
   }
 }
 
+trait WsContextProvider[F[+_, +_], Ctx, ClientId] {
+  def toContext(id: WsClientId[ClientId], initial: Ctx, packet: RpcPacket): F[Throwable, Ctx]
 
-trait WsContextProvider[B[+ _, + _], Ctx, ClientId] {
-  def toContext(id: WsClientId[ClientId], initial: Ctx, packet: RpcPacket): B[Throwable, Ctx]
-
-  def toId(initial: Ctx, currentId: WsClientId[ClientId], packet: RpcPacket): B[Throwable, Option[ClientId]]
+  def toId(initial: Ctx, currentId: WsClientId[ClientId], packet: RpcPacket): F[Throwable, Option[ClientId]]
 
   // TODO: we use this to mangle with authorization but it's dirty
-  def handleEmptyBodyPacket(id: WsClientId[ClientId], initial: Ctx, packet: RpcPacket): B[Throwable, (Option[ClientId], B[Throwable, Option[RpcPacket]])]
+  def handleEmptyBodyPacket(id: WsClientId[ClientId], initial: Ctx, packet: RpcPacket): F[Throwable, (Option[ClientId], F[Throwable, Option[RpcPacket]])]
 }
 
 class IdContextProvider[C <: Http4sContext](val c: C#IMPL[C]) extends WsContextProvider[C#BiIO, C#RequestContext, C#ClientId] {
 
   import c._
 
-  override def handleEmptyBodyPacket(id: WsClientId[ClientId], initial: C#RequestContext, packet: RpcPacket): C#BiIO[Throwable, (Option[ClientId], C#BiIO[Throwable, Option[RpcPacket]])] = {
+  override def handleEmptyBodyPacket(
+    id: WsClientId[ClientId],
+    initial: C#RequestContext,
+    packet: RpcPacket
+  ): C#BiIO[Throwable, (Option[ClientId], C#BiIO[Throwable, Option[RpcPacket]])] = {
     Quirks.discard(id, initial, packet)
     F.pure((None, F.pure(None)))
   }
@@ -81,17 +84,16 @@ class IdContextProvider[C <: Http4sContext](val c: C#IMPL[C]) extends WsContextP
     F.pure(initial)
   }
 
-  override def toId(initial:  C#RequestContext, currentId:  WsClientId[C#ClientId], packet:  RpcPacket): C#BiIO[Throwable, Option[ClientId]] = {
+  override def toId(initial: C#RequestContext, currentId: WsClientId[C#ClientId], packet: RpcPacket): C#BiIO[Throwable, Option[ClientId]] = {
     Quirks.discard(initial, packet)
     F.pure(None)
   }
 }
 
-class WsSessionsStorageImpl[C <: Http4sContext]
-(
-  val c: C#IMPL[C]
-  , logger: IzLogger
-  , codec: IRTClientMultiplexor[C#BiIO]
+class WsSessionsStorageImpl[C <: Http4sContext](
+  val c: C#IMPL[C],
+  logger: IzLogger,
+  codec: IRTClientMultiplexor[C#BiIO]
 ) extends WsSessionsStorage[C#BiIO, C#ClientId, C#RequestContext] {
 
   import c._
@@ -103,70 +105,70 @@ class WsSessionsStorageImpl[C <: Http4sContext]
   protected val timeout: FiniteDuration = 20.seconds
   protected val pollingInterval: FiniteDuration = 50.millis
 
-  def addClient(id: WsSessionId, ctx: WSC): WSC = {
+  override def addClient(
+    id: WsSessionId,
+    ctx: WebsocketClientContext[C#BiIO, C#ClientId, C#RequestContext]
+  ): C#BiIO[Throwable, WebsocketClientContext[C#BiIO, C#ClientId, C#RequestContext]] = F.sync {
     logger.debug(s"Adding a client with session - $id")
     clients.put(id, ctx)
   }
 
-  def deleteClient(id: WsSessionId): WSC = {
+  override def deleteClient(id: WsSessionId): C#BiIO[Throwable, WebsocketClientContext[C#BiIO, C#ClientId, C#RequestContext]] = F.sync {
     logger.debug(s"Deleting a client with session - $id")
     clients.remove(id)
   }
 
-  def allClients(): Seq[WSC] = {
+  override def allClients(): C#BiIO[Throwable, Seq[WebsocketClientContext[C#BiIO, C#ClientId, C#RequestContext]]] = F.sync {
     clients.values().asScala.toSeq
   }
 
-  def buzzersFor(clientId: ClientId): Option[IRTDispatcher[BiIO]] = {
-    val buzzerClient = allClients()
-      .find(_.id.id.contains(clientId))
-
-    logger.debug(s"Asked for buzzer for $clientId. Found: $buzzerClient")
-
-    buzzerClient
-      .map {
-        session =>
-          new IRTDispatcher[BiIO] {
-            override def dispatch(request: IRTMuxRequest): BiIO[Throwable, IRTMuxResponse] = {
-              for {
-                json <- codec.encode(request)
-                id <- F.sync(session.enqueue(request.method, json))
-                resp <- F.bracket(F.pure(id)) {
-                  id =>
-                    logger.debug(s"${request.method -> "method"}, ${id -> "id"}: cleaning request state")
-                    F.sync(session.requestState.forget(id))
-                } {
-                  id =>
-                    session.requestState.poll(id, pollingInterval, timeout).flatMap {
-                      case Some(value: RawResponse.GoodRawResponse) =>
-                        logger.debug(s"${request.method -> "method"}, $id: Have response: $value")
-                        codec.decode(value.data, value.method)
-
-                      case Some(value: RawResponse.BadRawResponse) =>
-                        logger.debug(s"${request.method -> "method"}, $id: Generic failure response: $value")
-                        F.fail(new IRTGenericFailure(s"${request.method -> "method"}, $id: generic failure: $value"))
-
-                      case None =>
-                        F.fail(new TimeoutException(s"${request.method -> "method"}, $id: No response in $timeout"))
-                    }
+  override def buzzersFor(clientId: ClientId): C#BiIO[Throwable, Option[IRTDispatcher[BiIO]]] = {
+    allClients().map(_.find(_.id.id.contains(clientId))).map {
+      buzzerClient =>
+        logger.debug(s"Asked for buzzer for $clientId. Found: $buzzerClient")
+        buzzerClient.map {
+          session =>
+            new IRTDispatcher[BiIO] {
+              override def dispatch(request: IRTMuxRequest): BiIO[Throwable, IRTMuxResponse] = {
+                for {
+                  json <- codec.encode(request)
+                  id <- session.enqueue(request.method, json)
+                  resp <- F.bracket(F.pure(id)) {
+                    id =>
+                      logger.debug(s"${request.method -> "method"}, ${id -> "id"}: cleaning request state")
+                      F.sync(session.requestState.forget(id))
+                  } {
+                    id =>
+                      session.requestState.poll(id, pollingInterval, timeout).flatMap {
+                        case Some(value: RawResponse.GoodRawResponse) =>
+                          logger.debug(s"${request.method -> "method"}, $id: Have response: $value")
+                          codec.decode(value.data, value.method)
+
+                        case Some(value: RawResponse.BadRawResponse) =>
+                          logger.debug(s"${request.method -> "method"}, $id: Generic failure response: $value")
+                          F.fail(new IRTGenericFailure(s"${request.method -> "method"}, $id: generic failure: $value"))
+
+                        case None =>
+                          F.fail(new TimeoutException(s"${request.method -> "method"}, $id: No response in $timeout"))
+                      }
+                  }
+                } yield {
+                  resp
                 }
-              } yield {
-                resp
               }
             }
-          }
-      }
+        }
+    }
   }
 }
 
-class WebsocketClientContextImpl[C <: Http4sContext]
-(
-  val c: C#IMPL[C]
-  , val initialRequest: AuthedRequest[C#MonoIO, C#RequestContext]
-  , val initialContext: C#RequestContext
-  , listeners: Seq[WsSessionListener[C#ClientId]]
-  , wsSessionStorage: WsSessionsStorage[C#BiIO, C#ClientId, C#RequestContext]
-  , logger: IzLogger
+class WebsocketClientContextImpl[C <: Http4sContext](
+  val c: C#IMPL[C],
+  val initialRequest: AuthedRequest[C#MonoIO, C#RequestContext],
+  val initialContext: C#RequestContext,
+  listeners: Seq[WsSessionListener[C#BiIO, C#ClientId]],
+  wsSessionStorage: WsSessionsStorage[C#BiIO, C#ClientId, C#RequestContext],
+  logger: IzLogger
 ) extends WebsocketClientContext[C#BiIO, C#ClientId, C#RequestContext] {
 
   import c._
@@ -192,7 +194,7 @@ class WebsocketClientContextImpl[C <: Http4sContext]
     FiniteDuration(d.toNanos, TimeUnit.NANOSECONDS)
   }
 
-  def enqueue(method: IRTMethodId, data: Json): RpcPacketId = {
+  def enqueue(method: IRTMethodId, data: Json): C#MonoIO[RpcPacketId] = F.sync {
     val request = RpcPacket.buzzerRequestRndId(method, data)
     val id = request.id.get
     logger.debug(s"Enqueue $request with $id to request state & send queue")
@@ -201,32 +203,32 @@ class WebsocketClientContextImpl[C <: Http4sContext]
     id
   }
 
-  def onWsSessionOpened(): Unit = {
-  }
+  def onWsSessionOpened(): C#MonoIO[Unit] = F.unit
 
-  def onWsClientIdUpdate(maybeNewId: Option[ClientId], oldId: WsClientId[ClientId]): Unit = {
+  def onWsClientIdUpdate(maybeNewId: Option[ClientId], oldId: WsClientId[ClientId]): C#MonoIO[Unit] = F.sync {
     logger.debug(s"Id updated to $maybeNewId, was: ${oldId.id}")
   }
 
-  def onWsSessionClosed(): Unit = {
+  def onWsSessionClosed(): C#MonoIO[Unit] = F.sync {
     logger.debug("Finish called. Clear request state")
   }
 
-  protected[http4s] def updateId(maybeNewId: Option[ClientId]): Unit = {
+  protected[http4s] def updateId(maybeNewId: Option[ClientId]): C#MonoIO[Unit] = {
     val oldId = id
     maybeId.set(maybeNewId)
     val newId = id
 
-    def notifyListeners(): Unit = {
-      onWsClientIdUpdate(maybeNewId, oldId)
-      listeners.foreach { listener =>
-        listener.onClientIdUpdate(newId, oldId)
+    def notifyListeners(): C#MonoIO[Unit] = {
+      onWsClientIdUpdate(maybeNewId, oldId) *>
+      F.traverse_(listeners) {
+        listener =>
+          listener.onClientIdUpdate(newId, oldId)
       }
     }
 
     (newId, oldId) match {
       case (WsClientId(_, Some(prev)), WsClientId(_, Some(next))) if prev != next => notifyListeners()
-      case (WsClientId(_, None), WsClientId(_, None)) => ()
+      case (WsClientId(_, None), WsClientId(_, None)) => F.unit
       case _ => notifyListeners()
     }
   }
@@ -246,20 +248,22 @@ class WebsocketClientContextImpl[C <: Http4sContext]
   protected[http4s] val pingStream: fs2.Stream[MonoIO, WebSocketFrame] =
     fs2.Stream.awakeEvery[MonoIO](pingTimeout) >> fs2.Stream(Ping())
 
-  override def finish(): Unit = {
-    onWsSessionClosed()
-    Quirks.discard(wsSessionStorage.deleteClient(sessionId))
-    listeners.foreach { listener =>
-      listener.onSessionClosed(id)
-    }
-    requestState.clear()
+  override def finish(): C#MonoIO[Unit] = {
+    onWsSessionClosed() *>
+    wsSessionStorage.deleteClient(sessionId) *>
+    F.traverse_(listeners) {
+      listener =>
+        listener.onSessionClosed(id)
+    } *>
+    F.sync(requestState.clear())
   }
 
-  protected[http4s] def start(): Unit = {
-    Quirks.discard(wsSessionStorage.addClient(sessionId, this))
-    onWsSessionOpened()
-    listeners.foreach { listener =>
-      listener.onSessionOpened(id)
+  protected[http4s] def start(): C#MonoIO[Unit] = {
+    wsSessionStorage.addClient(sessionId, this) *>
+    onWsSessionOpened() *>
+    F.traverse_(listeners) {
+      listener =>
+        listener.onSessionOpened(id)
     }
   }
 
diff --git a/project/Deps.sc b/project/Deps.sc
index 7610aa857..257396f0b 100644
--- a/project/Deps.sc
+++ b/project/Deps.sc
@@ -1,4 +1,4 @@
-import $ivy.`io.7mind.izumi.sbt::sbtgen:0.0.44`
+import $ivy.`io.7mind.izumi.sbt::sbtgen:0.0.46`
 import izumi.sbtgen._
 import izumi.sbtgen.model._
 
@@ -52,7 +52,7 @@ object Izumi {
 
   val settings = GlobalSettings(
     groupId = "io.7mind.izumi",
-    sbtVersion = "1.3.6",
+    sbtVersion = "1.3.7",
     scalaJsVersion = PV.scala_js_version,
     crossProjectVersion = PV.crossproject_version,
     bundlerVersion = PV.scalajs_bundler_version,
@@ -222,6 +222,7 @@ object Izumi {
 
       final val sharedSettings = Defaults.SbtMeta ++ Seq(
         "testOptions" in SettingScope.Test += """Tests.Argument("-oDF")""".raw,
+        //"testOptions" in (SettingScope.Test, Platform.Jvm) ++= s"""Seq(Tests.Argument("-u"), Tests.Argument(s"$${target.value}/junit-xml-$${scalaVersion.value}"))""".raw,
         "scalacOptions" ++= Seq(
           SettingKey(Some(scala212), None) := Defaults.Scala212Options,
           SettingKey(Some(scala213), None) := Defaults.Scala213Options,
diff --git a/project/Versions.scala b/project/Versions.scala
index bfeadf3cb..35aa2687f 100644
--- a/project/Versions.scala
+++ b/project/Versions.scala
@@ -18,12 +18,12 @@ object V {
   val circe_generic_extras = "0.12.2"
   val circe_derivation = "0.12.0-M7"
   val circe_config = "0.7.0"
-  val jawn = "0.14.3"
+  val jawn = "1.0.0"
 
-  val http4s = "0.21.0-M6"
+  val http4s = "0.21.0-RC1"
 
   val scalameta = "4.3.0"
-  val fastparse = "2.2.2"
+  val fastparse = "2.2.3"
 
   val scala_xml = "1.2.0"
 
diff --git a/project/build.properties b/project/build.properties
index 3913aa893..e7c6f8aac 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1,1 +1,1 @@
-sbt.version = 1.3.6
\ No newline at end of file
+sbt.version = 1.3.7
\ No newline at end of file
diff --git a/project/plugins.sbt b/project/plugins.sbt
index 3d5b74818..0dcb9c85a 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,7 +1,7 @@
 
 ////////////////////////////////////////////////////////////////////////////////
 
-addSbtPlugin("io.7mind.izumi.sbt" % "sbt-izumi" % "0.0.44")
+addSbtPlugin("io.7mind.izumi.sbt" % "sbt-izumi" % "0.0.46")
 
 addSbtPlugin("com.eed3si9n" % "sbt-assembly" % PV.sbt_assembly)
 


Test patch:
diff --git a/distage/distage-core/.jvm/src/test/scala/izumi/distage/injector/AnimalModel.scala b/distage/distage-core/.jvm/src/test/scala/izumi/distage/injector/AnimalModel.scala
index f86aa09da..75d7f1cff 100644
--- a/distage/distage-core/.jvm/src/test/scala/izumi/distage/injector/AnimalModel.scala
+++ b/distage/distage-core/.jvm/src/test/scala/izumi/distage/injector/AnimalModel.scala
@@ -24,9 +24,9 @@ class AnimalModel extends AnyWordSpec with MkInjector {
       val debug = false
 
       val injector = if (debug) {
-        Injector.Standard(GraphDumpBootstrapModule())
+        Injector(GraphDumpBootstrapModule())
       } else {
-        Injector.Standard()
+        Injector()
       }
 
       val plan = injector.plan(definition)
diff --git a/distage/distage-core/src/test/scala/izumi/distage/fixtures/ResourceCases.scala b/distage/distage-core/src/test/scala/izumi/distage/fixtures/ResourceCases.scala
index c9b810025..b512bb66f 100644
--- a/distage/distage-core/src/test/scala/izumi/distage/fixtures/ResourceCases.scala
+++ b/distage/distage-core/src/test/scala/izumi/distage/fixtures/ResourceCases.scala
@@ -167,8 +167,8 @@ object ResourceCases {
           case Right(value) => Right(value)
         })
       }
-      override def definitelyRecoverCause[A](action: => Suspend2[E, A])(recoverCause: Throwable => Suspend2[E, A]): Suspend2[E, A] = {
-        definitelyRecover(action)(recoverCause)
+      override def definitelyRecoverCause[A](action: => Suspend2[E, A])(recoverCause: (Throwable, Throwable) => Suspend2[E, A]): Suspend2[E, A] = {
+        definitelyRecover(action)(e => recoverCause(e, e))
       }
 
       override def bracket[A, B](acquire: => Suspend2[E, A])(release: A => Suspend2[E, Unit])(use: A => Suspend2[E, B]): Suspend2[E, B] =
diff --git a/distage/distage-core/src/test/scala/izumi/distage/impl/ProviderMagnetTest.scala b/distage/distage-core/src/test/scala/izumi/distage/impl/ProviderMagnetTest.scala
index f0c0c54be..2fd9af478 100644
--- a/distage/distage-core/src/test/scala/izumi/distage/impl/ProviderMagnetTest.scala
+++ b/distage/distage-core/src/test/scala/izumi/distage/impl/ProviderMagnetTest.scala
@@ -4,7 +4,9 @@ import distage._
 import izumi.distage.fixtures.BasicCases.BasicCase4.ClassTypeAnnT
 import izumi.distage.fixtures.ProviderCases.ProviderCase1
 import izumi.distage.model.providers.ProviderMagnet
-import izumi.distage.model.reflection.universe.RuntimeDIUniverse
+import izumi.distage.model.reflection.universe.RuntimeDIUniverse.TypedRef
+import izumi.fundamentals.platform.build.ProjectAttributeMacro
+import izumi.fundamentals.platform.language.IzScala.ScalaRelease
 import izumi.fundamentals.platform.language.Quirks._
 import org.scalatest.wordspec.AnyWordSpec
 
@@ -252,10 +254,82 @@ class ProviderMagnetTest extends AnyWordSpec {
       assert(fn.diKeys contains DIKey.get[Any])
       var counter = 0
       class CountInstantiations { counter += 1 }
-      fn.unsafeApply(Seq(RuntimeDIUniverse.TypedRef.byName(new CountInstantiations)))
+      fn.unsafeApply(Seq(TypedRef.byName(new CountInstantiations)))
       assert(counter == 0)
     }
 
+    "zip is correct" in {
+      val fn = ProviderMagnet.pure(5).zip(ProviderMagnet.pure("Hello"))
+
+      assert(fn.get.parameters.isEmpty)
+      assert(fn.get.ret == SafeType.get[(Int, String)])
+    }
+
+    "map2 is correct" in {
+      val fn = ProviderMagnet.pure(5).map2(ProviderMagnet.identity[String])(
+        (i: Int, s: String) => StringContext(s + i)
+      )
+
+      assert(fn.get.unsafeApply(Seq(TypedRef("Hello"))) == StringContext("Hello5"))
+      assert(fn.get.parameters.size == 1)
+      assert(fn.get.parameters.head.key == DIKey.get[String])
+      assert(fn.get.ret == SafeType.get[StringContext])
+    }
+
+    "flatAp is correct" in {
+      val fn = ProviderMagnet.pure(5).flatAp(
+        (s: String) => (i: Int) => StringContext(s + i)
+      )
+
+      assert(fn.get.unsafeApply(Seq(TypedRef("Hello"))) == StringContext("Hello5"))
+      assert(fn.get.parameters.size == 1)
+      assert(fn.get.parameters.head.key == DIKey.get[String])
+      assert(fn.get.ret == SafeType.get[StringContext])
+    }
+
+    "ap is correct" in {
+      val fn = ProviderMagnet(
+        (s: String) => (i: Int) => StringContext(s + i)
+      ).ap(ProviderMagnet.pure(5))
+
+      assert(fn.get.unsafeApply(Seq(TypedRef("Hello"))) == StringContext("Hello5"))
+      assert(fn.get.parameters.size == 1)
+      assert(fn.get.parameters.head.key == DIKey.get[String])
+      assert(fn.get.ret == SafeType.get[StringContext])
+    }
+
+    "ProviderMagnet.single is correct" in {
+      val fn = ProviderMagnet.single((_: String).length)
+
+      assert(fn.get.unsafeApply(Seq(TypedRef("Hello"))) == 5)
+      assert(fn.get.parameters.size == 1)
+      assert(fn.get.parameters.head.key == DIKey.get[String])
+      assert(fn.get.ret == SafeType.get[Int])
+    }
+
+    "ProviderMagnet.singleton is correct" in {
+      val xa = new {}
+      val fn = ProviderMagnet.singleton[xa.type](xa)
+
+      assert(fn.get.unsafeApply(Seq()).asInstanceOf[AnyRef] eq xa)
+      assert(fn.get.parameters.isEmpty)
+      assert(fn.get.ret == SafeType.get[xa.type])
+    }
+
+    "ProviderMagnet.singleton is correct with constant types" in {
+      import Ordering.Implicits._
+      assume(ScalaRelease.parse(ProjectAttributeMacro.extractScalaVersion().get) >= ScalaRelease.`2_13`(0))
+      assertCompiles(
+        """
+      val fn = ProviderMagnet.singleton["xa"]("xa")
+
+      assert(fn.get.unsafeApply(Seq()) == "xa")
+      assert(fn.get.parameters.isEmpty)
+      assert(fn.get.ret == SafeType.get["xa"])
+        """
+      )
+    }
+
     "generic parameters without TypeTag should fail" in {
       assertTypeError(
         """def fn[T]  = ProviderMagnet.apply((x: T @Id("gentypeann")) => x).get"""
diff --git a/distage/distage-core/src/test/scala/izumi/distage/injector/AutoSetTest.scala b/distage/distage-core/src/test/scala/izumi/distage/injector/AutoSetTest.scala
index d45881637..9ddc26c07 100644
--- a/distage/distage-core/src/test/scala/izumi/distage/injector/AutoSetTest.scala
+++ b/distage/distage-core/src/test/scala/izumi/distage/injector/AutoSetTest.scala
@@ -19,7 +19,7 @@ class AutoSetTest extends AnyWordSpec with MkInjector {
       make[ServiceD]
     }
 
-    val injector = Injector.Standard(new BootstrapModuleDef {
+    val injector = Injector(new BootstrapModuleDef {
       many[PlanningHook]
         .add(new AutoSetHook[Ordered, Ordered](identity))
     })
@@ -39,7 +39,7 @@ class AutoSetTest extends AnyWordSpec with MkInjector {
         .addValue(5)
     }
 
-    val injector = Injector.Standard(new BootstrapModuleDef {
+    val injector = Injector(new BootstrapModuleDef {
       many[PlanningHook]
         .add(new AutoSetHook[Int, Int](identity))
     })
diff --git a/distage/distage-extension-logstage/src/test/scala/izumi/logstage/distage/LoggerInjectionTest.scala b/distage/distage-extension-logstage/src/test/scala/izumi/logstage/distage/LoggerInjectionTest.scala
index b95296467..af026fb34 100644
--- a/distage/distage-extension-logstage/src/test/scala/izumi/logstage/distage/LoggerInjectionTest.scala
+++ b/distage/distage-extension-logstage/src/test/scala/izumi/logstage/distage/LoggerInjectionTest.scala
@@ -33,7 +33,7 @@ class LoggerInjectionTest extends AnyWordSpec {
 
       val loggerModule = new LogstageModule(router, false)
 
-      val injector = Injector.Standard(loggerModule)
+      val injector = Injector(loggerModule)
       val plan = injector.plan(definition)
       val context = injector.produceUnsafe(plan)
       assert(context.get[ExampleApp].test == 265)
diff --git a/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/DistageTestDockerBIO.scala b/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/DistageTestDockerBIO.scala
index 7176b4ca1..861d324ba 100644
--- a/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/DistageTestDockerBIO.scala
+++ b/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/DistageTestDockerBIO.scala
@@ -1,7 +1,6 @@
 package izumi.distage.testkit.docker
 
 import distage.DIKey
-import izumi.distage.docker.examples.{DynamoDocker, PostgresDocker}
 import izumi.distage.testkit.TestConfig
 import izumi.distage.testkit.docker.fixtures.PgSvcExample
 import izumi.distage.testkit.scalatest.DistageBIOSpecScalatest
@@ -32,8 +31,9 @@ final class DistageTestDockerBIO extends DistageBIOSpecScalatest[IO] {
   override protected def config: TestConfig = {
     super.config.copy(
       memoizationRoots = Set(
-        DIKey.get[DynamoDocker.Container],
-        DIKey.get[PostgresDocker.Container],
+        DIKey.get[PgSvcExample],
+//        DIKey.get[DynamoDocker.Container],
+//        DIKey.get[PostgresDocker.Container],
       ))
   }
 }
diff --git a/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala b/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala
index 6032d85f8..60712a206 100644
--- a/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala
+++ b/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala
@@ -8,9 +8,18 @@ import izumi.distage.plugins.PluginDef
 import izumi.distage.docker.Docker.AvailablePort
 import izumi.distage.docker.examples.{DynamoDocker, PostgresDocker}
 import izumi.distage.docker.modules.DockerContainerModule
+import izumi.distage.framework.model.IntegrationCheck
+import izumi.fundamentals.platform.integration.{PortCheck, ResourceCheck}
 import zio.Task
 
-class PgSvcExample(val pg: AvailablePort @Id("pg"), val ddb: AvailablePort @Id("ddb"))
+class PgSvcExample(
+                    val pg: AvailablePort @Id("pg"),
+                    val ddb: AvailablePort @Id("ddb"),
+                  ) extends IntegrationCheck {
+  override def resourcesAvailable(): ResourceCheck = {
+    new PortCheck(10).checkPort(pg.hostV4, pg.port)
+  }
+}
 
 object MonadPlugin extends PluginDef
   with CatsDIEffectModule
diff --git a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestEnv.scala b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestEnv.scala
index 11257edd9..31cfce980 100644
--- a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestEnv.scala
+++ b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestEnv.scala
@@ -32,7 +32,7 @@ trait DistageTestEnv {
     }
   }
 
-  private def makeEnv(logger: IzLogger, testConfig: TestConfig, pluginLoader: PluginLoader, roles: RolesInfo, mergeStrategy: PluginMergeStrategy): TestEnvironment = {
+  protected[distage] def makeEnv(logger: IzLogger, testConfig: TestConfig, pluginLoader: PluginLoader, roles: RolesInfo, mergeStrategy: PluginMergeStrategy): TestEnvironment = {
     val appPlugins = pluginLoader.load(testConfig.pluginConfig)
     val bsPlugins = pluginLoader.load(testConfig.bootstrapPluginConfig)
     val appModule = mergeStrategy.merge(appPlugins) overridenBy testConfig.moduleOverrides
@@ -72,8 +72,7 @@ trait DistageTestEnv {
 }
 
 object DistageTestEnv {
-  private lazy val cache = new SyncCache[EnvCacheKey, TestEnvironment]
-
-  case class EnvCacheKey(config: TestConfig, rolesInfo: RolesInfo, mergeStrategy: PluginMergeStrategy)
+  private[distage] lazy val cache = new SyncCache[EnvCacheKey, TestEnvironment]
 
+  final case class EnvCacheKey(config: TestConfig, rolesInfo: RolesInfo, mergeStrategy: PluginMergeStrategy)
 }
diff --git a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestRunner.scala b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestRunner.scala
index 3ea6816d5..093cd5d2a 100644
--- a/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestRunner.scala
+++ b/distage/distage-testkit-core/src/main/scala/izumi/distage/testkit/services/dstest/DistageTestRunner.scala
@@ -2,7 +2,7 @@ package izumi.distage.testkit.services.dstest
 
 import java.util.concurrent.TimeUnit
 
-import distage.{DIKey, Injector, PlannerInput}
+import distage.{Activation, DIKey, Injector, PlannerInput}
 import izumi.distage.framework.model.IntegrationCheck
 import izumi.distage.framework.model.exceptions.IntegrationCheckException
 import izumi.distage.framework.services.{IntegrationChecker, PlanCircularDependencyCheck}
@@ -15,15 +15,16 @@ import izumi.distage.model.exceptions.ProvisioningException
 import izumi.distage.model.plan.{OrderedPlan, TriSplittedPlan}
 import izumi.distage.model.providers.ProviderMagnet
 import izumi.distage.testkit.DebugProperties
-import izumi.distage.testkit.services.dstest.DistageTestRunner.{DistageTest, SuiteData, TestReporter, TestStatus}
+import izumi.distage.testkit.services.dstest.DistageTestRunner._
 import izumi.fundamentals.platform.console.TrivialLogger
 import izumi.fundamentals.platform.functional.Identity
 import izumi.fundamentals.platform.integration.ResourceCheck
 import izumi.fundamentals.platform.language.CodePosition
+import izumi.fundamentals.platform.strings.IzString._
 import izumi.fundamentals.reflection.Tags.TagK
 
-import scala.concurrent.duration.FiniteDuration
-import izumi.fundamentals.platform.strings.IzString._
+import scala.concurrent.ExecutionContext
+import scala.concurrent.duration.{Duration, FiniteDuration}
 
 class DistageTestRunner[F[_]: TagK]
 (
@@ -32,7 +33,8 @@ class DistageTestRunner[F[_]: TagK]
   runnerEnvironment: SpecEnvironment,
   tests: Seq[DistageTest[F]],
   isTestSkipException: Throwable => Boolean,
-  parallelTests: Boolean
+  parallelTests: Boolean,
+  parallelEnvs: Boolean,
 ) {
   def run(): Unit = {
     val groups = tests.groupBy(_.environment)
@@ -45,20 +47,23 @@ class DistageTestRunner[F[_]: TagK]
     val checker = new PlanCircularDependencyCheck(options, logger)
 
     logger.info(s"Starting tests across ${groups.size -> "num envs"}, ${groups.values.map(_.size).toList -> "num tests"} in ${TagK[F].tag -> "monad"}")
-    debugLogger.log(
-      s"""Env contents:
-         |test environments=${groups.keys.niceList()}
-         |tests=${groups.values.map(_.map(_.meta.id).niceList()).zipWithIndex.map(_.swap).niceList()}""".stripMargin)
-
-    groups.foreach {
-      case (env, tests) =>
+    debugLogger.log {
+      val printedEnvs = groups.toList.map {
+        case (t, tests) =>
+          final case class TestEnvironment(Activation: Activation, memoizationRoots: DIKey => Boolean, tests: String)
+          TestEnvironment(t.activation, t.memoizationRoots, tests.map(_.meta.id).niceList().shift(2))
+      }.niceList()
+      s"Env contents: $printedEnvs"
+    }
 
+    try configuredForeach(groups) {
+      case (env, tests) => try {
         // here we scan our classpath to enumerate of our components (we have "bootstrap" components - injector plugins, and app components)
         val provider = runnerEnvironment.makeModuleProvider(options, config, logger, env.roles, env.activationInfo, env.activation)
         val bsModule = provider.bootstrapModules().merge overridenBy env.bsModule overridenBy runnerEnvironment.bootstrapOverrides
         val appModule = provider.appModules().merge overridenBy env.appModule overridenBy runnerEnvironment.moduleOverrides
 
-        val injector = Injector.Standard(bsModule)
+        val injector = Injector(bsModule)
 
         // first we need to plan runtime for our monad. Identity is also supported
         val runtimeGcRoots: Set[DIKey] = Set(
@@ -115,13 +120,14 @@ class DistageTestRunner[F[_]: TagK]
             implicit val F: DIEffect[F] = runtimeLocator.get[DIEffect[F]]
             implicit val P: DIEffectAsync[F] = runtimeLocator.get[DIEffectAsync[F]]
 
-            try {
-              runner.run {
-                // now we produce integration components for our shared plan
-                checker.verify(shared.side)
+            runner.run {
+              // now we produce integration components for our shared plan
+              checker.verify(shared.shared)
 
+              F.definitelyRecoverCause {
                 Injector.inherit(runtimeLocator).produceF[F](shared.shared).use {
                   sharedLocator =>
+                    checker.verify(shared.side)
 
                     Injector.inherit(sharedLocator).produceF[F](shared.side).use {
                       sharedIntegrationLocator =>
@@ -130,19 +136,26 @@ class DistageTestRunner[F[_]: TagK]
                         }
                     }
                 }
+              } {
+                case (ProvisioningIntegrationException(integrations), _) =>
+                  // FIXME: temporary hack to allow missing containers to skip tests (happens when both DockerWrapper & integration check that depends on Docker.Container are memoized)
+                  F.maybeSuspend(ignoreIntegrationCheckFailedTests(tests, integrations))
+                case (_, cause) =>
+                  // fail all tests (if an exception reached here, it must have happened before the individual test runs)
+                  F.maybeSuspend(failAllTests(tests, cause))
               }
-            } catch {
-              case p: ProvisioningException =>
-                val integrations = p.getSuppressed.collect { case i: IntegrationCheckException => i.failures }.toSeq
-                if (integrations.nonEmpty) {
-                  ignoreIntegrationCheckFailedTests(tests, integrations.flatten)
-                } else throw p
             }
         }
-    }
+      } catch {
+        case t: Throwable =>
+          // fail all tests (if an exception reaches here, it must have happened before the runtime was successfully produced)
+          failAllTests(tests, t)
+          reporter.onFailure(t)
+      }
+    } finally reporter.endAll()
   }
 
-  private def ifIntegChecksOk(integLocator: Locator)(testplans: Seq[DistageTest[F]], plans: TriSplittedPlan)(onSuccess: => F[Unit])(implicit F: DIEffect[F]): F[Unit] = {
+  protected def ifIntegChecksOk(integLocator: Locator)(testplans: Seq[DistageTest[F]], plans: TriSplittedPlan)(onSuccess: => F[Unit])(implicit F: DIEffect[F]): F[Unit] = {
     integrationChecker.collectFailures(plans.side.declaredRoots, integLocator).flatMap {
       case Left(failures) =>
         F.maybeSuspend(ignoreIntegrationCheckFailedTests(testplans, failures))
@@ -152,43 +165,53 @@ class DistageTestRunner[F[_]: TagK]
     }
   }
 
-  private def ignoreIntegrationCheckFailedTests(tests: Seq[DistageTest[F]], failures: Seq[ResourceCheck.Failure]): Unit = {
+  protected def ignoreIntegrationCheckFailedTests(tests: Seq[DistageTest[F]], failures: Seq[ResourceCheck.Failure]): Unit = {
     tests.foreach {
       test =>
         reporter.testStatus(test.meta, TestStatus.Ignored(failures))
     }
   }
 
-  private def proceed(appmodule: ModuleBase,
-                      checker: PlanCircularDependencyCheck,
-                      testplans: Seq[(DistageTest[F], OrderedPlan)],
-                      shared: TriSplittedPlan,
-                      parent: Locator,
-                     )(implicit F: DIEffect[F], P: DIEffectAsync[F]): F[Unit] = {
+  protected def failAllTests(tests: Seq[DistageTest[F]], cause: Throwable): Unit = {
+    tests.foreach {
+      test =>
+        reporter.testStatus(test.meta, TestStatus.Failed(cause, Duration.Zero))
+    }
+  }
+
+  protected def proceed(appmodule: ModuleBase,
+                        checker: PlanCircularDependencyCheck,
+                        testplans: Seq[(DistageTest[F], OrderedPlan)],
+                        shared: TriSplittedPlan,
+                        parent: Locator,
+                       )(implicit F: DIEffect[F], P: DIEffectAsync[F]): F[Unit] = {
     // here we produce our shared plan
     checker.verify(shared.primary)
+
     Injector.inherit(parent).produceF[F](shared.primary).use {
       mainSharedLocator =>
         val testInjector = Injector.inherit(mainSharedLocator)
 
+        val testsBySuite = testplans.groupBy {
+          t =>
+            val testId = t._1.meta.id
+            SuiteData(testId.suiteName, testId.suiteId, testId.suiteClassName)
+        }
         // now we are ready to run each individual test
         // note: scheduling here is custom also and tests may automatically run in parallel for any non-trivial monad
-        val tests = configuredTraverse_(testplans.groupBy {
-          t =>
-            val id = t._1.meta.id
-            SuiteData(id.suiteName, id.suiteId, id.suiteClassName)
-        }) {
-          case (id, plans) =>
-            for {
-              _ <- F.maybeSuspend(reporter.beginSuite(id))
-              _ <- configuredTraverse_(plans) {
+        configuredTraverse_(testsBySuite) {
+          case (suiteData, plans) =>
+            F.bracket(
+              acquire = F.maybeSuspend(reporter.beginSuite(suiteData))
+            )(release = _ => F.maybeSuspend(reporter.endSuite(suiteData))) { _ =>
+              configuredTraverse_(plans) {
                 case (test, testplan) =>
                   val allSharedKeys = mainSharedLocator.allInstances.map(_.key).toSet
 
                   val integrations = testplan.collectChildren[IntegrationCheck].map(_.target).toSet -- allSharedKeys
                   val newtestplan = testInjector.trisectByRoots(appmodule.drop(allSharedKeys), testplan.keys -- allSharedKeys, integrations)
 
-                  debugLogger.log(s"Running, test Id=$id")
+                  debugLogger.log(s"Running `test Id`=${test.meta.id}")
 
                   checker.verify(newtestplan.primary)
                   checker.verify(newtestplan.side)
@@ -205,58 +228,47 @@ class DistageTestRunner[F[_]: TagK]
                       }
                   }
               }
-              _ <- F.maybeSuspend(reporter.endSuite(id))
-            } yield ()
-        }
-
-        F.definitelyRecover(tests.flatMap(_ => F.maybeSuspend(reporter.endAll()))) {
-          f =>
-            F.maybeSuspend(reporter.onFailure(f)).flatMap(_ => F.fail(f))
+            }
         }
-
     }
   }
 
-  private def proceedIndividual(test: DistageTest[F], newtestplan: TriSplittedPlan, parent: Locator)(implicit F: DIEffect[F]): F[Unit] = {
-    Injector.inherit(parent)
-      .produceF[F](newtestplan.primary).use {
-      testLocator =>
-        def doRun(before: Long): F[Unit] = {
-          for {
-            _ <- F.definitelyRecover(testLocator.run(test.test).flatMap {
-              _ =>
-                F.maybeSuspend {
-                  val after = System.nanoTime()
-                  val testDuration = FiniteDuration(after - before, TimeUnit.NANOSECONDS)
-                  reporter.testStatus(test.meta, TestStatus.Succeed(testDuration))
-                }
-            }) {
-              case s if isTestSkipException(s) =>
-                F.maybeSuspend {
-                  val after = System.nanoTime()
-                  val testDuration = FiniteDuration(after - before, TimeUnit.NANOSECONDS)
-                  reporter.testStatus(test.meta, TestStatus.Cancelled(s.getMessage, testDuration))
-                }
-              case o =>
-                F.fail(o)
-            }
-          } yield ()
-        }
+  protected def proceedIndividual(test: DistageTest[F], newtestplan: TriSplittedPlan, parent: Locator)(implicit F: DIEffect[F]): F[Unit] = {
+    def testDuration(before: Option[Long]): FiniteDuration = {
+      before.fold(Duration.Zero) {
+        before =>
+          val after = System.nanoTime()
+          FiniteDuration(after - before, TimeUnit.NANOSECONDS)
+      }
+    }
+
+    def doRecover(before: Option[Long])(action: => F[Unit]): F[Unit] = {
+      F.definitelyRecoverCause(action) {
+        case (s, _) if isTestSkipException(s) =>
+          F.maybeSuspend {
+            reporter.testStatus(test.meta, TestStatus.Cancelled(s.getMessage, testDuration(before)))
+          }
+        case (_, cause) =>
+          F.maybeSuspend {
+            reporter.testStatus(test.meta, TestStatus.Failed(cause, testDuration(before)))
+          }
+      }
+    }
 
-        def doRecover(before: Long): Throwable => F[Unit] = {
-          // TODO: here we may also ignore individual tests
-          throwable =>
-            F.maybeSuspend {
-              val after = System.nanoTime()
-              reporter.testStatus(test.meta, TestStatus.Failed(throwable, FiniteDuration(after - before, TimeUnit.NANOSECONDS)))
+    doRecover(None) {
+      Injector.inherit(parent).produceF[F](newtestplan.primary).use {
+        testLocator =>
+          F.suspendF {
+            val before = System.nanoTime()
+            reporter.testStatus(test.meta, TestStatus.Running)
+
+            doRecover(Some(before)) {
+              testLocator
+                .run(test.test)
+                .flatMap(_ => F.maybeSuspend(reporter.testStatus(test.meta, TestStatus.Succeed(testDuration(Some(before))))))
             }
+          }
         }
-
-        for {
-          before <- F.maybeSuspend(System.nanoTime())
-          _ <- F.maybeSuspend(reporter.testStatus(test.meta, TestStatus.Running))
-          _ <- F.definitelyRecoverCause(doRun(before))(doRecover(before))
-        } yield ()
     }
   }
 
@@ -268,6 +280,17 @@ class DistageTestRunner[F[_]: TagK]
     }
   }
 
+  protected def configuredForeach[A](environments: Iterable[A])(f: A => Unit): Unit = {
+    if (parallelEnvs && environments.size > 1) {
+      val ec = ExecutionContext.fromExecutorService(null)
+      try {
+        DIEffectAsync.diEffectParIdentity.parTraverse_(environments)(f)
+      } finally ec.shutdown()
+    } else {
+      environments.foreach(f)
+    }
+  }
+
   lazy val debugLogger: TrivialLogger = TrivialLogger.make[DistageTestRunner[F]](DebugProperties.`izumi.distage.testkit.debug`)
 }
 
@@ -283,12 +306,10 @@ object DistageTestRunner {
 
   sealed trait TestStatus
   object TestStatus {
-    //    case object Scheduled extends TestStatus
     case object Running extends TestStatus
 
     sealed trait Done extends TestStatus
     final case class Ignored(checks: Seq[ResourceCheck.Failure]) extends Done
-
     sealed trait Finished extends Done
     final case class Cancelled(clue: String, duration: FiniteDuration) extends Finished
     final case class Succeed(duration: FiniteDuration) extends Finished
@@ -303,4 +324,12 @@ object DistageTestRunner {
     def testStatus(test: TestMeta, testStatus: TestStatus): Unit
   }
 
+  object ProvisioningIntegrationException {
+    def unapply(arg: ProvisioningException): Option[Seq[ResourceCheck.Failure]] = {
+      Some(arg.getSuppressed.collect { case i: IntegrationCheckException => i.failures }.toSeq)
+        .filter(_.nonEmpty)
+        .map(_.flatten)
+    }
+  }
+
 }
diff --git a/distage/distage-testkit-legacy/src/main/scala/izumi/distage/testkit/services/scalatest/adapter/DistagePluginTestSupport.scala b/distage/distage-testkit-legacy/src/main/scala/izumi/distage/testkit/services/scalatest/adapter/DistagePluginTestSupport.scala
index b135ea8b9..7121e0ba8 100644
--- a/distage/distage-testkit-legacy/src/main/scala/izumi/distage/testkit/services/scalatest/adapter/DistagePluginTestSupport.scala
+++ b/distage/distage-testkit-legacy/src/main/scala/izumi/distage/testkit/services/scalatest/adapter/DistagePluginTestSupport.scala
@@ -4,10 +4,8 @@ import distage.SafeType
 import izumi.distage.model.definition.Activation
 import izumi.distage.model.definition.StandardAxis._
 import izumi.distage.plugins.PluginConfig
-import izumi.distage.plugins.load.{PluginLoader, PluginLoaderDefaultImpl}
 import izumi.distage.testkit.TestConfig
 import izumi.distage.testkit.services.dstest.{DistageTestEnv, TestEnvironment}
-import izumi.fundamentals.platform.language.unused
 import izumi.fundamentals.reflection.Tags.TagK
 import izumi.logstage.api.IzLogger
 
diff --git a/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageAbstractScalatestSpec.scala b/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageAbstractScalatestSpec.scala
index 9e899022a..e3f915d33 100644
--- a/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageAbstractScalatestSpec.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageAbstractScalatestSpec.scala
@@ -3,7 +3,6 @@ package izumi.distage.testkit.services.scalatest.dstest
 import distage.{Tag, TagK, TagKK}
 import izumi.distage.model.effect.DIEffect
 import izumi.distage.model.providers.ProviderMagnet
-import izumi.distage.plugins.load.PluginLoader
 import izumi.distage.testkit.TestConfig
 import izumi.distage.testkit.services.dstest.DistageTestRunner.{DistageTest, TestId, TestMeta}
 import izumi.distage.testkit.services.dstest._
diff --git a/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageTestsRegistrySingleton.scala b/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageTestsRegistrySingleton.scala
index 08cd04ab5..190fd7fbb 100644
--- a/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageTestsRegistrySingleton.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala/izumi/distage/testkit/services/scalatest/dstest/DistageTestsRegistrySingleton.scala
@@ -5,6 +5,7 @@ import java.util.concurrent.atomic.AtomicBoolean
 
 import distage.{SafeType, TagK}
 import izumi.distage.testkit.services.dstest.DistageTestRunner.DistageTest
+import izumi.functional.Value
 import izumi.fundamentals.platform.language.Quirks._
 import org.scalatest.{StatefulStatus, Tracker}
 
@@ -14,7 +15,7 @@ object DistageTestsRegistrySingleton {
   protected[this] type Fake[T]
   private[this] object Fake
   private[this] val registry = new mutable.HashMap[SafeType, mutable.ArrayBuffer[DistageTest[Fake]]]()
-  private[this] val statuses = new mutable.HashMap[SafeType, mutable.HashMap[String, StatefulStatus]]()
+  private[this] val statuses = new mutable.HashMap[SafeType, Option[mutable.HashMap[String, StatefulStatus]]]()
   private[this] val trackers = new mutable.HashMap[String, Either[mutable.ArrayBuffer[Tracker => Unit], Tracker]]()
   private[this] val runTracker = new ConcurrentHashMap[SafeType, Fake.type]()
   private[this] val knownSuites = new ConcurrentHashMap[(SafeType, String), Fake.type]()
@@ -31,7 +32,9 @@ object DistageTestsRegistrySingleton {
 
   def register[F[_]: TagK](t: DistageTest[F]): Unit = synchronized {
     if (registrationOpen.get()) {
-      registry.getOrElseUpdate(SafeType.getK[F], mutable.ArrayBuffer.empty).append(t.asInstanceOf[DistageTest[Fake]]).discard()
+      registry
+        .getOrElseUpdate(SafeType.getK[F], mutable.ArrayBuffer.empty)
+        .append(castTest(t))
     }
   }
 
@@ -45,24 +48,33 @@ object DistageTestsRegistrySingleton {
   }
 
   def registeredTests[F[_]: TagK]: Seq[DistageTest[F]] = synchronized {
-    registry.getOrElseUpdate(SafeType.getK[F], mutable.ArrayBuffer.empty).map(_.asInstanceOf[DistageTest[F]]).toSeq
+    val arr = registry.getOrElseUpdate(SafeType.getK[F], mutable.ArrayBuffer.empty)
+    castArray(arr).toSeq
   }
 
   def registerStatus[F[_]: TagK](suiteId: String): StatefulStatus = synchronized {
     statuses
-      .getOrElseUpdate(SafeType.getK[F], mutable.HashMap.empty)
-      .getOrElseUpdate(suiteId, new StatefulStatus)
+      .getOrElseUpdate(SafeType.getK[F], Some(mutable.HashMap.empty))
+      .fold {
+        // return completed test if the runner has already ran before this test got registered
+        Value(new StatefulStatus).eff(_.setCompleted()).get
+      } {
+        _.getOrElseUpdate(suiteId, new StatefulStatus)
+      }
   }
 
   def completeStatuses[F[_]: TagK](): Unit = synchronized {
-    statuses
-      .getOrElseUpdate(SafeType.getK[F], mutable.HashMap.empty)
-      .valuesIterator.foreach {
-        status =>
-          if (!status.isCompleted) {
-            status.setCompleted()
-          }
+    statuses.get(SafeType.getK[F])
+      .flatten
+      .foreach {
+        _.valuesIterator.foreach {
+          status =>
+            if (!status.isCompleted) {
+              status.setCompleted()
+            }
+        }
       }
+    statuses.put(SafeType.getK[F], None).discard()
   }
 
   def runReport(suiteId: String)(f: Tracker => Unit): Unit = synchronized {
@@ -82,4 +94,7 @@ object DistageTestsRegistrySingleton {
       case Right(_) =>
     }
   }
+
+  @inline private[this] def castTest[F[_]](t: DistageTest[F]): DistageTest[Fake] = t.asInstanceOf[DistageTest[Fake]]
+  @inline private[this] def castArray[C[_], F[_]](a: C[DistageTest[Fake]]): C[DistageTest[F]] = a.asInstanceOf[C[DistageTest[F]]]
 }
diff --git a/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/DistageScalatestTestSuiteRunner.scala b/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/DistageScalatestTestSuiteRunner.scala
index 91478f6aa..56ad0a58a 100644
--- a/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/DistageScalatestTestSuiteRunner.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/DistageScalatestTestSuiteRunner.scala
@@ -43,16 +43,19 @@ object ScalatestInitWorkaround {
     def doScan[F[_]](instance: DistageScalatestTestSuiteRunner[F]): Unit = {
       if (classpathScanned.compareAndSet(false, true)) {
         val classLoader = instance.getClass.getClassLoader
-        val nearest2Packages = instance.getClass.getPackage.getName.split('.').toSeq.inits.take(2).map(_.mkString(".")).toSeq
         val scan = new ClassGraph()
           .whitelistJars("distage-testkit-scalatest*")
-          .whitelistPackages(nearest2Packages: _*)
           .enableClassInfo()
           .addClassLoader(classLoader)
           .scan()
         try {
-          val specs = scan.getSubclasses(classOf[DistageScalatestTestSuiteRunner[Identity]].getCanonicalName).asScala.filterNot(_.isAbstract)
-          specs.foreach(spec => Try(spec.loadClass().getDeclaredConstructor().newInstance()))
+          val suiteClassName = classOf[DistageScalatestTestSuiteRunner[Identity]].getName
+          val testClasses = scan.getSubclasses(suiteClassName).asScala.filterNot(_.isAbstract)
+          lazy val debugLogger = TrivialLogger.make[ScalatestInitWorkaroundImpl.type](DebugProperties.`izumi.distage.testkit.debug`)
+          testClasses.foreach(classInfo => Try {
+            debugLogger.log(s"Added scanned class `${classInfo.getName}` to current test run")
+            classInfo.loadClass().getDeclaredConstructor().newInstance()
+          })
           DistageTestsRegistrySingleton.disableRegistration()
           latch.countDown()
         } finally {
@@ -60,7 +63,6 @@ object ScalatestInitWorkaround {
         }
       }
     }
-
   }
 
 }
@@ -195,7 +197,7 @@ abstract class DistageScalatestTestSuiteRunner[F[_]](implicit override val tagMo
         val runner = {
           val logger = IzLogger(Log.Level.Debug)("phase" -> "test")
           val checker = new IntegrationChecker.Impl(logger)
-          new DistageTestRunner[F](testReporter, checker, specEnv, toRun, _.isInstanceOf[TestCanceledException], parallelTestsAlways)
+          new DistageTestRunner[F](testReporter, checker, specEnv, toRun, _.isInstanceOf[TestCanceledException], parallelExecution, parallelEnvExecution)
         }
         runner.run()
       }
@@ -204,7 +206,8 @@ abstract class DistageScalatestTestSuiteRunner[F[_]](implicit override val tagMo
     }
   }
 
-  protected def parallelTestsAlways: Boolean = true
+  protected def parallelExecution: Boolean = true
+  protected def parallelEnvExecution: Boolean = true
 
   private def mkTestReporter(args: Args): TestReporter = {
     val scalatestReporter = new ScalatestReporter(args.reporter)
diff --git a/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/SafeTestReporter.scala b/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/SafeTestReporter.scala
index 707e3407f..b973ef0e9 100644
--- a/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/SafeTestReporter.scala
+++ b/distage/distage-testkit-scalatest/src/main/scala/org/scalatest/SafeTestReporter.scala
@@ -1,15 +1,14 @@
 package org.scalatest
 
-import izumi.distage.testkit.services.dstest.DistageTestRunner.TestStatus.{Done, Finished, Ignored, Running}
+import izumi.distage.testkit.services.dstest.DistageTestRunner.TestStatus.{Done, Running}
 import izumi.distage.testkit.services.dstest.DistageTestRunner.{SuiteData, TestMeta, TestReporter, TestStatus}
-import izumi.fundamentals.platform.language.Quirks._
 import izumi.fundamentals.platform.language.unused
 
 import scala.collection.mutable
 
 class SafeTestReporter(underlying: TestReporter) extends TestReporter {
   private val delayedReports = new mutable.LinkedHashMap[TestMeta, TestStatus]()
-  private var runningTest: Option[TestMeta] = None
+  private val runningTests = new mutable.HashMap[String, TestMeta]()
 
   override def onFailure(f: Throwable): Unit = synchronized {
     endAll()
@@ -17,7 +16,7 @@ class SafeTestReporter(underlying: TestReporter) extends TestReporter {
   }
 
   override def endAll(): Unit = synchronized {
-    finish()
+    finish(_ => true)
     underlying.endAll()
   }
 
@@ -27,35 +26,32 @@ class SafeTestReporter(underlying: TestReporter) extends TestReporter {
 
   override def testStatus(test: TestMeta, testStatus: TestStatus): Unit = synchronized {
     testStatus match {
-//      case TestStatus.Scheduled => reportStatus(test, testStatus)
-
       case TestStatus.Running =>
-        runningTest match {
+        runningTests.get(test.id.suiteId) match {
           case Some(_) =>
+            // a test is running in this suite, delay new test
             delayedReports.put(test, testStatus)
           case None =>
+            // no test is running in this suite, report this one and set it as running
             reportStatus(test, testStatus)
-            runningTest = Some(test)
+            runningTests(test.id.suiteId) = test
         }
-
-      case _: Ignored =>
-        runningTest match {
-          case None =>
-            reportStatus(test, TestStatus.Running)
+      case _: Done =>
+        runningTests.get(test.id.suiteId) match {
+          case Some(value) if value == test =>
+            // finished current running test in suite,
+            // report delayed reports in suite &
+            // switch to another currently running test in suite if exists
+            runningTests.remove(test.id.suiteId)
             reportStatus(test, testStatus)
+            finish(_.id.suiteId == test.id.suiteId)
           case Some(_) =>
-            // Ignores happen before running, so an ignored test cannot be the same as `signalled`
-            delayedReports.put(test, testStatus)
-        }
-
-      case _: Finished =>
-        runningTest match {
-          case Some(value) if value == test =>
-            finishTest(test, testStatus)
-            runningTest = None
-            finish()
-          case _ =>
+            // another test is running in this suite, delay finish report
             delayedReports.put(test, testStatus)
+          case None =>
+            // no other test is running in this suite, report directly
+            reportStatus(test, TestStatus.Running)
+            reportStatus(test, testStatus)
         }
     }
     ()
@@ -65,26 +61,24 @@ class SafeTestReporter(underlying: TestReporter) extends TestReporter {
     underlying.testStatus(test, testStatus)
   }
 
-  private def finish(): Unit = {
-    val finishedTests = delayedReports.collect { case (t, s: Done) => (t, s) }
+  private def finish(predicate: TestMeta => Boolean): Unit = synchronized {
+    val finishedTests = delayedReports.collect { case (t, s: Done) if predicate(t) => (t, s) }
 
     // report all finished tests
     finishedTests
       .foreach {
         case (t, s) =>
           reportStatus(t, TestStatus.Running)
-          finishTest(t, s)
+          reportStatus(t, s)
+          delayedReports.remove(t)
       }
     // switch to another test if it's already running
-    delayedReports.collectFirst {
-      case (t, Running) =>
+    delayedReports.toList.foreach {
+      case (t, Running) if predicate(t) && !runningTests.contains(t.id.suiteId) =>
+        runningTests(t.id.suiteId) = t
         reportStatus(t, TestStatus.Running)
-        runningTest = Some(t)
-    }.discard()
-  }
-
-  private def finishTest(test: TestMeta, testStatus: TestStatus): Option[TestStatus] = {
-    reportStatus(test, testStatus)
-    delayedReports.remove(test)
+        delayedReports.remove(t)
+      case _ =>
+    }
   }
 }
diff --git a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/test/scala/izumi/idealingua/runtime/rpc/http4s/Http4sTransportTest.scala b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/test/scala/izumi/idealingua/runtime/rpc/http4s/Http4sTransportTest.scala
index a9a2159c8..7dcba0d6e 100644
--- a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/test/scala/izumi/idealingua/runtime/rpc/http4s/Http4sTransportTest.scala
+++ b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/test/scala/izumi/idealingua/runtime/rpc/http4s/Http4sTransportTest.scala
@@ -65,10 +65,14 @@ class Http4sTransportTest extends AnyWordSpec {
         assert(BIOR.unsafeRun(greeterClient.greet("John", "Smith")) == "Hi, John Smith!")
         assert(BIOR.unsafeRun(greeterClient.alternative()) == "value")
 
-        ioService.wsSessionStorage.buzzersFor("user").foreach {
-          buzzer =>
-            val client = new GreeterServiceClientWrapped(buzzer)
-            assert(BIOR.unsafeRun(client.greet("John", "Buzzer")) == "Hi, John Buzzer!")
+        BIOR.unsafeRunSyncAsEither(ioService.wsSessionStorage.buzzersFor("user")) match {
+          case Success(buzzers) =>
+            buzzers.foreach {
+              buzzer =>
+                val client = new GreeterServiceClientWrapped(buzzer)
+                assert(BIOR.unsafeRun(client.greet("John", "Buzzer")) == "Hi, John Buzzer!")
+            }
+          case v => fail(s"Expected success result but got $v")
         }
 
         disp.setupCredentials("user", "badpass")
diff --git a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/test/scala/izumi/idealingua/runtime/rpc/http4s/fixtures/Http4sTestContext.scala b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/test/scala/izumi/idealingua/runtime/rpc/http4s/fixtures/Http4sTestContext.scala
index 9c4c83b89..dd84ba2fe 100644
--- a/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/test/scala/izumi/idealingua/runtime/rpc/http4s/fixtures/Http4sTestContext.scala
+++ b/idealingua-v1/idealingua-v1-runtime-rpc-http4s/src/test/scala/izumi/idealingua/runtime/rpc/http4s/fixtures/Http4sTestContext.scala
@@ -114,9 +114,7 @@ object Http4sTestContext {
     AuthMiddleware(authUser),
     wsContextProvider,
     storage,
-    Seq(
-      WsSessionListener.empty[String]
-    ),
+    Seq(WsSessionListener.empty[rt.BiIO, String]),
     RT.logger,
     RT.printer,
   )