Skip to content

Commit fcd8a87

Browse files
authored
Merge pull request #78 from jennkao/jenn/threadsafe-enhancer
Jenn/threadsafe enhancer
2 parents 5db9bec + d2b6e15 commit fcd8a87

File tree

3 files changed

+110
-18
lines changed

3 files changed

+110
-18
lines changed

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,28 @@ Using the convenience helper function `middleware`:
112112

113113
__Create a store__
114114
```
115-
val store = createThreadSafeStore(reducer, AppState(user, listOf()), applyMiddleware(loggingMiddleware))
115+
val store = createStore(reducer, AppState(user, listOf()), applyMiddleware(loggingMiddleware))
116116
```
117117

118118
You then will have access to dispatch and subscribe functions from the `store`.
119119

120+
__Create a synchronized store__
121+
```
122+
val store = createThreadSafeStore(reducer, AppState(user, listOf()), applyMiddleware(loggingMiddleware))
123+
```
124+
125+
Access to `store` methods like `dispatch` and `getState` will be synchronized. Note: if using a thread safe store with enhancers or middleware that require access to store methods, see usage below.
126+
127+
__Create a synchronized store using an enhancer__
128+
```
129+
val store = createStore(reducer, AppState(user, listOf(), compose(
130+
applyMiddleware(createThunkMiddleware(), loggingMiddleware),
131+
createSynchronizedStoreEnhancer() // needs to be placed after enhancers that requires synchronized store methods
132+
))
133+
```
134+
135+
Access to `store` methods like `dispatch` and `getState` will be synchronized, and enhancers (eg. `applyMiddleware`) that are placed above `createSynchronizedStoreEnhancer` in the enhancer composition chain will receive the synchronized store.
136+
120137
## Communication
121138
Want to give feedback, contribute, or ask questions?
122139

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)