Say we want different flavors of effect wrappings:
- Cats, over
F[_]
- Cats, over
IO
- ZIO
- Monix
We introduce an enum for these:
sealed trait EffectWrappingFlavor
object EffectWrappingFlavor {
case object ZIO extends EffectWrappingFlavor
case object CatsF extends EffectWrappingFlavor
case object CatsIO extends EffectWrappingFlavor
case object Monix extends EffectWrappingFlavor
}
We also want to be able to describe how to wrap functions:
sealed trait EffectWrapping
object EffectWrapping {
case object Pure extends EffectWrapping
case object Synchronous extends EffectWrapping
case class Asynchronous(
successCallback: JSIdentifier,
failureCallback: Option[JSIdentifier]
) extends EffectWrapping
case class Stream(
addCallback: JSIdentifier,
removeCallback: Option[JSIdentifier]
) extends EffectWrapping
}
We introduce three new keys:
val stEffectWrappingFlavor =
settingKey[Option[EffectWrappingFlavor]](
"What flavor, if any, to generate effect wrappings in"
)
val stEffectWrappingMap =
settingKey[Map[JSIdentifier, EffectWrapping]](
"A map from JavaScript methods to how they should be wrapped"
)
val stEffectWrappingStrict =
settingKey[Boolean](
"If true, default wrappings will not be generated"
)
Now, say we:
Compile / npmDependencies ++= Seq(
"pixi.js" -> "5.2.4"
),
stEffectWrappingFlavor :=
Some(EffectWrappingFlavor.CatsF),
stEffectWrappingStrict := true,
Initially, no methods will be output by the effect wrapper, as we have turned on strict mode. Let's add a mapping:
stEffectWrappingMap +=
"PIXI.Loader#onComplete" -> EffectWrapping.Stream(
"add",
"remove
)
This will generate a two classes, like so:
package typings.pixiJs.effect.PIXI
import cats.effect.{ConcurrentEffect, ContextShift, IO}
import cats.implicits._
import fs2._
import fs2.concurrent.Queue
import scala.scalajs.js
class Loader(val internal: typings.pixiJs.PIXI.Loader) extends AnyVal {
def onComplete[F[_]](
implicit cce: ConcurrentEffect[F],
cs: ContextShift[F]
): Stream[F, (Loader, Map[String, LoaderResource])] =
for {
q <- Stream.eval(Queue.unbounded[F, (Loader, Map[String, LoaderResource])])
callback: js.Function2[
typings.pixiJs.PIXI.Loader,
typings.std.Partial[typings.std.Record[String, typings.pixiJs.PIXI.LoaderResource]],
Unit
] = (iloader, record) => {
val recordAsDict = record.asInstanceOf[js.Dictionary[typings.pixiJs.PIXI.LoaderResource]]
val dictAsMap = js.Object.entries(recordAsDict).map {
case js.Tuple2(name, value) => (name, new LoaderResource(value))
}.toMap
ce.runAsync(
q.enqueue1((new Loader(iloader), dictAsMap))
)(_ => IO.unit).unsafeRunSync()
}
_ <- Stream.bracket(
cce.delay(internal.onComplete.add(callback))
)(binding => cce.delay(internal.onComplete.detach(binding)))
item <- q.dequeue
} yield item
}
class LoaderResource(val internal: typings.pixiJs.PIXI.LoaderResource) extends AnyVal
This LoaderResource
will be filled with methods once mappings are added in stEffectWrappingMap
.
If, instead of CatsF
, we used:
CatsIO
, we would useIO.runAsync
instead ofcce.runAsync
, andIO.apply
instead ofcce.delay
Monix
, we would create anObservable
; I haven't been able to find a good resource on creating anObservable
from a repeated callbackZIO
, we would create a ZStream
; same problem as above
If we turned strict mode off, the methods above would be automatically generated like this:
type OnCompleteSignal = (Loader, Map[String, LoaderResource]) => Unit
class Loader(val internal: typings.pixiJs.PIXI.Loader) extends AnyVal {
def onComplete[F[_]](implicit s: Sync[F]): F[Signal[OnCompleteSignal]] =
s.delay(internal.onComplete).map(new Signal[OnCompleteSignal](_))
}
// PIXI doesn't actually have any bindings for Signal, which is odd
// https://englercj.github.io/type-signals/classes/signal.html
class Signal[T](val internal: ???) extends AnyVal {
def add[F[_]](callback: T)(implicit s: Sync[F]): F[SignalBinding] =
s.delay(internal.add(callback)).map(new SignalBinding(_))
def detach[F[_]](node: SignalBinding)(implicit s: Sync[F]): F[Unit] =
s.delay(internal.detach(node.internal)).as(())
}
Which isn't too usable, at least in a pure environment, because callbacks are not A => F[Unit]
, and there's no perfect way to
tell automatically whether a Higher Order Function wants the internal function in question to be pure or not.
Note that, when strict mode is off, all unbound methods are just wrapped in Sync
as if they're run-of-the-mill
impure functions; I did not write the generations for all methods that would be generated in practice.
Issues this method does not address:
- On an asynchronous callback, an error status may be returned in the same callback as the success callback.
- On an asynchronous failure callback, an error may not extend
Throwable
. JSIdentifier
is not defined here; someone with experience in the ScalablyTyped internals would probably be able to write it better.- Conversion from
typings.std.Partial[typings.std.Record[String, typings.pixiJs.PIXI.LoaderResource]]
is something that can safely be automatically generated, but I don't know if it's efficient/worth it/the right option. - TypeScript types that cannot be safely represented in the Scala world.