From 918ab4613f6f9e25ba14f8a1629ee601adc6cca0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 12 Mar 2025 20:44:27 +0400 Subject: [PATCH] Updated on 2026-08-14 --- domain/core/build.gradle.kts | 5 + .../domain/core/flow/FlowCachingSupplier.kt | 59 ++++++++ .../tangem/domain/core/flow/FlowFetcher.kt | 15 ++ .../tangem/domain/core/flow/FlowProducer.kt | 39 +++++ .../tangem/domain/core/flow/FlowSupplier.kt | 17 +++ .../core/flow/FlowCachingSupplierTest.kt | 139 ++++++++++++++++++ 6 files changed, 274 insertions(+) create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowCachingSupplier.kt create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowFetcher.kt create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowProducer.kt create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowSupplier.kt create mode 100644 domain/core/src/test/kotlin/com/tangem/domain/core/flow/FlowCachingSupplierTest.kt diff --git a/domain/core/build.gradle.kts b/domain/core/build.gradle.kts index 46d2670d68..9910d828a3 100644 --- a/domain/core/build.gradle.kts +++ b/domain/core/build.gradle.kts @@ -10,4 +10,9 @@ dependencies { api(deps.arrow.fx) implementation(deps.kotlin.serialization) + + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowCachingSupplier.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowCachingSupplier.kt new file mode 100644 index 0000000000..2a86b81acf --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowCachingSupplier.kt @@ -0,0 +1,59 @@ +package com.tangem.domain.core.flow + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.update + +private typealias FlowsStore = MutableStateFlow>> + +/** + * [Flow] supplier with caching mechanism + * + * @param Producer producer of flow [Flow] + * @param Params type of data that required to get [Flow] + * @param Data data type of [Flow] + * @property flowsStore store of flows + */ +abstract class FlowCachingSupplier, Params : Any, Data : Any>( + private val flowsStore: FlowsStore = MutableStateFlow(value = emptyMap()), +) : FlowSupplier { + + /** Factory of [FlowProducer] */ + abstract val factory: FlowProducer.Factory + + /** Key creator */ + abstract val keyCreator: (Params) -> String + + /** + * Supply [Flow] by [params]. + + */ + override operator fun invoke(params: Params): Flow { + val key = keyCreator(params) + val saved = flowsStore.value[key] + + if (saved != null) return saved + + val flowProducer = factory.create(params = params) + + return runCatching(flowProducer::produceWithFallback) + .onSuccess { flow -> + flowsStore.update { + it.toMutableMap().apply { + put(key = key, value = flow) + } + } + } + .getOrThrow() + .catch { cause -> + flowsStore.update { + it.toMutableMap().apply { + remove(key) + } + } + + throw cause + } + } +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowFetcher.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowFetcher.kt new file mode 100644 index 0000000000..3fcba3d9d4 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowFetcher.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.core.flow + +import arrow.core.Either + +/** + * Flow fetcher + * + * @param Params data that required to fetch flow + * +[REDACTED_AUTHOR] + */ +interface FlowFetcher { + + suspend operator fun invoke(params: Params): Either +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowProducer.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowProducer.kt new file mode 100644 index 0000000000..92e95f552d --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowProducer.kt @@ -0,0 +1,39 @@ +package com.tangem.domain.core.flow + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.retryWhen + +/** + * [Flow] producer + * + * @param Data data type of [Flow] + * +[REDACTED_AUTHOR] + */ +interface FlowProducer { + + /** Fallback value if [Flow] throws exception */ + val fallback: Data + + /** Produce [Flow] */ + fun produce(): Flow + + /** Produce [Flow] with retry mechanism */ + fun produceWithFallback(): Flow { + return produce().retryWhen { _, _ -> + emit(value = fallback) + + delay(timeMillis = 2000) + + true + } + } + + /** Factory for creating [Producer]. It helps to provide [Params] by constructor */ + interface Factory> { + + /** Create [Producer] using [params] */ + fun create(params: Params): Producer + } +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowSupplier.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowSupplier.kt new file mode 100644 index 0000000000..8de04d12bd --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowSupplier.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.core.flow + +import kotlinx.coroutines.flow.Flow + +/** + * [Flow] supplier + * + * @param Params type of data that required to get [Flow] + * @param Data data type of [Flow] + * +[REDACTED_AUTHOR] + */ +interface FlowSupplier { + + /** Supply [Flow] by [params] */ + operator fun invoke(params: Params): Flow +} \ No newline at end of file diff --git a/domain/core/src/test/kotlin/com/tangem/domain/core/flow/FlowCachingSupplierTest.kt b/domain/core/src/test/kotlin/com/tangem/domain/core/flow/FlowCachingSupplierTest.kt new file mode 100644 index 0000000000..42822e7796 --- /dev/null +++ b/domain/core/src/test/kotlin/com/tangem/domain/core/flow/FlowCachingSupplierTest.kt @@ -0,0 +1,139 @@ +package com.tangem.domain.core.flow + +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class FlowCachingSupplierTest { + + private val factory = mockk() + private val errorFactory = mockk() + + @Test + fun `flow was created before`() = runTest { + val flowsStore = MutableStateFlow>>(value = emptyMap()) + val supplier = MockFlowCachingSupplier(factory = factory, flowsStore = flowsStore) + + val params = 1 + + every { factory.create(params = params) } returns MockFlowProducer(params = params) + + val actual = supplier(params = params) + + verify { factory.create(params = params) } + + Truth.assertThat(actual.first()).isEqualTo("test_1") + + Truth.assertThat(flowsStore.value.size).isEqualTo(1) + Truth.assertThat(flowsStore.value.containsKey("mock_1")).isTrue() + Truth.assertThat(flowsStore.value["mock_1"]!!.first()).isEqualTo("test_1") + } + + @Test + fun `flow wasn't created before`() = runTest { + val storedFlow = flowOf("test_1") + val storedMap = mapOf("mock_1" to storedFlow) + val flowsStore = MutableStateFlow(value = storedMap) + + val supplier = MockFlowCachingSupplier(factory = factory, flowsStore = flowsStore) + + val params = 1 + + val actual = supplier(params = params) + + verify(inverse = true) { factory.create(params = params) } + + Truth.assertThat(actual).isEqualTo(storedFlow) + Truth.assertThat(flowsStore.value).isEqualTo(storedMap) + } + + @Test + fun `flow wasn't created before and store isn't empty`() = runTest { + val flowsStore = MutableStateFlow( + value = mapOf("mock_1" to flowOf("test_1")), + ) + + val supplier = MockFlowCachingSupplier(factory = factory, flowsStore = flowsStore) + + val params = 2 + + every { factory.create(params = params) } returns MockFlowProducer(params = params) + + val actual = supplier(params = params) + + verify { factory.create(params = params) } + + Truth.assertThat(actual.first()).isEqualTo("test_2") + + Truth.assertThat(flowsStore.value.size).isEqualTo(2) + Truth.assertThat(flowsStore.value.keys).isEqualTo(setOf("mock_1", "mock_2")) + Truth.assertThat(flowsStore.value["mock_1"]!!.first()).isEqualTo("test_1") + Truth.assertThat(flowsStore.value["mock_2"]!!.first()).isEqualTo("test_2") + } + + @Test + fun `flow throws exception`() = runTest { + val flowsStore = MutableStateFlow>>(value = emptyMap()) + val supplier = MockErrorFlowCachingSupplier(factory = errorFactory, flowsStore = flowsStore) + + val params = 1 + + every { errorFactory.create(params = params) } returns MockErrorFlowProducer() + + val actual = supplier(params = params) + + verify { errorFactory.create(params = params) } + + Truth.assertThat(actual.first()).isEqualTo("fallback") + Truth.assertThat(flowsStore.value.size).isEqualTo(1) + } + + private class MockFlowCachingSupplier( + override val factory: FlowProducer.Factory, + flowsStore: MutableStateFlow>>, + ) : FlowCachingSupplier(flowsStore = flowsStore) { + + override val keyCreator: (Int) -> String = { "mock_$it" } + } + + private class MockFlowProducer(private val params: Int) : FlowProducer { + + override val fallback: String + get() = "fallback" + + override fun produce(): Flow = flowOf("test_$params") + + class Factory : FlowProducer.Factory { + override fun create(params: Int): MockFlowProducer = MockFlowProducer(params = params) + } + } + + private class MockErrorFlowCachingSupplier( + override val factory: FlowProducer.Factory, + flowsStore: MutableStateFlow>>, + ) : FlowCachingSupplier(flowsStore = flowsStore) { + + override val keyCreator: (Int) -> String = { "mock_$it" } + } + + private class MockErrorFlowProducer : FlowProducer { + + override val fallback: String + get() = "fallback" + + override fun produce(): Flow = flow { + throw IllegalStateException() + } + + class Factory : FlowProducer.Factory { + override fun create(params: Int): MockErrorFlowProducer = MockErrorFlowProducer() + } + } +} \ No newline at end of file