Skip to content

Commit

Permalink
feat: Treat withState actions as side effects
Browse files Browse the repository at this point in the history
Fixes #29. This change changes the treatment of GetStateActions.
These actions are now treated as side effects, and the state processor
does not wait for their completion. Instead, it spins off a separate
to execute them, and moves on to process other actions in the queue
without waiting for it to finish.
  • Loading branch information
haroldadmin committed Feb 14, 2020
1 parent 822713e commit 8b8d98e
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 9 deletions.
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ abstract class VectorViewModel<S : VectorState>(
/**
* 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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ internal class SelectBasedStateProcessor<S : VectorState>(
stateHolder.stateObservable.offer(newState)
}
getStateChannel.onReceive { action ->
action.invoke(stateHolder.state)
launch {
action.invoke(stateHolder.state)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,20 @@ interface StateProcessor<S : VectorState> : 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)

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -109,7 +116,12 @@ internal class SelectBasedStateProcessorTest {

processor.start()

awaitAll(incrementActionsSourceJob, decrementActionsSourceJob, additionJobsCompletable, subtractionJobsCompletable)
awaitAll(
incrementActionsSourceJob,
decrementActionsSourceJob,
additionJobsCompletable,
subtractionJobsCompletable
)

assert(holder.state.count == 0)
}
Expand All @@ -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<Unit> {
processor.offerSetAction {
copy(count = initialCount + 1).also { complete(Unit) }
}
}

assert(holder.state.count == initialCount + 1) {
"Expected count = ${initialCount + 1}, actual: ${holder.state.count}"
}
}
}

0 comments on commit 8b8d98e

Please # to comment.