diff --git a/build.gradle b/build.gradle index 669cd65..1a15dda 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { "targetSdk" : 29, "kotlin" : "1.3.61", "agp" : "3.5.2", - "versionCode": 11, - "versionName": "0.5.5" + "versionCode": 12, + "versionName": "0.6.0" ] ext.versions = [ diff --git a/vector/src/main/java/com/haroldadmin/vector/VectorViewModel.kt b/vector/src/main/java/com/haroldadmin/vector/VectorViewModel.kt index 348371a..ae2f099 100644 --- a/vector/src/main/java/com/haroldadmin/vector/VectorViewModel.kt +++ b/vector/src/main/java/com/haroldadmin/vector/VectorViewModel.kt @@ -79,7 +79,10 @@ abstract class VectorViewModel( /** * Dispatch the given action the [stateStore]. This action shall be processed as soon as all existing * state reducers have been processed. The state parameter supplied to this action should be the - * latest value at the time of processing of this action + * latest value at the time of processing of this action. + * + * These actions are treated as side effects. A new coroutine is launched for each such action, so that the state + * processor does not get blocked if a particular action takes too long to finish. * * @param action The action to be performed with the current state * diff --git a/vector/src/main/java/com/haroldadmin/vector/state/SelectBasedStateProcessor.kt b/vector/src/main/java/com/haroldadmin/vector/state/SelectBasedStateProcessor.kt index 5b33bee..899c73e 100644 --- a/vector/src/main/java/com/haroldadmin/vector/state/SelectBasedStateProcessor.kt +++ b/vector/src/main/java/com/haroldadmin/vector/state/SelectBasedStateProcessor.kt @@ -91,7 +91,9 @@ internal class SelectBasedStateProcessor( stateHolder.stateObservable.offer(newState) } getStateChannel.onReceive { action -> - action.invoke(stateHolder.state) + launch { + action.invoke(stateHolder.state) + } } } } diff --git a/vector/src/main/java/com/haroldadmin/vector/state/StateProcessor.kt b/vector/src/main/java/com/haroldadmin/vector/state/StateProcessor.kt index 242a4cb..a8772e1 100644 --- a/vector/src/main/java/com/haroldadmin/vector/state/StateProcessor.kt +++ b/vector/src/main/java/com/haroldadmin/vector/state/StateProcessor.kt @@ -37,16 +37,20 @@ interface StateProcessor : CoroutineScope { /** * Offer a [SetStateAction] to this processor. This action will be processed as soon as - * possible, before all existing [GetStateAction], if any. + * possible, before all existing [GetStateAction] waiting in the queue, if any. * * @param reducer The action to be offered */ fun offerSetAction(reducer: suspend S.() -> S) /** - * Offer a [GetStateAction] to this processor. This action will be processed after any existing - * [GetStateAction] current waiting in this processor. The state parameter supplied to this action + * Offer a [GetStateAction] to this processor. The state parameter supplied to this action * shall be the latest state value at the time of processing this action. + * + * These actions are treated as side effects. When such an action is received, a separate coroutine is launched + * to process it. This means that when there are multiple such actions waiting in the queue, they will be launched + * in order, but their completion depends on how long it takes to process them. They will be processed in the + * coroutine context of their state processor. */ fun offerGetAction(action: suspend (S) -> Unit) diff --git a/vector/src/test/java/com/haroldadmin/vector/state/SelectBasedStateProcessorTest.kt b/vector/src/test/java/com/haroldadmin/vector/state/SelectBasedStateProcessorTest.kt index 9366f08..652bc7f 100644 --- a/vector/src/test/java/com/haroldadmin/vector/state/SelectBasedStateProcessorTest.kt +++ b/vector/src/test/java/com/haroldadmin/vector/state/SelectBasedStateProcessorTest.kt @@ -1,5 +1,6 @@ package com.haroldadmin.vector.state +import com.haroldadmin.vector.Vector import com.haroldadmin.vector.extensions.awaitCompletion import com.haroldadmin.vector.loggers.systemOutLogger import kotlinx.coroutines.CompletableDeferred @@ -8,6 +9,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.channels.ClosedSendChannelException +import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import org.junit.After import org.junit.Before @@ -21,7 +23,12 @@ internal class SelectBasedStateProcessorTest { @Before fun setup() { holder = StateHolderFactory.create(CountingState(), systemOutLogger()) - processor = SelectBasedStateProcessor(true, holder, systemOutLogger(), Dispatchers.Default + Job()) + processor = SelectBasedStateProcessor( + isLazy = true, + stateHolder = holder, + logger = systemOutLogger(), + coroutineContext = Dispatchers.Unconfined + Job() + ) } @After @@ -109,7 +116,12 @@ internal class SelectBasedStateProcessorTest { processor.start() - awaitAll(incrementActionsSourceJob, decrementActionsSourceJob, additionJobsCompletable, subtractionJobsCompletable) + awaitAll( + incrementActionsSourceJob, + decrementActionsSourceJob, + additionJobsCompletable, + subtractionJobsCompletable + ) assert(holder.state.count == 0) } @@ -126,4 +138,24 @@ internal class SelectBasedStateProcessorTest { processor.start() repeat(10) { processor.clearProcessor() } } + + @Test + fun `should not wait for get-state actions to complete before processing the next action`() = runBlocking { + val initialCount = holder.state.count + 1 + processor.start() + + processor.offerGetAction { + delay(1000L) + } + + awaitCompletion { + processor.offerSetAction { + copy(count = initialCount + 1).also { complete(Unit) } + } + } + + assert(holder.state.count == initialCount + 1) { + "Expected count = ${initialCount + 1}, actual: ${holder.state.count}" + } + } } \ No newline at end of file