Skip to content

Commit 1387be5

Browse files
only allow one state collector at a time (#308)
* only allow one state collector at a time * Update flowredux/src/commonTest/kotlin/com/freeletics/flowredux/dsl/FlowReduxStateMachineTest.kt Co-authored-by: Hannes Dorfmann <[email protected]> Co-authored-by: Hannes Dorfmann <[email protected]>
1 parent 85061f0 commit 1387be5

File tree

3 files changed

+64
-41
lines changed

3 files changed

+64
-41
lines changed

flowredux/src/commonMain/kotlin/com/freeletics/flowredux/dsl/Dsl.kt

Lines changed: 0 additions & 37 deletions
This file was deleted.

flowredux/src/commonMain/kotlin/com/freeletics/flowredux/dsl/FlowReduxStateMachine.kt

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
package com.freeletics.flowredux.dsl
22

3+
import com.freeletics.flowredux.dsl.internal.Action
4+
import com.freeletics.flowredux.dsl.internal.ExternalWrappedAction
5+
import com.freeletics.flowredux.dsl.internal.InitialStateAction
6+
import com.freeletics.flowredux.dsl.internal.reducer
37
import com.freeletics.flowredux.dsl.util.AtomicCounter
8+
import com.freeletics.flowredux.reduxStore
49
import com.freeletics.mad.statemachine.StateMachine
510
import kotlinx.coroutines.ExperimentalCoroutinesApi
611
import kotlinx.coroutines.FlowPreview
712
import kotlinx.coroutines.channels.Channel
813
import kotlinx.coroutines.flow.Flow
14+
import kotlinx.coroutines.flow.distinctUntilChanged
15+
import kotlinx.coroutines.flow.map
916
import kotlinx.coroutines.flow.onCompletion
10-
import kotlinx.coroutines.flow.onEach
1117
import kotlinx.coroutines.flow.onStart
1218
import kotlinx.coroutines.flow.receiveAsFlow
1319

@@ -32,11 +38,24 @@ public abstract class FlowReduxStateMachine<S : Any, A : Any>(
3238
)
3339
}
3440

41+
val sideEffects = FlowReduxStoreBuilder<S, A>().apply(specBlock).generateSideEffects()
42+
3543
outputState = inputActions
3644
.receiveAsFlow()
37-
.reduxStore(initialStateSupplier, specBlock)
45+
.map<A, Action<S, A>> { ExternalWrappedAction(it) }
46+
.onStart {
47+
emit(InitialStateAction())
48+
}
49+
.reduxStore(initialStateSupplier, sideEffects, ::reducer)
50+
.distinctUntilChanged { old, new -> old === new } // distinct until not the same object reference.
3851
.onStart {
39-
activeFlowCounter.incrementAndGet()
52+
if (activeFlowCounter.incrementAndGet() > 1) {
53+
throw IllegalStateException(
54+
"Can not collect state more than once at the same time. Make sure the" +
55+
"previous collection is cancelled before starting a new one. " +
56+
"Collecting state in parallel would lead to subtle bugs."
57+
)
58+
}
4059
}
4160
.onCompletion {
4261
activeFlowCounter.decrementAndGet()

flowredux/src/commonTest/kotlin/com/freeletics/flowredux/dsl/FlowReduxStateMachineTest.kt

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import kotlin.test.Test
88
import kotlin.test.assertEquals
99
import kotlin.test.assertFailsWith
1010
import kotlin.test.fail
11-
import kotlin.time.ExperimentalTime
11+
import kotlinx.coroutines.delay
12+
import kotlinx.coroutines.flow.first
13+
import kotlinx.coroutines.launch
1214

1315
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
1416
class FlowReduxStateMachineTest {
@@ -88,4 +90,43 @@ class FlowReduxStateMachineTest {
8890

8991
assertEquals(expectedMsg, exception.message)
9092
}
93+
94+
@Test
95+
fun `observing state multiple times in parallel throws exception`() = suspendTest {
96+
val sm = StateMachine {}
97+
98+
var collectionStarted = false
99+
val job = launch {
100+
sm.state.collect {
101+
collectionStarted = true
102+
}
103+
}
104+
105+
while (!collectionStarted) {
106+
delay(1)
107+
}
108+
109+
val exception = assertFailsWith<IllegalStateException> {
110+
sm.state.collect { }
111+
}
112+
113+
114+
val expectedMsg =
115+
"Can not collect state more than once at the same time. Make sure the" +
116+
"previous collection is cancelled before starting a new one. " +
117+
"Collecting state in parallel would lead to subtle bugs."
118+
119+
assertEquals(expectedMsg, exception.message)
120+
121+
job.cancel()
122+
}
123+
124+
@Test
125+
fun `observing state multiple times in sequence`() = suspendTest {
126+
val sm = StateMachine {}
127+
128+
// each call will collect the first item and then stop collecting
129+
sm.state.first()
130+
sm.state.first()
131+
}
91132
}

0 commit comments

Comments
 (0)