diff --git a/arrow-libs/core/arrow-autoclose/api/arrow-scope.api b/arrow-libs/core/arrow-autoclose/api/arrow-scope.api new file mode 100644 index 00000000000..49f0f82e513 --- /dev/null +++ b/arrow-libs/core/arrow-autoclose/api/arrow-scope.api @@ -0,0 +1,53 @@ +public abstract interface class arrow/AutoCloseScope { + public abstract fun autoClose (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; + public abstract fun autoClose (Lkotlin/jvm/functions/Function1;)V + public abstract fun install (Ljava/lang/AutoCloseable;)Ljava/lang/AutoCloseable; +} + +public final class arrow/AutoCloseScope$DefaultImpls { + public static fun autoClose (Larrow/AutoCloseScope;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; + public static fun install (Larrow/AutoCloseScope;Ljava/lang/AutoCloseable;)Ljava/lang/AutoCloseable; +} + +public final class arrow/AutoCloseScopeKt { + public static final fun autoCloseScope (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; +} + +public final class arrow/AutoCloseableExtensionsKt { + public static final fun install (Larrow/AutoCloseScope;Ljava/lang/AutoCloseable;)Ljava/lang/AutoCloseable; +} + +public final class arrow/CoroutineScopeKt { + public static final fun coroutineScope (Larrow/Scope;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class arrow/DefaultAutoCloseScope : arrow/AutoCloseScope { + public fun ()V + public fun autoClose (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; + public fun autoClose (Lkotlin/jvm/functions/Function1;)V + public final fun close (Ljava/lang/Throwable;)Ljava/lang/Void; + public fun install (Ljava/lang/AutoCloseable;)Ljava/lang/AutoCloseable; +} + +public final class arrow/SagaKt { + public static final fun saga (Larrow/Scope;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class arrow/Scope : arrow/AutoCloseScope { + public abstract fun autoClose (Lkotlin/jvm/functions/Function1;)V + public abstract fun finalise (Lkotlin/jvm/functions/Function2;)V +} + +public final class arrow/Scope$DefaultImpls { + public static fun autoClose (Larrow/Scope;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; + public static fun install (Larrow/Scope;Ljava/lang/AutoCloseable;)Ljava/lang/AutoCloseable; +} + +public final class arrow/ScopeKt { + public static final fun scoped (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class arrow/ThrowIfFatalKt { + public static final fun throwIfFatal (Ljava/lang/Throwable;)Ljava/lang/Throwable; +} + diff --git a/arrow-libs/core/arrow-autoclose/api/arrow-scope.klib.api b/arrow-libs/core/arrow-autoclose/api/arrow-scope.klib.api new file mode 100644 index 00000000000..d457b00e1cf --- /dev/null +++ b/arrow-libs/core/arrow-autoclose/api/arrow-scope.klib.api @@ -0,0 +1,27 @@ +// Klib ABI Dump +// Targets: [iosArm64, iosSimulatorArm64, iosX64, js, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosSimulatorArm64, watchosX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +abstract interface arrow/AutoCloseScope { // arrow/AutoCloseScope|null[0] + abstract fun autoClose(kotlin/Function1) // arrow/AutoCloseScope.autoClose|autoClose(kotlin.Function1){}[0] + open fun <#A1: kotlin/Any?> autoClose(kotlin/Function0<#A1>, kotlin/Function2<#A1, kotlin/Throwable?, kotlin/Unit>): #A1 // arrow/AutoCloseScope.autoClose|autoClose(kotlin.Function0<0:0>;kotlin.Function2<0:0,kotlin.Throwable?,kotlin.Unit>){0§}[0] + open fun <#A1: kotlin/AutoCloseable> install(#A1): #A1 // arrow/AutoCloseScope.install|install(0:0){0§}[0] +} +abstract interface arrow/Scope : arrow/AutoCloseScope { // arrow/Scope|null[0] + abstract fun autoClose(kotlin/Function1) // arrow/Scope.autoClose|autoClose(kotlin.Function1){}[0] + abstract fun finalise(kotlin.coroutines/SuspendFunction1) // arrow/Scope.finalise|finalise(kotlin.coroutines.SuspendFunction1){}[0] +} +final class arrow/DefaultAutoCloseScope : arrow/AutoCloseScope { // arrow/DefaultAutoCloseScope|null[0] + constructor () // arrow/DefaultAutoCloseScope.|(){}[0] + final fun autoClose(kotlin/Function1) // arrow/DefaultAutoCloseScope.autoClose|autoClose(kotlin.Function1){}[0] + final fun close(kotlin/Throwable?): kotlin/Nothing? // arrow/DefaultAutoCloseScope.close|close(kotlin.Throwable?){}[0] +} +final fun (kotlin/Throwable).arrow/throwIfFatal(): kotlin/Throwable // arrow/throwIfFatal|throwIfFatal@kotlin.Throwable(){}[0] +final inline fun <#A: kotlin/Any?> arrow/autoCloseScope(kotlin/Function1): #A // arrow/autoCloseScope|autoCloseScope(kotlin.Function1){0§}[0] +final suspend fun (arrow/Scope).arrow/coroutineScope(kotlin.coroutines/CoroutineContext): kotlinx.coroutines/CoroutineScope // arrow/coroutineScope|coroutineScope@arrow.Scope(kotlin.coroutines.CoroutineContext){}[0] +final suspend fun <#A: kotlin/Any?> (arrow/Scope).arrow/saga(kotlin.coroutines/SuspendFunction0<#A>, kotlin.coroutines/SuspendFunction2<#A, kotlin/Throwable?, kotlin/Unit>): #A // arrow/saga|saga@arrow.Scope(kotlin.coroutines.SuspendFunction0<0:0>;kotlin.coroutines.SuspendFunction2<0:0,kotlin.Throwable?,kotlin.Unit>){0§}[0] +final suspend fun <#A: kotlin/Any?> arrow/scoped(kotlin/Function1): #A // arrow/scoped|scoped(kotlin.Function1){0§}[0] diff --git a/arrow-libs/core/arrow-autoclose/build.gradle.kts b/arrow-libs/core/arrow-autoclose/build.gradle.kts index 41bbbead065..02a8534b5ea 100644 --- a/arrow-libs/core/arrow-autoclose/build.gradle.kts +++ b/arrow-libs/core/arrow-autoclose/build.gradle.kts @@ -22,7 +22,6 @@ kotlin { sourceSets { commonMain { dependencies { - implementation(libs.kotlin.stdlib) implementation(projects.arrowAtomic) } } diff --git a/arrow-libs/core/arrow-autoclose/src/commonMain/kotlin/arrow/AutoCloseScope.kt b/arrow-libs/core/arrow-autoclose/src/commonMain/kotlin/arrow/AutoCloseScope.kt index d42f23cd54d..5602eca94b2 100644 --- a/arrow-libs/core/arrow-autoclose/src/commonMain/kotlin/arrow/AutoCloseScope.kt +++ b/arrow-libs/core/arrow-autoclose/src/commonMain/kotlin/arrow/AutoCloseScope.kt @@ -61,6 +61,20 @@ import kotlin.coroutines.cancellation.CancellationException * ``` * */ +public interface AutoCloseScope { + public fun autoClose(close: (Throwable?) -> Unit): Unit + + public fun autoClose( + acquire: () -> A, + release: (A, Throwable?) -> Unit + ): A = + acquire().also { a -> autoClose { e -> release(a, e) } } + + @ExperimentalStdlibApi + public fun install(autoCloseable: A): A = + autoClose({ autoCloseable }) { a, _ -> a.close() } +} + public inline fun autoCloseScope(block: AutoCloseScope.() -> A): A { val scope = DefaultAutoCloseScope() return try { @@ -73,29 +87,11 @@ public inline fun autoCloseScope(block: AutoCloseScope.() -> A): A { } } -public interface AutoCloseScope { - public fun autoClose( - acquire: () -> A, - release: (A, Throwable?) -> Unit - ): A - - @ExperimentalStdlibApi - public fun install(autoCloseable: A): A = - autoClose({ autoCloseable }) { a, _ -> a.close() } -} - @PublishedApi internal class DefaultAutoCloseScope : AutoCloseScope { private val finalizers = Atomic(emptyList<(Throwable?) -> Unit>()) - - override fun autoClose(acquire: () -> A, release: (A, Throwable?) -> Unit): A = - try { - acquire().also { a -> - finalizers.update { it + { e -> release(a, e) } } - } - } catch (e: Throwable) { - throw e - } + override fun autoClose(close: (Throwable?) -> Unit) = + finalizers.update { it + { e -> close(e) } } fun close(error: Throwable?): Nothing? { return finalizers.get().asReversed().fold(error) { acc, function -> diff --git a/arrow-libs/core/arrow-continuations/api/arrow-continuations.api b/arrow-libs/core/arrow-continuations/api/arrow-continuations.api deleted file mode 100644 index 8816ae2818c..00000000000 --- a/arrow-libs/core/arrow-continuations/api/arrow-continuations.api +++ /dev/null @@ -1,56 +0,0 @@ -public abstract interface class arrow/continuations/Effect { - public static final field Companion Larrow/continuations/Effect$Companion; - public abstract fun control ()Larrow/continuations/generic/DelimitedScope; -} - -public final class arrow/continuations/Effect$Companion { - public final fun restricted (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; - public final fun suspended (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public final class arrow/continuations/Reset { - public static final field INSTANCE Larrow/continuations/Reset; - public final fun restricted (Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; - public final fun suspended (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public final class arrow/continuations/generic/AtomicRefKt { - public static final fun getAndUpdate (Ljava/util/concurrent/atomic/AtomicReference;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; - public static final fun loop (Ljava/util/concurrent/atomic/AtomicReference;Lkotlin/jvm/functions/Function1;)Ljava/lang/Void; - public static final fun update (Ljava/util/concurrent/atomic/AtomicReference;Lkotlin/jvm/functions/Function1;)V - public static final fun updateAndGet (Ljava/util/concurrent/atomic/AtomicReference;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; -} - -public class arrow/continuations/generic/ControlThrowable : java/lang/Throwable { - public fun ()V - public fun fillInStackTrace ()Ljava/lang/Throwable; -} - -public final class arrow/continuations/generic/ControlThrowableKt { - public static final field deprecateArrowContinuation Ljava/lang/String; -} - -public abstract interface class arrow/continuations/generic/DelimitedContinuation { - public abstract fun invoke (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public abstract interface class arrow/continuations/generic/DelimitedScope { - public abstract fun shift (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public abstract interface class arrow/continuations/generic/RestrictedScope : arrow/continuations/generic/DelimitedScope { - public abstract fun shift (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public abstract fun shift (Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public final class arrow/continuations/generic/RestrictedScope$DefaultImpls { - public static fun shift (Larrow/continuations/generic/RestrictedScope;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public final class arrow/continuations/generic/ShortCircuit : arrow/continuations/generic/ControlThrowable { - public final fun getRaiseValue ()Ljava/lang/Object; -} - -public abstract interface class arrow/continuations/generic/SuspendedScope : arrow/continuations/generic/DelimitedScope { -} - diff --git a/arrow-libs/core/arrow-continuations/src/wasmJsMain/kotlin/arrow/continuations/generic/AtomicRef.kt b/arrow-libs/core/arrow-continuations/src/wasmJsMain/kotlin/arrow/continuations/generic/AtomicRef.kt deleted file mode 100644 index 9b845acb0a9..00000000000 --- a/arrow-libs/core/arrow-continuations/src/wasmJsMain/kotlin/arrow/continuations/generic/AtomicRef.kt +++ /dev/null @@ -1,31 +0,0 @@ -package arrow.continuations.generic - -@Deprecated(deprecateArrowContinuation) -public actual class AtomicRef actual constructor(initialValue: V) { - private var internalValue: V = initialValue - - /** - * Compare current value with expected and set to new if they're the same. Note, 'compare' is checking - * the actual object id, not 'equals'. - */ - public actual fun compareAndSet(expected: V, new: V): Boolean { - return if (expected === internalValue) { - internalValue = new - true - } else { - false - } - } - - public actual fun getAndSet(value: V): V { - val oldValue = internalValue - internalValue = value - return oldValue - } - - public actual fun get(): V = internalValue - - public actual fun set(value: V) { - internalValue = value - } -} diff --git a/arrow-libs/core/arrow-continuations/src/wasmJsMain/kotlin/arrow/continuations/generic/ControlThrowable.kt b/arrow-libs/core/arrow-continuations/src/wasmJsMain/kotlin/arrow/continuations/generic/ControlThrowable.kt deleted file mode 100644 index 3ba52914bac..00000000000 --- a/arrow-libs/core/arrow-continuations/src/wasmJsMain/kotlin/arrow/continuations/generic/ControlThrowable.kt +++ /dev/null @@ -1,10 +0,0 @@ -package arrow.continuations.generic - -/** - * A [Throwable] class intended for control flow. - * Instance of [ControlThrowable] should **not** be caught, - * and `arrow.core.NonFatal` does not catch this [Throwable]. - * By extension, `Either.catch` and `Raise.catch` also don't catch [ControlThrowable]. - */ -@Deprecated(deprecateArrowContinuation) -public actual open class ControlThrowable : Throwable() diff --git a/arrow-libs/core/arrow-core-serialization/build.gradle.kts b/arrow-libs/core/arrow-core-serialization/build.gradle.kts index b5032a523d2..e2a452c3f7a 100644 --- a/arrow-libs/core/arrow-core-serialization/build.gradle.kts +++ b/arrow-libs/core/arrow-core-serialization/build.gradle.kts @@ -3,7 +3,7 @@ plugins { id(libs.plugins.kotlin.multiplatform.get().pluginId) alias(libs.plugins.arrowGradleConfig.kotlin) - + alias(libs.plugins.publish) alias(libs.plugins.kotlinx.kover) alias(libs.plugins.spotless) id(libs.plugins.kotlinx.serialization.get().pluginId) diff --git a/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api b/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api index 315aa8734d7..2c30383a98b 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api +++ b/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api @@ -253,6 +253,7 @@ public abstract interface class arrow/fx/coroutines/ResourceScope : arrow/AutoCl } public final class arrow/fx/coroutines/ResourceScope$DefaultImpls { + public static fun autoClose (Larrow/fx/coroutines/ResourceScope;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; public static fun install (Larrow/fx/coroutines/ResourceScope;Ljava/lang/AutoCloseable;)Ljava/lang/AutoCloseable; public static fun onRelease (Larrow/fx/coroutines/ResourceScope;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun release (Larrow/fx/coroutines/ResourceScope;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/arrow-libs/fx/arrow-fx-coroutines/build.gradle.kts b/arrow-libs/fx/arrow-fx-coroutines/build.gradle.kts index dc5673e8672..7969d14c84f 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/build.gradle.kts +++ b/arrow-libs/fx/arrow-fx-coroutines/build.gradle.kts @@ -22,7 +22,7 @@ kotlin { commonMain { dependencies { api(projects.arrowCore) - api(projects.arrowAutoclose) + api(projects.arrowFxDsl) api(libs.coroutines.core) implementation(libs.kotlin.stdlib) } diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt index 6ce0262c9b2..6c8a16de0df 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt @@ -513,6 +513,9 @@ private value class ResourceScopeImpl( } }) + override fun autoClose(close: (Throwable?) -> Unit) = + autoClose({ }) { _, e -> close(e) } + override fun autoClose(acquire: () -> A, release: (A, Throwable?) -> Unit): A = acquire().also { a -> val finalizer: suspend (ExitCase) -> Unit = { exitCase -> diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/await/AwaitAllScope.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/await/AwaitAllScope.kt deleted file mode 100644 index 3c20c5500af..00000000000 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/await/AwaitAllScope.kt +++ /dev/null @@ -1,72 +0,0 @@ -package arrow.fx.coroutines.await - -import arrow.atomic.Atomic -import arrow.atomic.update -import kotlinx.coroutines.async as coroutinesAsync -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.awaitAll as coroutinesAwaitAll -import kotlinx.coroutines.coroutineScope -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext - -public suspend fun awaitAll( - block: suspend AwaitAllScope.() -> A -): A = coroutineScope { block(AwaitAllScope(this)) } - -public suspend fun CoroutineScope.awaitAll( - block: suspend AwaitAllScope.() -> A -): A = block(AwaitAllScope(this)) - -/** - * Within an [AwaitAllScope], any call to [kotlinx.coroutines.Deferred.await] - * causes all the other [Deferred] in the same block to be awaited too. - * That way you can get more concurrency without having to sacrifice - * readability. - * - * ```kotlin - * suspend fun loadUserInfo(id: UserId): UserInfo = await { - * val name = async { loadUserFromDb(id) } - * val avatar = async { loadAvatar(id) } - * UserInfo( - * name.await(), // <- at this point every 'async' is 'await'ed - * avatar.await() // <- so when you reach this 'await', the value is already there - * ) - * } - * - * suspend fun loadUserInfoWithoutAwait(id: UserId): UserInfo { - * val name = async { loadUserFromDb(id) } - * val avatar = async { loadAvatar(id) } - * awaitAll(name, avatar) // <- this is required otherwise - * return UserInfo( - * name.await(), - * avatar.await() - * ) - * } - * ``` - */ -public class AwaitAllScope( - private val scope: CoroutineScope -): CoroutineScope by scope { - private val tasks: Atomic>> = Atomic(emptyList()) - - public fun async( - context: CoroutineContext = EmptyCoroutineContext, - start: CoroutineStart = CoroutineStart.DEFAULT, - block: suspend CoroutineScope.() -> T - ): Deferred { - val deferred = coroutinesAsync(context, start, block) - tasks.update { it + deferred } - return Await(deferred) - } - - private inner class Await( - private val deferred: Deferred - ): Deferred by deferred { - override suspend fun await(): T { - tasks.getAndSet(emptyList()).coroutinesAwaitAll() - return deferred.await() - } - } -} diff --git a/arrow-libs/fx/arrow-fx-dsl/api/arrow-autoclose.api b/arrow-libs/fx/arrow-fx-dsl/api/arrow-autoclose.api new file mode 100644 index 00000000000..76d49898115 --- /dev/null +++ b/arrow-libs/fx/arrow-fx-dsl/api/arrow-autoclose.api @@ -0,0 +1,28 @@ +public abstract interface class arrow/AutoCloseScope { + public abstract fun autoClose (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; + public abstract fun install (Ljava/lang/AutoCloseable;)Ljava/lang/AutoCloseable; +} + +public final class arrow/AutoCloseScope$DefaultImpls { + public static fun install (Larrow/AutoCloseScope;Ljava/lang/AutoCloseable;)Ljava/lang/AutoCloseable; +} + +public final class arrow/AutoCloseScopeKt { + public static final fun autoCloseScope (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; +} + +public final class arrow/AutoCloseableExtensionsKt { + public static final fun install (Larrow/AutoCloseScope;Ljava/lang/AutoCloseable;)Ljava/lang/AutoCloseable; +} + +public final class arrow/DefaultAutoCloseScope : arrow/AutoCloseScope { + public fun ()V + public fun autoClose (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; + public final fun close (Ljava/lang/Throwable;)Ljava/lang/Void; + public fun install (Ljava/lang/AutoCloseable;)Ljava/lang/AutoCloseable; +} + +public final class arrow/ThrowIfFatalKt { + public static final fun throwIfFatal (Ljava/lang/Throwable;)Ljava/lang/Throwable; +} + diff --git a/arrow-libs/fx/arrow-fx-dsl/api/arrow-autoclose.klib.api b/arrow-libs/fx/arrow-fx-dsl/api/arrow-autoclose.klib.api new file mode 100644 index 00000000000..986c15df535 --- /dev/null +++ b/arrow-libs/fx/arrow-fx-dsl/api/arrow-autoclose.klib.api @@ -0,0 +1,19 @@ +// Klib ABI Dump +// Targets: [iosArm64, iosSimulatorArm64, iosX64, js, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosSimulatorArm64, watchosX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +abstract interface arrow/AutoCloseScope { // arrow/AutoCloseScope|null[0] + abstract fun <#A1: kotlin/Any?> autoClose(kotlin/Function0<#A1>, kotlin/Function2<#A1, kotlin/Throwable?, kotlin/Unit>): #A1 // arrow/AutoCloseScope.autoClose|autoClose(kotlin.Function0<0:0>;kotlin.Function2<0:0,kotlin.Throwable?,kotlin.Unit>){0§}[0] + open fun <#A1: kotlin/AutoCloseable> install(#A1): #A1 // arrow/AutoCloseScope.install|install(0:0){0§}[0] +} +final class arrow/DefaultAutoCloseScope : arrow/AutoCloseScope { // arrow/DefaultAutoCloseScope|null[0] + constructor () // arrow/DefaultAutoCloseScope.|(){}[0] + final fun <#A1: kotlin/Any?> autoClose(kotlin/Function0<#A1>, kotlin/Function2<#A1, kotlin/Throwable?, kotlin/Unit>): #A1 // arrow/DefaultAutoCloseScope.autoClose|autoClose(kotlin.Function0<0:0>;kotlin.Function2<0:0,kotlin.Throwable?,kotlin.Unit>){0§}[0] + final fun close(kotlin/Throwable?): kotlin/Nothing? // arrow/DefaultAutoCloseScope.close|close(kotlin.Throwable?){}[0] +} +final fun (kotlin/Throwable).arrow/throwIfFatal(): kotlin/Throwable // arrow/throwIfFatal|throwIfFatal@kotlin.Throwable(){}[0] +final inline fun <#A: kotlin/Any?> arrow/autoCloseScope(kotlin/Function1): #A // arrow/autoCloseScope|autoCloseScope(kotlin.Function1){0§}[0] diff --git a/arrow-libs/fx/arrow-fx-dsl/api/arrow-scope.api b/arrow-libs/fx/arrow-fx-dsl/api/arrow-scope.api new file mode 100644 index 00000000000..40d2386ab6c --- /dev/null +++ b/arrow-libs/fx/arrow-fx-dsl/api/arrow-scope.api @@ -0,0 +1,53 @@ +public abstract interface class arrow/AutoCloseScope { + public abstract fun autoClose (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; + public abstract fun autoClose (Lkotlin/jvm/functions/Function1;)V + public abstract fun install (Ljava/lang/AutoCloseable;)Ljava/lang/AutoCloseable; +} + +public final class arrow/AutoCloseScope$DefaultImpls { + public static fun autoClose (Larrow/AutoCloseScope;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; + public static fun install (Larrow/AutoCloseScope;Ljava/lang/AutoCloseable;)Ljava/lang/AutoCloseable; +} + +public final class arrow/AutoCloseScopeKt { + public static final fun autoCloseScope (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; +} + +public final class arrow/AutoCloseableExtensionsKt { + public static final fun install (Larrow/AutoCloseScope;Ljava/lang/AutoCloseable;)Ljava/lang/AutoCloseable; +} + +public final class arrow/CoroutineScopeKt { + public static final fun coroutineScope (Larrow/Finally;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class arrow/DefaultAutoCloseScope : arrow/AutoCloseScope { + public fun ()V + public fun autoClose (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; + public fun autoClose (Lkotlin/jvm/functions/Function1;)V + public final fun close (Ljava/lang/Throwable;)Ljava/lang/Void; + public fun install (Ljava/lang/AutoCloseable;)Ljava/lang/AutoCloseable; +} + +public abstract interface class arrow/Finally : arrow/AutoCloseScope { + public abstract fun autoClose (Lkotlin/jvm/functions/Function1;)V + public abstract fun finalise (Lkotlin/jvm/functions/Function2;)V +} + +public final class arrow/Finally$DefaultImpls { + public static fun autoClose (Larrow/Finally;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; + public static fun install (Larrow/Finally;Ljava/lang/AutoCloseable;)Ljava/lang/AutoCloseable; +} + +public final class arrow/FinallyScopeKt { + public static final fun finally (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class arrow/SagaKt { + public static final fun saga (Larrow/Finally;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class arrow/ThrowIfFatalKt { + public static final fun throwIfFatal (Ljava/lang/Throwable;)Ljava/lang/Throwable; +} + diff --git a/arrow-libs/fx/arrow-fx-dsl/api/arrow-scope.klib.api b/arrow-libs/fx/arrow-fx-dsl/api/arrow-scope.klib.api new file mode 100644 index 00000000000..2caadc12eb9 --- /dev/null +++ b/arrow-libs/fx/arrow-fx-dsl/api/arrow-scope.klib.api @@ -0,0 +1,27 @@ +// Klib ABI Dump +// Targets: [iosArm64, iosSimulatorArm64, iosX64, js, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosSimulatorArm64, watchosX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +abstract interface arrow/AutoCloseScope { // arrow/AutoCloseScope|null[0] + abstract fun autoClose(kotlin/Function1) // arrow/AutoCloseScope.autoClose|autoClose(kotlin.Function1){}[0] + open fun <#A1: kotlin/Any?> autoClose(kotlin/Function0<#A1>, kotlin/Function2<#A1, kotlin/Throwable?, kotlin/Unit>): #A1 // arrow/AutoCloseScope.autoClose|autoClose(kotlin.Function0<0:0>;kotlin.Function2<0:0,kotlin.Throwable?,kotlin.Unit>){0§}[0] + open fun <#A1: kotlin/AutoCloseable> install(#A1): #A1 // arrow/AutoCloseScope.install|install(0:0){0§}[0] +} +abstract interface arrow/Finally : arrow/AutoCloseScope { // arrow/Finally|null[0] + abstract fun autoClose(kotlin/Function1) // arrow/Finally.autoClose|autoClose(kotlin.Function1){}[0] + abstract fun finalise(kotlin.coroutines/SuspendFunction1) // arrow/Finally.finalise|finalise(kotlin.coroutines.SuspendFunction1){}[0] +} +final class arrow/DefaultAutoCloseScope : arrow/AutoCloseScope { // arrow/DefaultAutoCloseScope|null[0] + constructor () // arrow/DefaultAutoCloseScope.|(){}[0] + final fun autoClose(kotlin/Function1) // arrow/DefaultAutoCloseScope.autoClose|autoClose(kotlin.Function1){}[0] + final fun close(kotlin/Throwable?): kotlin/Nothing? // arrow/DefaultAutoCloseScope.close|close(kotlin.Throwable?){}[0] +} +final fun (kotlin/Throwable).arrow/throwIfFatal(): kotlin/Throwable // arrow/throwIfFatal|throwIfFatal@kotlin.Throwable(){}[0] +final inline fun <#A: kotlin/Any?> arrow/autoCloseScope(kotlin/Function1): #A // arrow/autoCloseScope|autoCloseScope(kotlin.Function1){0§}[0] +final suspend fun (arrow/Finally).arrow/coroutineScope(kotlin.coroutines/CoroutineContext): kotlinx.coroutines/CoroutineScope // arrow/coroutineScope|coroutineScope@arrow.Finally(kotlin.coroutines.CoroutineContext){}[0] +final suspend fun <#A: kotlin/Any?> (arrow/Finally).arrow/saga(kotlin.coroutines/SuspendFunction0<#A>, kotlin.coroutines/SuspendFunction2<#A, kotlin/Throwable?, kotlin/Unit>): #A // arrow/saga|saga@arrow.Finally(kotlin.coroutines.SuspendFunction0<0:0>;kotlin.coroutines.SuspendFunction2<0:0,kotlin.Throwable?,kotlin.Unit>){0§}[0] +final suspend fun <#A: kotlin/Any?> arrow/finally(kotlin/Function1): #A // arrow/finally|finally(kotlin.Function1){0§}[0] diff --git a/arrow-libs/fx/arrow-fx-dsl/build.gradle.kts b/arrow-libs/fx/arrow-fx-dsl/build.gradle.kts new file mode 100644 index 00000000000..1271b3a5444 --- /dev/null +++ b/arrow-libs/fx/arrow-fx-dsl/build.gradle.kts @@ -0,0 +1,51 @@ +@file:Suppress("DSL_SCOPE_VIOLATION") + +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + + +plugins { + id(libs.plugins.kotlin.multiplatform.get().pluginId) + alias(libs.plugins.arrowGradleConfig.kotlin) + alias(libs.plugins.publish) + alias(libs.plugins.spotless) +} + +spotless { + kotlin { + ktlint().editorConfigOverride(mapOf("ktlint_standard_filename" to "disabled")) + } +} + +apply(from = property("ANIMALSNIFFER_MPP")) + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(libs.coroutines.core) + implementation(projects.arrowAtomic) + implementation(projects.arrowAutoclose) + } + } + commonTest { + dependencies { + implementation(libs.kotlin.test) + implementation(libs.kotest.assertionsCore) + implementation(libs.coroutines.test) + } + } + } + + jvm { + tasks.jvmJar { + manifest { + attributes["Automatic-Module-Name"] = "arrow.autocloseable" + } + } + } +} + +// enables context receivers for Jvm Tests +tasks.named("compileTestKotlinJvm") { + compilerOptions.freeCompilerArgs.add("-Xcontext-receivers") +} diff --git a/arrow-libs/fx/arrow-fx-dsl/gradle.properties b/arrow-libs/fx/arrow-fx-dsl/gradle.properties new file mode 100644 index 00000000000..cdbe5073d01 --- /dev/null +++ b/arrow-libs/fx/arrow-fx-dsl/gradle.properties @@ -0,0 +1,2 @@ +# Maven publishing configuration +POM_NAME=Arrow AutoCloseable diff --git a/arrow-libs/fx/arrow-fx-dsl/knit.code.include b/arrow-libs/fx/arrow-fx-dsl/knit.code.include new file mode 100644 index 00000000000..ad6f7cd7937 --- /dev/null +++ b/arrow-libs/fx/arrow-fx-dsl/knit.code.include @@ -0,0 +1,18 @@ +// This file was automatically generated from ${file.name} by Knit tool. Do not edit. +@file:OptIn(ExperimentalStdlibApi::class) +package ${knit.package}.${knit.name} + +import arrow.AutoCloseScope +import arrow.autoCloseScope +import arrow.install + +public class Scanner( + private val path: String, +) : AutoCloseable, Iterable by listOf("Hello", "World", "!") { + override fun close(): Unit = Unit +} + +public class Printer(private val path: String) : AutoCloseable { + public fun print(line: String): Unit = Unit + override fun close(): Unit = Unit +} diff --git a/arrow-libs/fx/arrow-fx-dsl/knit.properties b/arrow-libs/fx/arrow-fx-dsl/knit.properties new file mode 100644 index 00000000000..da6dcaafced --- /dev/null +++ b/arrow-libs/fx/arrow-fx-dsl/knit.properties @@ -0,0 +1,7 @@ +knit.package=arrow.autocloseable.examples +knit.dir=src/jvmTest/kotlin/examples/ + +test.package=arrow.autocloseable.examples.test +test.dir=src/jvmTest/kotlin/examples/autogenerated/ + +knit.include=knit.code.include \ No newline at end of file diff --git a/arrow-libs/fx/arrow-fx-dsl/src/commonMain/kotlin/arrow/await/AwaitAllScope.kt b/arrow-libs/fx/arrow-fx-dsl/src/commonMain/kotlin/arrow/await/AwaitAllScope.kt new file mode 100644 index 00000000000..f9b5fea6e8a --- /dev/null +++ b/arrow-libs/fx/arrow-fx-dsl/src/commonMain/kotlin/arrow/await/AwaitAllScope.kt @@ -0,0 +1,125 @@ +package arrow.await + +import arrow.atomic.Atomic +import arrow.atomic.update +import kotlinx.coroutines.async as coroutinesAsync +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.awaitAll as coroutinesAwaitAll +import kotlinx.coroutines.coroutineScope +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +public suspend fun awaitAll( + block: suspend AwaitAllScope.() -> A +): A = coroutineScope { block(AwaitAllScope(this)) } + +public suspend fun CoroutineScope.awaitAll( + block: suspend AwaitAllScope.() -> A +): A = block(AwaitAllScope(this)) + +/** + * Within an [AwaitAllScope], any call to [kotlinx.coroutines.Deferred.await] + * causes all the other [Deferred] in the same block to be awaited too. + * + * Why is this needed at all? Let's look at some examples: + * + * ```kotlin + * suspend fun loadUserInfo(id: UserId): UserInfo = awaitAll { + * val name = async { loadUserFromDb(id) } + * val avatar = async { loadAvatar(id) } + * UserInfo( + * name.await(), // <- at this point every 'async' is 'await'ed + * avatar.await() // <- so when you reach this 'await', the value is already there + * ) + * } + *``` + * Okay, great! But doesn't a `coroutineScope { }` do the same? Let's look at some examples! + * ```kotlin + * suspend fun loadUserFromDb(id: UserId): String = + * awaitCancellation() + * + * suspend fun loadAvatar(id: UserId): String = + * throw CancellationException("DataSource got closed!") + * ``` + * Our `loadUserFromDb` function hangs forever, stub this using `awaitCancellation()`. + * Which means it will only finish, when our scope is cancelled, or any sibling `Job` cancels us. + * Our `loadAvatar` functions cancels here, due to our `DataSource` getting closed for whatever reason. + * We stub this by throwing `CancellationException`, but let's see what happens. + * ```kotlin + * suspend fun loadUserInfo(id: UserId): UserInfo = coroutineScope { + * val name = async { loadUserFromDb(id) } + * val avatar = async { loadAvatar(id) } + * UserInfo( + * name.await(), // <- Waiting forever, awaitCancellation + * avatar.await() // <- `CancellationException` ignored until `await` is called. + * ) + * } + * ``` + * Our program hangs forever even though `CoroutineScope` saw our `CancellationException`. + * Strange, if any exception is used besides `CancellationException` it works as expected, + * if we call `awaitAll(name, avatar)` instead, it also works as expected, + * if we'd use `launch` it would also work as expected! + * So, only in this case where we use `async` for parallel operations it hangs forever. + * This comes with a **big** downside for Arrow Core's Raise, + * since it also relies on `CancellationException` for signalling early returning, or short-circuiting. + * + * However, if we use `awaitAll`, which is the DSL form of `awaitAll(name, avatar)` everything works as desired. + * + * ```kotlin + * suspend fun loadUserInfoWithoutAwait(id: UserId): UserInfo = awaitAll { + * val name = async { loadUserFromDb(id) } + * val avatar = async { loadAvatar(id) } + * return UserInfo( + * name.await(), // <-- Awaits both name, and avatar + * avatar.await() // <-- CancellationException cancels the awaitAll, and is rethrown. + * ) + * } + * ``` + * + * **DISCLAIMER:** + * Since `AwaitAllScope` is not part of KotlinX Coroutines, + * you need to be careful to call the correct `async` function. + * If you accidentally call `kotlinx.coroutines.async` instead, + * the original `coroutineScope { }` behavior is still respected. + * Luckily this should not pose an issue often, since async is a member of `AwaitAllScope`. + */ +public class AwaitAllScope( + private val scope: CoroutineScope +) : CoroutineScope by scope { + private val tasks: Atomic>> = Atomic(emptyList()) + + public fun async( + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend CoroutineScope.() -> T + ): Deferred { + val deferred = coroutinesAsync(context, start, block) + tasks.update { it + deferred } + return Await(deferred) + } + + // Allow nesting in a custom scope, + // whilst still adding it to this AwaitAllScope + // TODO: discuss if this is good idea, or not. + public fun CoroutineScope.async( + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend CoroutineScope.() -> T + ): Deferred { + val scope = this + val deferred = scope.coroutinesAsync(context, start, block) + tasks.update { it + deferred } + return Await(deferred) + } + + private inner class Await( + private val deferred: Deferred + ) : Deferred by deferred { + override suspend fun await(): T { + tasks.getAndSet(emptyList()).coroutinesAwaitAll() + return deferred.await() + } + } +} diff --git a/arrow-libs/fx/arrow-fx-dsl/src/commonMain/kotlin/arrow/scoped/CoroutineScope.kt b/arrow-libs/fx/arrow-fx-dsl/src/commonMain/kotlin/arrow/scoped/CoroutineScope.kt new file mode 100644 index 00000000000..9d7ee15ae51 --- /dev/null +++ b/arrow-libs/fx/arrow-fx-dsl/src/commonMain/kotlin/arrow/scoped/CoroutineScope.kt @@ -0,0 +1,58 @@ +package arrow.scoped + +import kotlinx.coroutines.* +import kotlin.coroutines.CoroutineContext + +/** + * Builds a [CoroutineScope] on top of the [ScopingScope] DSL, + * this gives the same guarantees as: + * ```kotlin + * coroutineScope { + * + * } + * ``` + * but in a flattened imperative way, some use-cases. + * ```kotlin + * context(Scope, AwaitScope) + * fun parallel() = + * (0..10) + * .map { coroutineScope() } + * .map { scope.async { crazy-ness() } } + * .map { it.await() } + * ``` + * + * ```kotlin + * context(Scope) + * fun kafkaConsumer(): KConsumer = + * KConsumer(coroutineScope(), settings) + * ``` + */ +public suspend fun ScopingScope.coroutineScope(): CoroutineScope = + scope(currentCoroutineContext()) { Job(it) } + +/** + * Unlike [coroutineScope] a failure in a child, + * will not result in the scope failing, nor cancelling any sibling jobs. + */ +public suspend fun ScopingScope.supervisorScope(): CoroutineScope = + scope(currentCoroutineContext()) { SupervisorJob(it) } + +private fun ScopingScope.scope( + context: CoroutineContext, + createJob: (parent: Job?) -> Job, +) : CoroutineScope { + val newJob = createJob(context[Job]) + val scope = CoroutineScope(context + newJob) + closing { e -> + when (e) { + null -> newJob.join() + else -> try { + newJob.cancelAndJoin() + } catch (e: CancellationException) { + newJob.cancel(e) + newJob.join() + } + } + } + return scope +} diff --git a/arrow-libs/fx/arrow-fx-dsl/src/commonMain/kotlin/arrow/scoped/Resource.kt b/arrow-libs/fx/arrow-fx-dsl/src/commonMain/kotlin/arrow/scoped/Resource.kt new file mode 100644 index 00000000000..605072dd354 --- /dev/null +++ b/arrow-libs/fx/arrow-fx-dsl/src/commonMain/kotlin/arrow/scoped/Resource.kt @@ -0,0 +1,12 @@ +package arrow.scoped + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.withContext + +public suspend fun ScopingScope.resource( + action: suspend CoroutineScope.() -> A, + compensation: suspend (A, Throwable?) -> Unit +): A = + withContext(NonCancellable, action) + .also { a -> closing { e -> compensation(a, e) } } diff --git a/arrow-libs/fx/arrow-fx-dsl/src/commonMain/kotlin/arrow/scoped/Saga.kt b/arrow-libs/fx/arrow-fx-dsl/src/commonMain/kotlin/arrow/scoped/Saga.kt new file mode 100644 index 00000000000..64b4a6c43e4 --- /dev/null +++ b/arrow-libs/fx/arrow-fx-dsl/src/commonMain/kotlin/arrow/scoped/Saga.kt @@ -0,0 +1,56 @@ +package arrow.scoped + +/** + * The saga design pattern is a way to manage data consistency across microservices in distributed + * transaction scenarios. A [ScopingScope] is useful when you need to manage data in a consistent manner + * across services in distributed transaction scenarios. Or when you need to compose multiple + * [action] with a [compensation] that needs to run in a transaction like style. + * + * For example, let's say that we have the following domain types `Order`, `Payment`. + * + * ```kotlin + * data class Order(val id: UUID, val amount: Long) + * data class Payment(val id: UUID, val orderId: UUID) + * ``` + * + * The creation of an `Order` can only remain when a payment has been made. In SQL, you might run + * this inside a transaction, which can automatically roll back the creation of the `Order` when the + * creation of the Payment fails. + * + * When you need to do this across distributed services, or a multiple atomic references, etc. You + * need to manually facilitate the rolling back of the performed actions, or compensating actions. + * + * The [saga] DSL removes all the boilerplate of manually having to facilitate this + * with a convenient suspending DSL. + * + * ```kotlin + * data class Order(val id: UUID, val amount: Long) + * suspend fun createOrder(): Order = Order(UUID.randomUUID(), 100L) + * suspend fun deleteOrder(order: Order): Unit = println("Deleting $order") + * + * data class Payment(val id: UUID, val orderId: UUID) + * suspend fun createPayment(order: Order): Payment = Payment(UUID.randomUUID(), order.id) + * suspend fun deletePayment(payment: Payment): Unit = println("Deleting $payment") + * + * suspend fun Payment.awaitSuccess(): Unit = throw RuntimeException("Payment Failed") + * + * suspend fun main() = scoped { + * val order = saga({ createOrder() }) { deleteOrder(it) } + * val payment = saga { createPayment(order) }, ::deletePayment) + * payment.awaitSuccess() + * } + * ``` + */ +// TODO should this be implemented on ScopingScope +// Or should ScopingScope be used to implement a separate SagaScope?? +public suspend fun ScopingScope.saga( + action: suspend () -> A, + compensation: suspend (A, Throwable?) -> Unit +): A = action().also { a -> + closing { e -> + when (e) { + null -> compensation(a, null) + else -> compensation(a, e) + } + } +} diff --git a/arrow-libs/fx/arrow-fx-dsl/src/commonMain/kotlin/arrow/scoped/ScopingScope.kt b/arrow-libs/fx/arrow-fx-dsl/src/commonMain/kotlin/arrow/scoped/ScopingScope.kt new file mode 100644 index 00000000000..1314337c59c --- /dev/null +++ b/arrow-libs/fx/arrow-fx-dsl/src/commonMain/kotlin/arrow/scoped/ScopingScope.kt @@ -0,0 +1,56 @@ +// Import throwIfFatal from AutoCloseScope +@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") + +package arrow.scoped + +import arrow.AutoCloseScope +import arrow.throwIfFatal +import arrow.atomic.Atomic +import arrow.atomic.update +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.withContext +import kotlin.coroutines.cancellation.CancellationException + +public interface ScopingScope : AutoCloseScope { + public override fun autoClose(close: (Throwable?) -> Unit): Unit + public fun closing(block: suspend (Throwable?) -> Unit): Unit +} + +public suspend fun scoped(block: suspend ScopingScope.() -> A): A { + val scope = DefaultScopingScope() + return try { + block(scope) + } catch (e: CancellationException) { + scope.close(e) ?: throw e + } catch (e: Throwable) { + scope.close(e.throwIfFatal()) ?: throw e + } +} + +private class DefaultScopingScope : ScopingScope { + private val closeables: Atomic Unit>> = Atomic(emptyList()) + + suspend fun close(error: Throwable?): Nothing? { + return withContext(NonCancellable) { + closeables.get().asReversed().fold(error) { acc, function -> + acc.add(runCatching { function(error) }.exceptionOrNull()) + }?.let { throw it } + } + } + + override fun closing(block: suspend (Throwable?) -> Unit): Unit = + closeables.update { + val closeLambda: suspend (Throwable?) -> Unit = + { e: Throwable? -> close(e) } + listOf(closeLambda) + it + } + + override fun autoClose(close: (Throwable?) -> Unit) = + closing(close) +} + +private fun Throwable?.add(other: Throwable?): Throwable? = + this?.apply { + other?.let { addSuppressed(it) } + } ?: other + diff --git a/arrow-libs/fx/arrow-fx-dsl/src/commonTest/kotlin/arrow/AutoCloseTest.kt b/arrow-libs/fx/arrow-fx-dsl/src/commonTest/kotlin/arrow/AutoCloseTest.kt new file mode 100644 index 00000000000..2dc8d63d192 --- /dev/null +++ b/arrow-libs/fx/arrow-fx-dsl/src/commonTest/kotlin/arrow/AutoCloseTest.kt @@ -0,0 +1,191 @@ +package arrow + +import arrow.atomic.AtomicBoolean +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.toList +import kotlinx.coroutines.test.runTest +import kotlin.coroutines.cancellation.CancellationException +import kotlin.test.Test + +@OptIn(ExperimentalStdlibApi::class) +class AutoCloseTest { + + @Test + fun canInstallResource() = runTest { + val promise = CompletableDeferred() + val wasActive = CompletableDeferred() + val res = Resource() + + autoCloseScope { + val r = autoClose({ res }) { r, e -> + promise.complete(e) + r.shutdown() + } + wasActive.complete(r.isActive()) + } + + promise.await() shouldBe null + wasActive.await() shouldBe true + res.isActive() shouldBe false + } + + @Test + fun canHandleWithFailingAutoClose() = runTest { + val promise = CompletableDeferred() + val wasActive = CompletableDeferred() + val error = RuntimeException("BOOM!") + val res = Resource() + + shouldThrow { + autoCloseScope { + val r = autoClose({ res }) { r, e -> + promise.complete(e) + r.shutdown() + } + wasActive.complete(r.isActive()) + throw error + } + } shouldBe error + + promise.await() shouldBe error + wasActive.await() shouldBe true + res.isActive() shouldBe false + } + + @Test + fun addsSuppressedErrors() = runTest { + val promise = CompletableDeferred() + val wasActive = CompletableDeferred() + val error = RuntimeException("BOOM!") + val error2 = RuntimeException("BOOM 2!") + val error3 = RuntimeException("BOOM 3!") + val res = Resource() + + val e = shouldThrow { + autoCloseScope { + val r = autoClose({ res }) { r, e -> + promise.complete(e) + r.shutdown() + throw error2 + } + autoClose({ Resource() }) { _, _ -> throw error3 } + wasActive.complete(r.isActive()) + throw error + } + } + + e shouldBe error + e.suppressedExceptions shouldBe listOf(error3, error2) + promise.await() shouldBe error + wasActive.await() shouldBe true + res.isActive() shouldBe false + } + + @Test + fun handlesAcquireFailure() = runTest { + val promise = CompletableDeferred() + val error = RuntimeException("BOOM!") + val error2 = RuntimeException("BOOM 2!") + + val e = shouldThrow { + autoCloseScope { + autoClose({ Resource() }) { r, e -> + promise.complete(e) + r.shutdown() + throw error2 + } + autoClose({ throw error }) { _, _ -> } + } + } + e shouldBe error + e.suppressedExceptions shouldBe listOf(error2) + promise.await() shouldBe error + } + + @Test + fun canInstallAutoCloseable() = runTest { + val wasActive = CompletableDeferred() + val res = Resource() + + autoCloseScope { + val r = install(res) + wasActive.complete(r.isActive()) + } + + wasActive.await() shouldBe true + res.isActive() shouldBe false + } + + @Test + fun closeTheAutoScopeOnCancellation() = runTest { + val wasActive = CompletableDeferred() + val res = Resource() + + shouldThrow { + autoCloseScope { + val r = install(res) + wasActive.complete(r.isActive()) + throw CancellationException("BOOM!") + } + }.message shouldBe "BOOM!" + + wasActive.await() shouldBe true + res.isActive() shouldBe false + } + + @Test + fun closeInReversedOrder() = runTest { + val res1 = Resource() + val res2 = Resource() + val res3 = Resource() + + val wasActive = Channel(Channel.UNLIMITED) + val closed = Channel(Channel.UNLIMITED) + + autoCloseScope { + val r1 = autoClose({ res1 }) { r, _ -> + closed.trySend(r).getOrThrow() + r.shutdown() + } + val r2 = autoClose({ res2 }) { r, _ -> + closed.trySend(r).getOrThrow() + r.shutdown() + } + val r3 = autoClose({ res3 }) { r, _ -> + closed.trySend(r).getOrThrow() + r.shutdown() + } + + wasActive.trySend(r1.isActive()).getOrThrow() + wasActive.trySend(r2.isActive()).getOrThrow() + wasActive.trySend(r3.isActive()).getOrThrow() + wasActive.close() + } + + wasActive.toList() shouldBe listOf(true, true, true) + closed.receive() shouldBe res3 + closed.receive() shouldBe res2 + closed.receive() shouldBe res1 + closed.cancel() + } + + @OptIn(ExperimentalStdlibApi::class) + private class Resource : AutoCloseable { + private val isActive = AtomicBoolean(true) + + fun isActive(): Boolean = isActive.get() + + fun shutdown() { + require(isActive.compareAndSet(expected = true, new = false)) { + "Already shut down" + } + } + + override fun close() { + shutdown() + } + } +} diff --git a/arrow-libs/fx/arrow-fx-dsl/src/jvmTest/kotlin/arrow/ScopeAwaitAll.kt b/arrow-libs/fx/arrow-fx-dsl/src/jvmTest/kotlin/arrow/ScopeAwaitAll.kt new file mode 100644 index 00000000000..2ff22419bb8 --- /dev/null +++ b/arrow-libs/fx/arrow-fx-dsl/src/jvmTest/kotlin/arrow/ScopeAwaitAll.kt @@ -0,0 +1,43 @@ +package arrow + +import arrow.await.AwaitAllScope +import arrow.await.awaitAll +import arrow.scoped.ScopingScope +import arrow.scoped.coroutineScope +import arrow.scoped.scoped +import kotlinx.coroutines.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.fail + +class ScopeAwaitAll { + @Test + fun customScopeAwaitAll() = runTest { + val ex = CancellationException("Boom!") + scoped { + awaitAll { + try { + parallel(ex) + fail("Should not finish success") + } catch (e: CancellationException) { + assertEquals(ex, e) + } catch (e: Throwable) { + fail("Cannot reach this place") + } + } + } + } +} + +context(ScopingScope, AwaitAllScope) +suspend fun parallel(ex: Throwable): List { + val scope = coroutineScope() + return (0..10) + .map { index -> + if (index == 10) scope.async { + println("index: $index") + delay(100); throw ex + } else scope.async { println(index); awaitCancellation() } + }.map { it.await() } +} diff --git a/arrow-libs/optics/arrow-optics-compose/build.gradle.kts b/arrow-libs/optics/arrow-optics-compose/build.gradle.kts index b400c9d5be6..a382b493bd2 100644 --- a/arrow-libs/optics/arrow-optics-compose/build.gradle.kts +++ b/arrow-libs/optics/arrow-optics-compose/build.gradle.kts @@ -8,7 +8,6 @@ repositories { plugins { id(libs.plugins.kotlin.multiplatform.get().pluginId) - // alias(libs.plugins.arrowGradleConfig.kotlin) alias(libs.plugins.publish) alias(libs.plugins.spotless) alias(libs.plugins.compose.jetbrains) diff --git a/arrow-libs/resilience/arrow-resilience/api/arrow-resilience.api b/arrow-libs/resilience/arrow-resilience/api/arrow-resilience.api index 52e034ccf6a..5f19dc72aba 100644 --- a/arrow-libs/resilience/arrow-resilience/api/arrow-resilience.api +++ b/arrow-libs/resilience/arrow-resilience/api/arrow-resilience.api @@ -119,6 +119,7 @@ public abstract interface annotation class arrow/resilience/SagaDSLMarker : java public final class arrow/resilience/SagaKt { public static final fun saga (Lkotlin/jvm/functions/Function2;)Lkotlin/jvm/functions/Function2; public static final fun saga (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lkotlin/jvm/functions/Function2; + public static final fun sagaScope (Lkotlin/jvm/functions/Function2;)Lkotlin/jvm/functions/Function2; public static final fun transact (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } diff --git a/arrow-libs/resilience/arrow-resilience/api/arrow-resilience.klib.api b/arrow-libs/resilience/arrow-resilience/api/arrow-resilience.klib.api index fbb4a9aec97..ac84f00feaa 100644 --- a/arrow-libs/resilience/arrow-resilience/api/arrow-resilience.klib.api +++ b/arrow-libs/resilience/arrow-resilience/api/arrow-resilience.klib.api @@ -124,6 +124,7 @@ final enum class arrow.resilience.common/Platform : kotlin/Enum (kotlinx.coroutines.flow/Flow<#A>).arrow.resilience/retry(arrow.resilience/Schedule): kotlinx.coroutines.flow/Flow<#A> // arrow.resilience/retry|retry@kotlinx.coroutines.flow.Flow<0:0>(arrow.resilience.Schedule){0§;1§}[0] final fun <#A: kotlin/Any?> arrow.resilience/saga(kotlin.coroutines/SuspendFunction1, kotlin.coroutines/SuspendFunction1<#A, kotlin/Unit>): kotlin.coroutines/SuspendFunction1 // arrow.resilience/saga|saga(kotlin.coroutines.SuspendFunction1;kotlin.coroutines.SuspendFunction1<0:0,kotlin.Unit>){0§}[0] final inline fun <#A: kotlin/Any?> arrow.resilience/saga(noinline kotlin.coroutines/SuspendFunction1): kotlin.coroutines/SuspendFunction1 // arrow.resilience/saga|saga(kotlin.coroutines.SuspendFunction1){0§}[0] +final inline fun <#A: kotlin/Any?> arrow.resilience/sagaScope(noinline kotlin.coroutines/SuspendFunction1): kotlin.coroutines/SuspendFunction1 // arrow.resilience/sagaScope|sagaScope(kotlin.coroutines.SuspendFunction1){0§}[0] final object arrow.resilience/SagaActionStep // arrow.resilience/SagaActionStep|null[0] final suspend fun <#A: kotlin/Any?> (kotlin.coroutines/SuspendFunction1).arrow.resilience/transact(): #A // arrow.resilience/transact|transact@kotlin.coroutines.SuspendFunction1(){0§}[0] final suspend fun <#A: kotlin/Throwable, #B: kotlin/Any?, #C: kotlin/Any?, #D: kotlin/Any?> (arrow.resilience/Schedule<#A, #C>).arrow.resilience/retryOrElseEither(kotlin.reflect/KClass<#A>, kotlin.coroutines/SuspendFunction0<#B>, kotlin.coroutines/SuspendFunction2<#A, #C, #D>): arrow.core/Either<#D, #B> // arrow.resilience/retryOrElseEither|retryOrElseEither@arrow.resilience.Schedule<0:0,0:2>(kotlin.reflect.KClass<0:0>;kotlin.coroutines.SuspendFunction0<0:1>;kotlin.coroutines.SuspendFunction2<0:0,0:2,0:3>){0§;1§;2§;3§}[0] diff --git a/arrow-libs/resilience/arrow-resilience/src/commonMain/kotlin/arrow/resilience/Saga.kt b/arrow-libs/resilience/arrow-resilience/src/commonMain/kotlin/arrow/resilience/Saga.kt index c38831bdd0a..19c450d94e5 100644 --- a/arrow-libs/resilience/arrow-resilience/src/commonMain/kotlin/arrow/resilience/Saga.kt +++ b/arrow-libs/resilience/arrow-resilience/src/commonMain/kotlin/arrow/resilience/Saga.kt @@ -84,14 +84,19 @@ public interface SagaScope { * By doing so we can guarantee that any transactional like operations made by the [Saga] will * guarantee that it results in the correct state. */ -@Suppress("NOTHING_TO_INLINE") +public inline fun sagaScope(noinline block: suspend SagaScope.() -> A): Saga = block + +@Deprecated( + "saga DSL builder is deprecated, use sagaScope instead.", + ReplaceWith("sagaScope(block)", "arrow.resilience.sagaScope") +) public inline fun saga(noinline block: suspend SagaScope.() -> A): Saga = block /** Create a lazy [Saga] that will only run when the [Saga] is invoked. */ public fun saga( action: suspend SagaActionStep.() -> A, compensation: suspend (A) -> Unit -): Saga = saga { saga(action, compensation) } +): Saga = sagaScope { saga(action, compensation) } /** * Transact runs the [Saga] turning it into a [suspend] effect that results in [A]. If the saga @@ -109,7 +114,8 @@ public suspend fun Saga.transact(): A { } /** DSL Marker for the SagaEffect DSL */ -@DslMarker public annotation class SagaDSLMarker +@DslMarker +public annotation class SagaDSLMarker /** * Marker object to protect [SagaScope.saga] from calling [SagaScope.bind] in its `action` step. diff --git a/arrow-libs/resilience/arrow-resilience/src/commonTest/kotlin/arrow/resilience/SagaSpec.kt b/arrow-libs/resilience/arrow-resilience/src/commonTest/kotlin/arrow/resilience/SagaSpec.kt index b6ae2ae121d..554c8d73cea 100644 --- a/arrow-libs/resilience/arrow-resilience/src/commonTest/kotlin/arrow/resilience/SagaSpec.kt +++ b/arrow-libs/resilience/arrow-resilience/src/commonTest/kotlin/arrow/resilience/SagaSpec.kt @@ -30,7 +30,7 @@ class SagaSpec { fun sagaRunsCompensationIfThrowInBuilderAndRethrowsException(): TestResult = runTest { val value = Random.nextInt() val compensation = CompletableDeferred() - val saga = saga { + val saga = sagaScope { saga({ value }) { compensation.complete(it) } throw SagaFailed("Exception in builder") } @@ -42,7 +42,7 @@ class SagaSpec { fun sagaRunsCompensationIfThrowInSagaAndRethrowsException(): TestResult = runTest { val value = Random.nextInt() val compensation = CompletableDeferred() - val saga = saga { + val saga = sagaScope { saga({ value }) { compensation.complete(it) } saga({ throw SagaFailed("Exception in saga") }) { fail("Doesn't run") } } @@ -56,7 +56,7 @@ class SagaSpec { val valueB = Random.nextInt() val compensations = Channel(2) - val saga = saga { + val saga = sagaScope { saga({ valueA }) { compensations.send(it) } saga({ valueB }) { compensations.send(it) } saga({ throw SagaFailed("Exception in saga") }) { fail("Doesn't run") } @@ -73,7 +73,7 @@ class SagaSpec { val compensationA = CompletableDeferred() val original = SagaFailed("Exception in saga") val compensation = SagaFailed("Exception in compensation") - val saga = saga { + val saga = sagaScope { saga({ value }) { compensationA.complete(it) } saga({}) { throw compensation } saga({ throw original }) { fail("Doesn't run") } @@ -92,7 +92,7 @@ class SagaSpec { val original = SagaFailed("Exception in builder") val compensation = SagaFailed("Exception in compensation") - val saga = saga { + val saga = sagaScope { saga({ value }) { compensationA.complete(it) } saga({}) { throw compensation } throw original @@ -111,7 +111,7 @@ class SagaSpec { val valueC = Random.nextInt() val values = listOf(valueA, valueB, valueC) - val result = saga { values.map { saga({ it }) { fail("Doesn't run") } } }.transact() + val result = sagaScope { values.map { saga({ it }) { fail("Doesn't run") } } }.transact() assertEquals(values, result) } @@ -122,7 +122,7 @@ class SagaSpec { val valueC = Random.nextInt() val values = listOf(valueA, valueB, valueC) - val result = saga { values.parMap(Dispatchers.Default) { saga({ it }) { fail("Doesn't run") } } }.transact() + val result = sagaScope { values.parMap(Dispatchers.Default) { saga({ it }) { fail("Doesn't run") } } }.transact() assertEquals(values, result) } @@ -132,7 +132,7 @@ class SagaSpec { val compensationA = CompletableDeferred() val latch = CompletableDeferred() - val saga = saga { + val saga = sagaScope { parZip({ saga({ latch.complete(Unit) @@ -156,7 +156,7 @@ class SagaSpec { val compensationB = CompletableDeferred() val latch = CompletableDeferred() - val saga = saga { + val saga = sagaScope { parZip({ saga({ latch.await() diff --git a/build.gradle.kts b/build.gradle.kts index 91afa0bf4f6..6df3685b9bb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -75,7 +75,7 @@ configure { dependencies { kover(projects.arrowAtomic) - kover(projects.arrowAutoclose) + kover(projects.arrowFxDsl) kover(projects.arrowCore) kover(projects.arrowCoreHighArity) kover(projects.arrowCoreRetrofit) diff --git a/settings.gradle.kts b/settings.gradle.kts index 6a8e95f1c73..c8b2f27132c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -83,6 +83,9 @@ include("arrow-autoclose") project(":arrow-autoclose").projectDir = file("arrow-libs/core/arrow-autoclose") // FX +include("arrow-fx-dsl") +project(":arrow-fx-dsl").projectDir = file("arrow-libs/fx/arrow-fx-dsl") + include("arrow-fx-coroutines") project(":arrow-fx-coroutines").projectDir = file("arrow-libs/fx/arrow-fx-coroutines")