Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,28 @@ Using the convenience helper function `middleware`:

__Create a store__
```
val store = createThreadSafeStore(reducer, AppState(user, listOf()), applyMiddleware(loggingMiddleware))
val store = createStore(reducer, AppState(user, listOf()), applyMiddleware(loggingMiddleware))
```

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

__Create a synchronized store__
```
val store = createThreadSafeStore(reducer, AppState(user, listOf()), applyMiddleware(loggingMiddleware))
```

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.

__Create a synchronized store using an enhancer__
```
val store = createStore(reducer, AppState(user, listOf(), compose(
applyMiddleware(createThunkMiddleware(), loggingMiddleware),
createSynchronizedStoreEnhancer() // needs to be placed after enhancers that requires synchronized store methods
))
```

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.

## Communication
Want to give feedback, contribute, or ask questions?

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.reduxkotlin

/**
* Creates a store enhancer that wraps a Redux store in a synchronization object,
* causing access to store methods to be synchronized.
*
* See `SynchronizedStore` for implementation of synchronization.
*
* This enhancer should be placed after all other enhancers that involve access to store methods in
* the composition chain, as this will result in those enhancers receiving the synchronized store object.

* @returns {StoreEnhancer} A store enhancer that synchronizes the store.
*/
fun <State> createSynchronizedStoreEnhancer(): StoreEnhancer<State> {
return { storeCreator ->
{ reducer, initialState, en: Any? ->
val store = storeCreator(reducer, initialState, en)
val synchronizedStore = SynchronizedStore(store)
synchronizedStore
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,101 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.junit.Test
import org.reduxkotlin.createThreadSafeStore
import org.reduxkotlin.applyMiddleware
import org.reduxkotlin.createStore
import org.reduxkotlin.createSynchronizedStoreEnhancer
import org.reduxkotlin.compose
import org.reduxkotlin.Dispatcher
import org.reduxkotlin.GetState
import org.reduxkotlin.Middleware
import java.util.Timer
import kotlin.concurrent.timerTask
import kotlin.system.measureTimeMillis
import kotlin.test.assertEquals

class MultiThreadedClass {
@Test
fun multithreadedIncrementsMassively() {
suspend fun massiveRun(action: suspend () -> Unit) {
val n = 100 // number of coroutines to launch
val k = 1000 // times an action is repeated by each coroutine
val time = measureTimeMillis {
coroutineScope {
// scope for coroutines
repeat(n) {
launch {
repeat(k) { action() }
}
private suspend fun massiveRun(numCoroutines: Int, numRepeats: Int, action: suspend () -> Unit) {
val time = measureTimeMillis {
coroutineScope {
repeat(numCoroutines) {
launch {
repeat(numRepeats) { action() }
}
}
}
println("Completed ${n * k} actions in $time ms")
}
println("Completed ${numCoroutines * numRepeats} actions in $time ms")
}

@Test
fun multithreadedIncrementsMassively() {
// NOTE: changing this to createStore() breaks the tests
val store = createThreadSafeStore(counterReducer, TestCounterState())
val store = createThreadSafeStore(counterReducer, TestState())
runBlocking {
withContext(Dispatchers.Default) {
massiveRun {
massiveRun(100, 1000) {
store.dispatch(Increment())
}
}
assertEquals(100000, store.state.counter)
}
}

@Test
fun multithreadedIncrementsMassivelyWithEnhancer() {
val store = createStore(counterReducer, TestState(), compose(
applyMiddleware(createTestThunkMiddleware()),
createSynchronizedStoreEnhancer() // needs to be placed after enhancers that requires synchronized store methods
))
runBlocking {
withContext(Dispatchers.Default) {
massiveRun(10, 100) {
store.dispatch(incrementThunk())
}
}
// wait to assert to account for the last of thunk delays
Timer().schedule(timerTask {
assertEquals(10000, store.state.counter)
}, 50)
}
}
}

class Increment

data class TestCounterState(val counter: Int = 0)
data class TestState(val counter: Int = 0)

val counterReducer = { state: TestCounterState, action: Any ->
val counterReducer = { state: TestState, action: Any ->
when (action) {
is Increment -> state.copy(counter = state.counter + 1)
else -> state
}
}

// Enhancer mimics the behavior of `createThunkMiddleware` provided by the redux-kotlin-thunk library
typealias TestThunk<State> = (dispatch: Dispatcher, getState: GetState<State>, extraArg: Any?) -> Any
fun <State> createTestThunkMiddleware(): Middleware<State> =
{ store ->
{ next: Dispatcher ->
{ action: Any ->
if (action is Function<*>) {
@Suppress("UNCHECKED_CAST")
val thunk = try {
(action as TestThunk<*>)
} catch (e: ClassCastException) {
throw IllegalArgumentException("Require type TestThunk", e)
}
thunk(store.dispatch, store.getState, null)
} else {
next(action)
}
}
}
}

fun incrementThunk(): TestThunk<TestState> = { dispatch, getState, _ ->
Timer().schedule(timerTask {
dispatch(Increment())
}, 50)
getState()
}