Skip to content

Commit f0ef7a4

Browse files
author
Jenn Kao
committed
Add createSynchronizedStoreEnhancer function
`createSynchronizedStoreEnhancer` generates a store enhancer that wraps a Redux store in a synchronization object, causing access to store methods to be synchronized. Recommended usecase is when using a thread-safe store with enhancers or middleware that require access to store methods.
1 parent 5db9bec commit f0ef7a4

File tree

2 files changed

+92
-17
lines changed

2 files changed

+92
-17
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.reduxkotlin
2+
3+
/**
4+
* Creates a store enhancer that wraps a Redux store in a synchronization object,
5+
* causing access to store methods to be synchronized.
6+
*
7+
* See `SynchronizedStore` for implementation of synchronization.
8+
*
9+
* This enhancer should be placed after all other enhancers that involve access to store methods in
10+
* the composition chain, as this will result in those enhancers receiving the synchronized store object.
11+
12+
* @returns {StoreEnhancer} A store enhancer that synchronizes the store.
13+
*/
14+
fun <State> createSynchronizedStoreEnhancer(): StoreEnhancer<State> {
15+
return { storeCreator ->
16+
{ reducer, initialState, en: Any? ->
17+
val store = storeCreator(reducer, initialState, en)
18+
val synchronizedStore = SynchronizedStore(store)
19+
synchronizedStore
20+
}
21+
}
22+
}

redux-kotlin-threadsafe/src/jvmTest/kotlin/org/reduxkotlin/util/CreateThreadSafeStoreSpec.kt

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,48 +7,101 @@ import kotlinx.coroutines.runBlocking
77
import kotlinx.coroutines.withContext
88
import org.junit.Test
99
import org.reduxkotlin.createThreadSafeStore
10+
import org.reduxkotlin.applyMiddleware
11+
import org.reduxkotlin.createStore
12+
import org.reduxkotlin.createSynchronizedStoreEnhancer
13+
import org.reduxkotlin.compose
14+
import org.reduxkotlin.Dispatcher
15+
import org.reduxkotlin.GetState
16+
import org.reduxkotlin.Middleware
17+
import java.util.Timer
18+
import kotlin.concurrent.timerTask
1019
import kotlin.system.measureTimeMillis
1120
import kotlin.test.assertEquals
1221

1322
class MultiThreadedClass {
14-
@Test
15-
fun multithreadedIncrementsMassively() {
16-
suspend fun massiveRun(action: suspend () -> Unit) {
17-
val n = 100 // number of coroutines to launch
18-
val k = 1000 // times an action is repeated by each coroutine
19-
val time = measureTimeMillis {
20-
coroutineScope {
21-
// scope for coroutines
22-
repeat(n) {
23-
launch {
24-
repeat(k) { action() }
25-
}
23+
private suspend fun massiveRun(numCoroutines: Int, numRepeats: Int, action: suspend () -> Unit) {
24+
val time = measureTimeMillis {
25+
coroutineScope {
26+
repeat(numCoroutines) {
27+
launch {
28+
repeat(numRepeats) { action() }
2629
}
2730
}
2831
}
29-
println("Completed ${n * k} actions in $time ms")
3032
}
33+
println("Completed ${numCoroutines * numRepeats} actions in $time ms")
34+
}
3135

36+
@Test
37+
fun multithreadedIncrementsMassively() {
3238
// NOTE: changing this to createStore() breaks the tests
33-
val store = createThreadSafeStore(counterReducer, TestCounterState())
39+
val store = createThreadSafeStore(counterReducer, TestState())
3440
runBlocking {
3541
withContext(Dispatchers.Default) {
36-
massiveRun {
42+
massiveRun(100, 1000) {
3743
store.dispatch(Increment())
3844
}
3945
}
4046
assertEquals(100000, store.state.counter)
4147
}
4248
}
49+
50+
@Test
51+
fun multithreadedIncrementsMassivelyWithEnhancer() {
52+
val store = createStore(counterReducer, TestState(), compose(
53+
applyMiddleware(createTestThunkMiddleware()),
54+
createSynchronizedStoreEnhancer() // needs to be placed after enhancers that requires synchronized store methods
55+
))
56+
runBlocking {
57+
withContext(Dispatchers.Default) {
58+
massiveRun(10, 100) {
59+
store.dispatch(incrementThunk())
60+
}
61+
}
62+
// wait to assert to account for the last of thunk delays
63+
Timer().schedule(timerTask {
64+
assertEquals(10000, store.state.counter)
65+
}, 50)
66+
}
67+
}
4368
}
4469

4570
class Increment
4671

47-
data class TestCounterState(val counter: Int = 0)
72+
data class TestState(val counter: Int = 0)
4873

49-
val counterReducer = { state: TestCounterState, action: Any ->
74+
val counterReducer = { state: TestState, action: Any ->
5075
when (action) {
5176
is Increment -> state.copy(counter = state.counter + 1)
5277
else -> state
5378
}
5479
}
80+
81+
// Enhancer mimics the behavior of `createThunkMiddleware` provided by the redux-kotlin-thunk library
82+
typealias TestThunk<State> = (dispatch: Dispatcher, getState: GetState<State>, extraArg: Any?) -> Any
83+
fun <State> createTestThunkMiddleware(): Middleware<State> =
84+
{ store ->
85+
{ next: Dispatcher ->
86+
{ action: Any ->
87+
if (action is Function<*>) {
88+
@Suppress("UNCHECKED_CAST")
89+
val thunk = try {
90+
(action as TestThunk<*>)
91+
} catch (e: ClassCastException) {
92+
throw IllegalArgumentException("Require type TestThunk", e)
93+
}
94+
thunk(store.dispatch, store.getState, null)
95+
} else {
96+
next(action)
97+
}
98+
}
99+
}
100+
}
101+
102+
fun incrementThunk(): TestThunk<TestState> = { dispatch, getState, _ ->
103+
Timer().schedule(timerTask {
104+
dispatch(Increment())
105+
}, 50)
106+
getState()
107+
}

0 commit comments

Comments
 (0)