All Downloads are FREE. Search and download functionalities are using the official Maven repository.

arrow.effects.reactor.MonoK.kt Maven / Gradle / Ivy

package arrow.effects.reactor

import arrow.core.Either
import arrow.core.Either.Left
import arrow.core.Either.Right
import arrow.core.NonFatal
import arrow.effects.OnCancel
import arrow.effects.internal.Platform
import arrow.effects.reactor.CoroutineContextReactorScheduler.asScheduler
import arrow.effects.typeclasses.Disposable
import arrow.effects.typeclasses.ExitCase
import arrow.higherkind
import reactor.core.publisher.Mono
import reactor.core.publisher.MonoSink
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.CoroutineContext

fun  Mono.k(): MonoK = MonoK(this)

fun  MonoKOf.value(): Mono =
  this.fix().mono

@higherkind
data class MonoK(val mono: Mono) : MonoKOf, MonoKKindedJ {
  fun  map(f: (A) -> B): MonoK =
    mono.map(f).k()

  fun  ap(fa: MonoKOf<(A) -> B>): MonoK =
    flatMap { a -> fa.fix().map { ff -> ff(a) } }

  fun  flatMap(f: (A) -> MonoKOf): MonoK =
    mono.flatMap { f(it).fix().mono }.k()

  /**
   * A way to safely acquire a resource and release in the face of errors and cancellation.
   * It uses [ExitCase] to distinguish between different exit cases when releasing the acquired resource.
   *
   * @param use is the action to consume the resource and produce an [MonoK] with the result.
   * Once the resulting [MonoK] terminates, either successfully, error or disposed,
   * the [release] function will run to clean up the resources.
   *
   * @param release the allocated resource after the resulting [MonoK] of [use] is terminates.
   *
   * {: data-executable='true'}
   * ```kotlin:ank
   * import arrow.effects.*
   * import arrow.effects.reactor.*
   * import arrow.effects.typeclasses.ExitCase
   *
   * class File(url: String) {
   *   fun open(): File = this
   *   fun close(): Unit {}
   *   fun content(): MonoK =
   *     MonoK.just("This file contains some interesting content!")
   * }
   *
   * fun openFile(uri: String): MonoK = MonoK { File(uri).open() }
   * fun closeFile(file: File): MonoK = MonoK { file.close() }
   *
   * fun main(args: Array) {
   *   //sampleStart
   *   val safeComputation = openFile("data.json").bracketCase(
   *     release = { file, exitCase ->
   *       when (exitCase) {
   *         is ExitCase.Completed -> { /* do something */ }
   *         is ExitCase.Canceled -> { /* do something */ }
   *         is ExitCase.Error -> { /* do something */ }
   *       }
   *       closeFile(file)
   *     },
   *     use = { file -> file.content() }
   *   )
   *   //sampleEnd
   *   println(safeComputation)
   * }
   *  ```
   */
  fun  bracketCase(use: (A) -> MonoKOf, release: (A, ExitCase) -> MonoKOf): MonoK =
    MonoK(Mono.create { sink ->
      val isCanceled = AtomicBoolean(false)
      sink.onCancel { isCanceled.set(true) }
      val a: A? = mono.block()
      if (a != null) {
        if (isCanceled.get()) release(a, ExitCase.Canceled).fix().mono.subscribe({}, sink::error)
        else try {
          sink.onDispose(use(a).fix()
            .flatMap { b -> release(a, ExitCase.Completed).fix().map { b } }
            .handleErrorWith { e -> release(a, ExitCase.Error(e)).fix().flatMap { MonoK.raiseError(e) } }
            .mono
            .doOnCancel { release(a, ExitCase.Canceled).fix().mono.subscribe({}, sink::error) }
            .subscribe(sink::success, sink::error)
          )
        } catch (e: Throwable) {
          if (NonFatal(e)) {
            release(a, ExitCase.Error(e)).fix().mono.subscribe({
              sink.error(e)
            }, { e2 ->
              sink.error(Platform.composeErrors(e, e2))
            })
          } else {
            throw e
          }
        }
      } else sink.success(null)
    })

  fun handleErrorWith(function: (Throwable) -> MonoK): MonoK =
    mono.onErrorResume { t: Throwable -> function(t).mono }.k()

  fun continueOn(ctx: CoroutineContext): MonoK =
    mono.publishOn(ctx.asScheduler()).k()

  fun runAsync(cb: (Either) -> MonoKOf): MonoK =
    mono.flatMap { cb(Right(it)).value() }.onErrorResume { cb(Left(it)).value() }.k()

  fun runAsyncCancellable(cb: (Either) -> MonoKOf): MonoK =
    Mono.fromCallable {
      val disposable: reactor.core.Disposable = runAsync(cb).value().subscribe()
      val dispose: Disposable = disposable::dispose
      dispose
    }.k()

  override fun equals(other: Any?): Boolean =
    when (other) {
      is MonoK<*> -> this.mono == other.mono
      is Mono<*> -> this.mono == other
      else -> false
    }

  override fun hashCode(): Int = mono.hashCode()

  companion object {
    fun  just(a: A): MonoK =
      Mono.just(a).k()

    fun  raiseError(t: Throwable): MonoK =
      Mono.error(t).k()

    operator fun  invoke(fa: () -> A): MonoK =
      defer { just(fa()) }

    fun  defer(fa: () -> MonoKOf): MonoK =
      Mono.defer { fa().value() }.k()

    /**
     * Creates a [MonoK] that'll run [MonoKProc].
     *
     * {: data-executable='true'}
     *
     * ```kotlin:ank
     * import arrow.core.Either
     * import arrow.core.right
     * import arrow.effects.reactor.MonoK
     * import arrow.effects.reactor.MonoKConnection
     * import arrow.effects.reactor.value
     *
     * class Resource {
     *   fun asyncRead(f: (String) -> Unit): Unit = f("Some value of a resource")
     *   fun close(): Unit = Unit
     * }
     *
     * fun main(args: Array) {
     *   //sampleStart
     *   val result = MonoK.async { conn: MonoKConnection, cb: (Either) -> Unit ->
     *     val resource = Resource()
     *     conn.push(MonoK { resource.close() })
     *     resource.asyncRead { value -> cb(value.right()) }
     *   }
     *   //sampleEnd
     *   result.value().subscribe(::println)
     * }
     * ```
     */
    fun  async(fa: MonoKProc): MonoK =
      Mono.create { sink ->
        val conn = MonoKConnection()
        val isCancelled = AtomicBoolean(false) //Sink is missing isCancelled so we have to do book keeping.
        conn.push(MonoK { if (!isCancelled.get()) sink.error(OnCancel.CancellationException) })
        sink.onCancel {
          isCancelled.compareAndSet(false, true)
          conn.cancel().value().subscribe()
        }

        fa(conn) { either: Either ->
          either.fold({
            sink.error(it)
          }, {
            sink.success(it)
          })
        }
      }.k()

    fun  asyncF(fa: MonoKProcF): MonoK =
      Mono.create { sink: MonoSink ->
        val conn = MonoKConnection()
        val isCancelled = AtomicBoolean(false) //Sink is missing isCancelled so we have to do book keeping.
        conn.push(MonoK { if (!isCancelled.get()) sink.error(OnCancel.CancellationException) })
        sink.onCancel {
          isCancelled.compareAndSet(false, true)
          conn.cancel().value().subscribe()
        }

        fa(conn) { either: Either ->
          either.fold({
            sink.error(it)
          }, {
            sink.success(it)
          })
        }.fix().mono.subscribe({}, sink::error)
      }.k()

    tailrec fun  tailRecM(a: A, f: (A) -> MonoKOf>): MonoK {
      val either = f(a).value().block()
      return when (either) {
        is Either.Left -> tailRecM(either.a, f)
        is Either.Right -> Mono.just(either.b).k()
      }
    }
  }
}