From ca686fb6b78b42ac4132a9e0f2e2ab45e241f569 Mon Sep 17 00:00:00 2001 From: Dmitriy Berdnikov Date: Fri, 19 Apr 2024 16:27:37 +0300 Subject: [PATCH] handle incoming event sequentially by limiting dispatcher --- .../vivid/elmslie/core/store/ElmStore.kt | 66 +++++++++---------- .../vivid/elmslie/core/store/ElmStoreTest.kt | 12 ++-- 2 files changed, 37 insertions(+), 41 deletions(-) diff --git a/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/store/ElmStore.kt b/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/store/ElmStore.kt index 5893a6ad..77bf164c 100644 --- a/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/store/ElmStore.kt +++ b/elmslie-core/src/commonMain/kotlin/money/vivid/elmslie/core/store/ElmStore.kt @@ -1,6 +1,8 @@ package money.vivid.elmslie.core.store import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -13,12 +15,11 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import money.vivid.elmslie.core.ElmScope import money.vivid.elmslie.core.config.ElmslieConfig @Suppress("TooGenericExceptionCaught") +@OptIn(ExperimentalCoroutinesApi::class) class ElmStore( initialState: State, private val reducer: StateReducer, @@ -26,14 +27,14 @@ class ElmStore( storeListeners: Set>? = null, override val startEvent: Event? = null, private val key: String = - (reducer::class.qualifiedName?: reducer::class.simpleName).orEmpty().replace( + (reducer::class.qualifiedName ?: reducer::class.simpleName).orEmpty().replace( "Reducer", "Store", ), ) : Store { private val logger = ElmslieConfig.logger - private val eventMutex = Mutex() + private val eventDispatcher = ElmslieConfig.ioDispatchers.limitedParallelism(parallelism = 1) private val effectsFlow = MutableSharedFlow() @@ -51,7 +52,9 @@ class ElmStore( override val effects: Flow = effectsFlow.asSharedFlow() - override fun accept(event: Event) = dispatchEvent(event) + override fun accept(event: Event) { + scope.handleEvent(event) + } override fun start(): Store { startEvent?.let(::accept) @@ -62,37 +65,30 @@ class ElmStore( scope.cancel() } - private fun dispatchEvent(event: Event) { - scope.launch { - try { - storeListeners.forEach { it.onBeforeEvent(key, event, statesFlow.value) } - logger.debug( - message = "New event: $event", - tag = key, - ) - val (_, effects, commands) = - eventMutex.withLock { - val oldState = statesFlow.value - val result = reducer.reduce(event, statesFlow.value) - val newState = result.state - statesFlow.value = newState - storeListeners.forEach { - it.onAfterEvent(key, newState, oldState, event) - } - result - } - effects.forEach { effect -> if (isActive) dispatchEffect(effect) } - commands.forEach { if (isActive) executeCommand(it) } - } catch (error: CancellationException) { - throw error - } catch (t: Throwable) { - storeListeners.forEach { it.onReducerError(key, t, event) } - logger.fatal( - message = "You must handle all errors inside reducer", - tag = key, - error = t, - ) + private fun CoroutineScope.handleEvent(event: Event) = launch(eventDispatcher) { + try { + storeListeners.forEach { it.onBeforeEvent(key, event, statesFlow.value) } + logger.debug( + message = "New event: $event", + tag = key, + ) + val oldState = statesFlow.value + val (state, effects, commands) = reducer.reduce(event, statesFlow.value) + statesFlow.value = state + storeListeners.forEach { + it.onAfterEvent(key, state, oldState, event) } + effects.forEach { effect -> if (isActive) dispatchEffect(effect) } + commands.forEach { if (isActive) executeCommand(it) } + } catch (error: CancellationException) { + throw error + } catch (t: Throwable) { + storeListeners.forEach { it.onReducerError(key, t, event) } + logger.fatal( + message = "You must handle all errors inside reducer", + tag = key, + error = t, + ) } } diff --git a/elmslie-core/src/commonTest/kotlin/money/vivid/elmslie/core/store/ElmStoreTest.kt b/elmslie-core/src/commonTest/kotlin/money/vivid/elmslie/core/store/ElmStoreTest.kt index 7b0098a5..fb923b17 100644 --- a/elmslie-core/src/commonTest/kotlin/money/vivid/elmslie/core/store/ElmStoreTest.kt +++ b/elmslie-core/src/commonTest/kotlin/money/vivid/elmslie/core/store/ElmStoreTest.kt @@ -1,9 +1,5 @@ package money.vivid.elmslie.core.store -import kotlin.test.AfterTest -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay @@ -14,6 +10,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runCurrent @@ -24,6 +21,10 @@ import money.vivid.elmslie.core.testutil.model.Command import money.vivid.elmslie.core.testutil.model.Effect import money.vivid.elmslie.core.testutil.model.Event import money.vivid.elmslie.core.testutil.model.State +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals @OptIn(ExperimentalCoroutinesApi::class) class ElmStoreTest { @@ -73,8 +74,7 @@ class ElmStoreTest { val emittedStates = mutableListOf() val collectJob = launch { store.states.toList(emittedStates) } store.accept(Event()) - runCurrent() - delay(3500) + advanceTimeBy(3500) store.stop() assertEquals(