From f0ef7a4744e6d14806a9437de51dbaf07b30d875 Mon Sep 17 00:00:00 2001 From: Jenn Kao Date: Sun, 2 May 2021 12:34:35 -0700 Subject: [PATCH 1/2] 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. --- .../kotlin/org/reduxkotlin/Enhancers.kt | 22 +++++ .../util/CreateThreadSafeStoreSpec.kt | 87 +++++++++++++++---- 2 files changed, 92 insertions(+), 17 deletions(-) create mode 100644 redux-kotlin-threadsafe/src/commonMain/kotlin/org/reduxkotlin/Enhancers.kt diff --git a/redux-kotlin-threadsafe/src/commonMain/kotlin/org/reduxkotlin/Enhancers.kt b/redux-kotlin-threadsafe/src/commonMain/kotlin/org/reduxkotlin/Enhancers.kt new file mode 100644 index 0000000..db1df99 --- /dev/null +++ b/redux-kotlin-threadsafe/src/commonMain/kotlin/org/reduxkotlin/Enhancers.kt @@ -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 createSynchronizedStoreEnhancer(): StoreEnhancer { + return { storeCreator -> + { reducer, initialState, en: Any? -> + val store = storeCreator(reducer, initialState, en) + val synchronizedStore = SynchronizedStore(store) + synchronizedStore + } + } +} diff --git a/redux-kotlin-threadsafe/src/jvmTest/kotlin/org/reduxkotlin/util/CreateThreadSafeStoreSpec.kt b/redux-kotlin-threadsafe/src/jvmTest/kotlin/org/reduxkotlin/util/CreateThreadSafeStoreSpec.kt index 6057d97..6bc1630 100644 --- a/redux-kotlin-threadsafe/src/jvmTest/kotlin/org/reduxkotlin/util/CreateThreadSafeStoreSpec.kt +++ b/redux-kotlin-threadsafe/src/jvmTest/kotlin/org/reduxkotlin/util/CreateThreadSafeStoreSpec.kt @@ -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 = (dispatch: Dispatcher, getState: GetState, extraArg: Any?) -> Any +fun createTestThunkMiddleware(): Middleware = + { 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 = { dispatch, getState, _ -> + Timer().schedule(timerTask { + dispatch(Increment()) + }, 50) + getState() +} From d2b6e153c420355f55d01b29672d61202990b397 Mon Sep 17 00:00:00 2001 From: Jenn Kao Date: Sun, 2 May 2021 12:35:57 -0700 Subject: [PATCH 2/2] Update README to include createSynchronizedStoreEnhancer enhancer usage --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1fff389..658d63b 100644 --- a/README.md +++ b/README.md @@ -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?