From 7a672d4d0f4f378609be2c304c1470377c7bc970 Mon Sep 17 00:00:00 2001 From: Nikolay Kochetkov Date: Fri, 29 Sep 2023 12:00:55 +0200 Subject: [PATCH] Bypassing cancellation errors Refs: #48 --- .../coroutines/CacheThenNetLceModel.kt | 3 ++- .../rxlcemodel/coroutines/UpdateWrapper.kt | 5 +++-- .../rxlcemodel/coroutines/coroutines.kt | 15 +++++++++++++++ .../motorro/rxlcemodel/coroutines/lceUtils.kt | 5 ++--- .../rxlcemodel/coroutines/LceUtilsKtTest.kt | 19 +++++++++++++++++++ 5 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 coroutines/src/commonMain/kotlin/com/motorro/rxlcemodel/coroutines/coroutines.kt diff --git a/coroutines/src/commonMain/kotlin/com/motorro/rxlcemodel/coroutines/CacheThenNetLceModel.kt b/coroutines/src/commonMain/kotlin/com/motorro/rxlcemodel/coroutines/CacheThenNetLceModel.kt index cdc68e29..1adaf8c0 100644 --- a/coroutines/src/commonMain/kotlin/com/motorro/rxlcemodel/coroutines/CacheThenNetLceModel.kt +++ b/coroutines/src/commonMain/kotlin/com/motorro/rxlcemodel/coroutines/CacheThenNetLceModel.kt @@ -21,6 +21,7 @@ import com.motorro.rxlcemodel.common.Logger import com.motorro.rxlcemodel.coroutines.service.ServiceSet import com.motorro.rxlcemodel.lce.LceState import com.motorro.rxlcemodel.lce.LceState.* +import coroutinesRunCatching import kotlinx.coroutines.* import kotlinx.coroutines.flow.* @@ -104,7 +105,7 @@ class CacheThenNetLceModel( type = if (null == data) Loading.Type.LOADING else Loading.Type.REFRESHING ) ) - runCatching { loadAndCacheNetwork() }.onFailure { error -> + coroutinesRunCatching { loadAndCacheNetwork() }.onFailure { error -> if (error !is CancellationException) { withLogger { modelLog(WARNING, "Error getting data from network - ERROR: $error") diff --git a/coroutines/src/commonMain/kotlin/com/motorro/rxlcemodel/coroutines/UpdateWrapper.kt b/coroutines/src/commonMain/kotlin/com/motorro/rxlcemodel/coroutines/UpdateWrapper.kt index 35864127..ba3bb1b2 100644 --- a/coroutines/src/commonMain/kotlin/com/motorro/rxlcemodel/coroutines/UpdateWrapper.kt +++ b/coroutines/src/commonMain/kotlin/com/motorro/rxlcemodel/coroutines/UpdateWrapper.kt @@ -22,6 +22,7 @@ import com.motorro.rxlcemodel.common.UpdateOperationState import com.motorro.rxlcemodel.common.UpdateOperationState.* import com.motorro.rxlcemodel.coroutines.service.CacheService import com.motorro.rxlcemodel.lce.LceState +import coroutinesRunCatching import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* @@ -55,7 +56,7 @@ abstract class UpdateWrapper( */ protected suspend fun doUpdate(dataSource: suspend (params: PARAMS) -> Entity) { networkOperationState.emit(LOADING) - try { + coroutinesRunCatching { withContext(ioDispatcher) { withLogger { modelLog(INFO, "Subscribing network...") } val data = dataSource(params) @@ -63,7 +64,7 @@ abstract class UpdateWrapper( cacheService.save(params, data) } networkOperationState.emit(IDLE) - } catch (error: Throwable) { + }.onFailure { error -> withLogger { modelLog(WARNING, "Network error: $error") } networkOperationState.emit(ERROR(error)) } diff --git a/coroutines/src/commonMain/kotlin/com/motorro/rxlcemodel/coroutines/coroutines.kt b/coroutines/src/commonMain/kotlin/com/motorro/rxlcemodel/coroutines/coroutines.kt new file mode 100644 index 00000000..f39bcd96 --- /dev/null +++ b/coroutines/src/commonMain/kotlin/com/motorro/rxlcemodel/coroutines/coroutines.kt @@ -0,0 +1,15 @@ +import kotlinx.coroutines.CancellationException + +/** + * Calls the specified function [block] with `this` value as its receiver and returns its encapsulated result if invocation was successful, + * catching any [Throwable] exception besides [CancellationException] that was thrown from the [block] function execution and encapsulating it as a failure. + */ +public inline fun T.coroutinesRunCatching(block: T.() -> R): Result { + return try { + Result.success(block()) + } catch (c: CancellationException) { + throw c + } catch (e: Throwable) { + Result.failure(e) + } +} diff --git a/coroutines/src/commonMain/kotlin/com/motorro/rxlcemodel/coroutines/lceUtils.kt b/coroutines/src/commonMain/kotlin/com/motorro/rxlcemodel/coroutines/lceUtils.kt index f2fc8bf9..d6c0232b 100644 --- a/coroutines/src/commonMain/kotlin/com/motorro/rxlcemodel/coroutines/lceUtils.kt +++ b/coroutines/src/commonMain/kotlin/com/motorro/rxlcemodel/coroutines/lceUtils.kt @@ -16,6 +16,7 @@ package com.motorro.rxlcemodel.coroutines import com.motorro.rxlcemodel.lce.LceState import com.motorro.rxlcemodel.lce.combine import com.motorro.rxlcemodel.lce.map +import coroutinesRunCatching import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* @@ -117,9 +118,7 @@ fun Flow>.flatMapSingleData(mapper: suspend fun stateMapper(data1: DATA_1?): LceState = when(data1) { null -> LceState.Loading(null, false) - else -> try { - LceState.Content(mapper(data1), true) - } catch (e: Throwable) { + else -> coroutinesRunCatching { LceState.Content(mapper(data1), true) }.getOrElse { e -> LceState.Error(null, false, e) } } diff --git a/coroutines/src/commonTest/kotlin/com/motorro/rxlcemodel/coroutines/LceUtilsKtTest.kt b/coroutines/src/commonTest/kotlin/com/motorro/rxlcemodel/coroutines/LceUtilsKtTest.kt index 00fff491..e88540e8 100644 --- a/coroutines/src/commonTest/kotlin/com/motorro/rxlcemodel/coroutines/LceUtilsKtTest.kt +++ b/coroutines/src/commonTest/kotlin/com/motorro/rxlcemodel/coroutines/LceUtilsKtTest.kt @@ -14,6 +14,7 @@ package com.motorro.rxlcemodel.coroutines import com.motorro.rxlcemodel.lce.LceState +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -22,6 +23,7 @@ import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertTrue @OptIn(ExperimentalCoroutinesApi::class) class LceUtilsKtTest { @@ -247,6 +249,23 @@ class LceUtilsKtTest { ) } + @Test + fun singleDataMapperBypassesCancellationExceptions() = runTest { + val error1 = Exception("error 1") + val error2 = CancellationException("Cancelled") + + @Suppress("UNUSED_PARAMETER") + fun mapper(input: Int): String = throw error2 + + val source = flowOf( + LceState.Loading(null, false), + LceState.Content(2, true) + ) + + val result = source.flatMapSingleData(::mapper).toList() + assertEquals(1, result.size) + } + @Test fun mapsUseCaseData() = runTest { val error = Exception()