Skip to content

Commit 8b8d98e

Browse files
committed
feat: Treat withState actions as side effects
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.
1 parent 822713e commit 8b8d98e

File tree

5 files changed

+50
-9
lines changed

5 files changed

+50
-9
lines changed

build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ buildscript {
55
"targetSdk" : 29,
66
"kotlin" : "1.3.61",
77
"agp" : "3.5.2",
8-
"versionCode": 11,
9-
"versionName": "0.5.5"
8+
"versionCode": 12,
9+
"versionName": "0.6.0"
1010
]
1111

1212
ext.versions = [

vector/src/main/java/com/haroldadmin/vector/VectorViewModel.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,10 @@ abstract class VectorViewModel<S : VectorState>(
7979
/**
8080
* Dispatch the given action the [stateStore]. This action shall be processed as soon as all existing
8181
* state reducers have been processed. The state parameter supplied to this action should be the
82-
* latest value at the time of processing of this action
82+
* latest value at the time of processing of this action.
83+
*
84+
* These actions are treated as side effects. A new coroutine is launched for each such action, so that the state
85+
* processor does not get blocked if a particular action takes too long to finish.
8386
*
8487
* @param action The action to be performed with the current state
8588
*

vector/src/main/java/com/haroldadmin/vector/state/SelectBasedStateProcessor.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ internal class SelectBasedStateProcessor<S : VectorState>(
9191
stateHolder.stateObservable.offer(newState)
9292
}
9393
getStateChannel.onReceive { action ->
94-
action.invoke(stateHolder.state)
94+
launch {
95+
action.invoke(stateHolder.state)
96+
}
9597
}
9698
}
9799
}

vector/src/main/java/com/haroldadmin/vector/state/StateProcessor.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,20 @@ interface StateProcessor<S : VectorState> : CoroutineScope {
3737

3838
/**
3939
* Offer a [SetStateAction] to this processor. This action will be processed as soon as
40-
* possible, before all existing [GetStateAction], if any.
40+
* possible, before all existing [GetStateAction] waiting in the queue, if any.
4141
*
4242
* @param reducer The action to be offered
4343
*/
4444
fun offerSetAction(reducer: suspend S.() -> S)
4545

4646
/**
47-
* Offer a [GetStateAction] to this processor. This action will be processed after any existing
48-
* [GetStateAction] current waiting in this processor. The state parameter supplied to this action
47+
* Offer a [GetStateAction] to this processor. The state parameter supplied to this action
4948
* shall be the latest state value at the time of processing this action.
49+
*
50+
* These actions are treated as side effects. When such an action is received, a separate coroutine is launched
51+
* to process it. This means that when there are multiple such actions waiting in the queue, they will be launched
52+
* in order, but their completion depends on how long it takes to process them. They will be processed in the
53+
* coroutine context of their state processor.
5054
*/
5155
fun offerGetAction(action: suspend (S) -> Unit)
5256

vector/src/test/java/com/haroldadmin/vector/state/SelectBasedStateProcessorTest.kt

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.haroldadmin.vector.state
22

3+
import com.haroldadmin.vector.Vector
34
import com.haroldadmin.vector.extensions.awaitCompletion
45
import com.haroldadmin.vector.loggers.systemOutLogger
56
import kotlinx.coroutines.CompletableDeferred
@@ -8,6 +9,7 @@ import kotlinx.coroutines.Job
89
import kotlinx.coroutines.async
910
import kotlinx.coroutines.awaitAll
1011
import kotlinx.coroutines.channels.ClosedSendChannelException
12+
import kotlinx.coroutines.delay
1113
import kotlinx.coroutines.runBlocking
1214
import org.junit.After
1315
import org.junit.Before
@@ -21,7 +23,12 @@ internal class SelectBasedStateProcessorTest {
2123
@Before
2224
fun setup() {
2325
holder = StateHolderFactory.create(CountingState(), systemOutLogger())
24-
processor = SelectBasedStateProcessor(true, holder, systemOutLogger(), Dispatchers.Default + Job())
26+
processor = SelectBasedStateProcessor(
27+
isLazy = true,
28+
stateHolder = holder,
29+
logger = systemOutLogger(),
30+
coroutineContext = Dispatchers.Unconfined + Job()
31+
)
2532
}
2633

2734
@After
@@ -109,7 +116,12 @@ internal class SelectBasedStateProcessorTest {
109116

110117
processor.start()
111118

112-
awaitAll(incrementActionsSourceJob, decrementActionsSourceJob, additionJobsCompletable, subtractionJobsCompletable)
119+
awaitAll(
120+
incrementActionsSourceJob,
121+
decrementActionsSourceJob,
122+
additionJobsCompletable,
123+
subtractionJobsCompletable
124+
)
113125

114126
assert(holder.state.count == 0)
115127
}
@@ -126,4 +138,24 @@ internal class SelectBasedStateProcessorTest {
126138
processor.start()
127139
repeat(10) { processor.clearProcessor() }
128140
}
141+
142+
@Test
143+
fun `should not wait for get-state actions to complete before processing the next action`() = runBlocking {
144+
val initialCount = holder.state.count + 1
145+
processor.start()
146+
147+
processor.offerGetAction {
148+
delay(1000L)
149+
}
150+
151+
awaitCompletion<Unit> {
152+
processor.offerSetAction {
153+
copy(count = initialCount + 1).also { complete(Unit) }
154+
}
155+
}
156+
157+
assert(holder.state.count == initialCount + 1) {
158+
"Expected count = ${initialCount + 1}, actual: ${holder.state.count}"
159+
}
160+
}
129161
}

0 commit comments

Comments
 (0)