Skip to content

Commit d304b15

Browse files
committed
Introduce TypedStore<State,Action>!
Closes #92 Closes #46
1 parent 242da72 commit d304b15

File tree

13 files changed

+363
-171
lines changed

13 files changed

+363
-171
lines changed

gradle/detekt.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,20 @@ comments:
1212
active: false
1313
DeprecatedBlockTag:
1414
active: true
15-
excludes: [ "**/*Test.kt", "**/examples/**", "**/build-conventions/**" ]
15+
excludes: [ "**/src/*Test/**", "**/examples/**", "**/build-conventions/**" ]
1616
OutdatedDocumentation:
1717
active: true
18-
excludes: [ "**/*Test.kt", "**/examples/**", "**/build-conventions/**" ]
18+
excludes: [ "**/src/*Test/**", "**/examples/**", "**/build-conventions/**" ]
1919
allowParamOnConstructorProperties: false
2020
UndocumentedPublicClass:
2121
active: true
22-
excludes: [ "**/*Test.kt", "**/examples/**", "**/build-conventions/**" ]
22+
excludes: [ "**/src/*Test/**", "**/examples/**", "**/build-conventions/**" ]
2323
UndocumentedPublicFunction:
2424
active: true
25-
excludes: [ "**/*Test.kt", "**/examples/**", "**/build-conventions/**" ]
25+
excludes: [ "**/src/*Test/**", "**/examples/**", "**/build-conventions/**" ]
2626
UndocumentedPublicProperty:
2727
active: true
28-
excludes: [ "**/*Test.kt", "**/examples/**", "**/build-conventions/**" ]
28+
excludes: [ "**/src/*Test/**", "**/examples/**", "**/build-conventions/**" ]
2929

3030
naming:
3131
InvalidPackageDeclaration:

redux-kotlin-threadsafe/src/commonMain/kotlin/org/reduxkotlin/threadsafe/CreateThreadSafeStore.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package org.reduxkotlin.threadsafe
22

3-
import org.reduxkotlin.Reducer
4-
import org.reduxkotlin.Store
5-
import org.reduxkotlin.StoreEnhancer
6-
import org.reduxkotlin.createStore
3+
import org.reduxkotlin.*
74

85
/**
96
* Creates a SYNCHRONIZED, THREADSAFE Redux store that holds the state tree.
@@ -33,3 +30,12 @@ public fun <State> createThreadSafeStore(
3330
preloadedState: State,
3431
enhancer: StoreEnhancer<State>? = null
3532
): Store<State> = SynchronizedStore(createStore(reducer, preloadedState, enhancer))
33+
34+
/**
35+
* Creates a thread-safe [TypedStore]. For further details see the matching [createThreadSafeStore].
36+
*/
37+
public inline fun <State, reified Action : Any> createTypedThreadSafeStore(
38+
crossinline reducer: TypedReducer<State, Action>,
39+
preloadedState: State,
40+
noinline enhancer: StoreEnhancer<State>? = null
41+
): TypedStore<State, Action> = SynchronizedStore(createTypedStore(reducer, preloadedState, enhancer))

redux-kotlin-threadsafe/src/commonMain/kotlin/org/reduxkotlin/threadsafe/SynchronizedStore.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,18 @@ import org.reduxkotlin.*
1111
* This does have a performance impact for JVM/Native.
1212
* TODO more info at [https://ReduxKotlin.org]
1313
*/
14-
public class SynchronizedStore<TState>(private val store: Store<TState>) : Store<TState>, SynchronizedObject() {
14+
public class SynchronizedStore<State, Action>(private val store: TypedStore<State, Action>) : TypedStore<State, Action>,
15+
SynchronizedObject() {
1516

16-
override var dispatch: Dispatcher = { action ->
17+
override var dispatch: TypedDispatcher<Action> = { action ->
1718
synchronized(this) { store.dispatch(action) }
1819
}
1920

20-
override val getState: GetState<TState> = {
21+
override val getState: GetState<State> = {
2122
synchronized(this) { store.getState() }
2223
}
2324

24-
override val replaceReducer: (Reducer<TState>) -> Unit = { reducer ->
25+
override val replaceReducer: (TypedReducer<State, Action>) -> Unit = { reducer ->
2526
synchronized(this) { store.replaceReducer(reducer) }
2627
}
2728

redux-kotlin-threadsafe/src/jvmCommonTest/kotlin/org/reduxkotlin/threadsafe/CreateThreadSafeStoreTest.kt

Lines changed: 10 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package org.reduxkotlin.threadsafe
22

33
import kotlinx.coroutines.*
44
import org.junit.Test
5-
import org.reduxkotlin.*
5+
import org.reduxkotlin.applyMiddleware
6+
import org.reduxkotlin.compose
7+
import org.reduxkotlin.createStore
8+
import test.TestApp
69
import java.util.*
710
import kotlin.concurrent.timerTask
811
import kotlin.system.measureTimeMillis
@@ -25,11 +28,11 @@ class CreateThreadSafeStoreTest {
2528
@Test
2629
fun multithreadedIncrementsMassively() {
2730
// NOTE: changing this to createStore() breaks the tests
28-
val store = createThreadSafeStore(counterReducer, TestState())
31+
val store = createThreadSafeStore(TestApp.counterReducer, TestApp.TestState())
2932
runBlocking {
3033
withContext(Dispatchers.Default) {
3134
massiveRun(100, 1000) {
32-
store.dispatch(Increment())
35+
store.dispatch(TestApp.Increment)
3336
}
3437
}
3538
assertEquals(100000, store.state.counter)
@@ -39,18 +42,18 @@ class CreateThreadSafeStoreTest {
3942
@Test
4043
fun multithreadedIncrementsMassivelyWithEnhancer() {
4144
val store = createStore(
42-
counterReducer,
43-
TestState(),
45+
TestApp.counterReducer,
46+
TestApp.TestState(),
4447
compose(
45-
applyMiddleware(createTestThunkMiddleware()),
48+
applyMiddleware(TestApp.createTestThunkMiddleware()),
4649
// needs to be placed after enhancers that requires synchronized store methods
4750
createSynchronizedStoreEnhancer()
4851
)
4952
)
5053
runBlocking {
5154
withContext(Dispatchers.Default) {
5255
massiveRun(10, 100) {
53-
store.dispatch(incrementThunk())
56+
store.dispatch(TestApp.incrementThunk())
5457
}
5558
}
5659
// wait to assert to account for the last of thunk delays
@@ -63,45 +66,3 @@ class CreateThreadSafeStoreTest {
6366
}
6467
}
6568
}
66-
67-
class Increment
68-
69-
data class TestState(val counter: Int = 0)
70-
71-
val counterReducer = { state: TestState, action: Any ->
72-
when (action) {
73-
is Increment -> state.copy(counter = state.counter + 1)
74-
else -> state
75-
}
76-
}
77-
78-
// Enhancer mimics the behavior of `createThunkMiddleware` provided by the redux-kotlin-thunk library
79-
typealias TestThunk<State> = (dispatch: Dispatcher, getState: GetState<State>, extraArg: Any?) -> Any
80-
81-
fun <State> createTestThunkMiddleware(): Middleware<State> = { store ->
82-
{ next: Dispatcher ->
83-
{ action: Any ->
84-
if (action is Function<*>) {
85-
@Suppress("UNCHECKED_CAST")
86-
val thunk = try {
87-
(action as TestThunk<*>)
88-
} catch (e: ClassCastException) {
89-
throw IllegalArgumentException("Require type TestThunk", e)
90-
}
91-
thunk(store.dispatch, store.getState, null)
92-
} else {
93-
next(action)
94-
}
95-
}
96-
}
97-
}
98-
99-
fun incrementThunk(): TestThunk<TestState> = { dispatch, getState, _ ->
100-
Timer().schedule(
101-
timerTask {
102-
dispatch(Increment())
103-
},
104-
50
105-
)
106-
getState()
107-
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package test
2+
3+
import org.reduxkotlin.*
4+
import java.util.*
5+
import kotlin.concurrent.timerTask
6+
7+
// Enhancer mimics the behavior of `createThunkMiddleware` provided by the redux-kotlin-thunk library
8+
typealias TestThunk<State> = (dispatch: Dispatcher, getState: GetState<State>, extraArg: Any?) -> Any
9+
10+
object TestApp {
11+
sealed interface TestAction
12+
object Increment : TestAction
13+
14+
data class TestState(val counter: Int = 0)
15+
16+
val counterReducer = { state: TestState, action: Any ->
17+
when (action) {
18+
is Increment -> state.copy(counter = state.counter + 1)
19+
else -> state
20+
}
21+
}
22+
23+
fun <State> createTestThunkMiddleware(): Middleware<State> = { store ->
24+
{ next: Dispatcher ->
25+
{ action: Any ->
26+
if (action is Function<*>) {
27+
@Suppress("UNCHECKED_CAST")
28+
val thunk = try {
29+
(action as TestThunk<*>)
30+
} catch (e: ClassCastException) {
31+
throw IllegalArgumentException("Require type TestThunk", e)
32+
}
33+
thunk(store.dispatch, store.getState, null)
34+
} else {
35+
next(action)
36+
}
37+
}
38+
}
39+
}
40+
41+
fun incrementThunk(): TestThunk<TestState> = { dispatch, getState, _ ->
42+
Timer().schedule(
43+
timerTask {
44+
dispatch(Increment)
45+
},
46+
50
47+
)
48+
getState()
49+
}
50+
}

redux-kotlin/src/commonMain/kotlin/org/reduxkotlin/CreateSameThreadEnforcedStore.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,26 @@ public fun <State> createSameThreadEnforcedStore(
5252
}
5353
}
5454
}
55+
56+
/**
57+
* Creates a [TypedStore]. For further details see the matching [createSameThreadEnforcedStore].
58+
*/
59+
public inline fun <State, reified Action : Any> createTypedSameThreadEnforcedStore(
60+
crossinline reducer: TypedReducer<State, Action>,
61+
preloadedState: State,
62+
noinline enhancer: StoreEnhancer<State>? = null
63+
): TypedStore<State, Action> {
64+
val store = createSameThreadEnforcedStore(
65+
reducer = typedReducer(reducer),
66+
preloadedState,
67+
enhancer,
68+
)
69+
return object : TypedStore<State, Action> {
70+
override val getState: GetState<State> = store.getState
71+
override var dispatch: TypedDispatcher<Action> = store.dispatch
72+
override val subscribe: (StoreSubscriber) -> StoreSubscription = store.subscribe
73+
override val replaceReducer: (TypedReducer<State, Action>) -> Unit = {
74+
store.replaceReducer(typedReducer(it))
75+
}
76+
}
77+
}

redux-kotlin/src/commonMain/kotlin/org/reduxkotlin/CreateStore.kt

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -179,17 +179,17 @@ public fun <State> createStore(
179179
""".trimMargin()
180180
}
181181

182-
/*
183-
check(!isDispatching) {
184-
"""You may not dispatch while state is being reduced.
185-
|2 conditions can cause this error:
186-
| 1) Dispatching from a reducer
187-
| 2) Dispatching from multiple threads
188-
|If #2 switch to createThreadSafeStore().
189-
|https://reduxkotlin.org/introduction/threading""".trimMargin()
190-
}
182+
/*
183+
check(!isDispatching) {
184+
"""You may not dispatch while state is being reduced.
185+
|2 conditions can cause this error:
186+
| 1) Dispatching from a reducer
187+
| 2) Dispatching from multiple threads
188+
|If #2 switch to createThreadSafeStore().
189+
|https://reduxkotlin.org/introduction/threading""".trimMargin()
190+
}
191191
192-
*/
192+
*/
193193

194194
try {
195195
isDispatching = true
@@ -246,3 +246,26 @@ public fun <State> createStore(
246246
override val replaceReducer = ::replaceReducer
247247
}
248248
}
249+
250+
/**
251+
* Creates a [TypedStore]. For further details see the matching [createStore].
252+
*/
253+
public inline fun <State, reified Action : Any> createTypedStore(
254+
crossinline reducer: TypedReducer<State, Action>,
255+
preloadedState: State,
256+
noinline enhancer: StoreEnhancer<State>? = null
257+
): TypedStore<State, Action> {
258+
val store = createStore(
259+
reducer = typedReducer(reducer),
260+
preloadedState,
261+
enhancer,
262+
)
263+
return object : TypedStore<State, Action> {
264+
override val getState: GetState<State> = store.getState
265+
override var dispatch: TypedDispatcher<Action> = store.dispatch
266+
override val subscribe: (StoreSubscriber) -> StoreSubscription = store.subscribe
267+
override val replaceReducer: (TypedReducer<State, Action>) -> Unit = {
268+
store.replaceReducer(typedReducer(it))
269+
}
270+
}
271+
}

redux-kotlin/src/commonMain/kotlin/org/reduxkotlin/Definitions.kt

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@ package org.reduxkotlin
33
/**
44
* See also https://github.com/reactjs/redux/blob/master/docs/Glossary.md#reducer
55
*/
6-
public typealias Reducer<State> = (state: State, action: Any) -> State
6+
public typealias Reducer<State> = TypedReducer<State, Any>
77

88
/**
99
* Reducer for a particular subclass of actions. Useful for Sealed classes &
10-
* exhaustive when statements. See [reducerForActionType].
10+
* exhaustive when statements. See [typedReducer].
1111
*/
12-
public typealias ReducerForActionType<TState, TAction> = (state: TState, action: TAction) -> TState
12+
public typealias TypedReducer<State, Action> = (state: State, action: Action) -> State
1313

1414
public typealias GetState<State> = () -> State
1515
public typealias StoreSubscriber = () -> Unit
1616
public typealias StoreSubscription = () -> Unit
17-
public typealias Dispatcher = (Any) -> Any
17+
public typealias Dispatcher = TypedDispatcher<Any>
18+
public typealias TypedDispatcher<Action> = (Action) -> Any
1819

1920
// Enhancer is type Any? to avoid a circular dependency of types.
2021
public typealias StoreCreator<State> = (
@@ -37,7 +38,12 @@ public typealias Middleware<State> = (store: Store<State>) -> (next: Dispatcher)
3738
/**
3839
* Main redux storage container for a given [State]
3940
*/
40-
public interface Store<State> {
41+
public typealias Store<State> = TypedStore<State, Any>
42+
43+
/**
44+
* Main redux storage container for a given [State] and typesafe actions
45+
*/
46+
public interface TypedStore<State, Action> {
4147
/**
4248
* Current store state getter
4349
*/
@@ -46,7 +52,7 @@ public interface Store<State> {
4652
/**
4753
* Dispatcher that can be used to update the store state
4854
*/
49-
public var dispatch: Dispatcher
55+
public var dispatch: TypedDispatcher<Action>
5056

5157
/**
5258
* Subscribes to state's updates.
@@ -57,7 +63,7 @@ public interface Store<State> {
5763
/**
5864
* Replace store's reducer with a new implementation
5965
*/
60-
public val replaceReducer: (Reducer<State>) -> Unit
66+
public val replaceReducer: (TypedReducer<State, Action>) -> Unit
6167

6268
/**
6369
* Current store state
@@ -80,12 +86,12 @@ public fun <State> middleware(dispatch: (Store<State>, next: Dispatcher, action:
8086
}
8187

8288
/**
83-
* Convenience function for creating a [ReducerForActionType]
89+
* Convenience function for creating a [TypedReducer]
8490
* usage:
8591
* sealed class LoginScreenAction
8692
* data class LoginComplete(val user: User): LoginScreenAction()
8793
*
88-
* val loginReducer = reducerForActionType<AppState, LoginAction> { state, action ->
94+
* val loginReducer = typedReducer<AppState, LoginAction> { state, action ->
8995
* when(action) {
9096
* is LoginComplete -> state.copy(user = action.user)
9197
* }
@@ -95,7 +101,7 @@ public fun <State> middleware(dispatch: (Store<State>, next: Dispatcher, action:
95101
* data class FeedLoaded(val items: FeedItems): FeedScreenAction
96102
* data class FeedLoadError(val msg: String): FeedScreenAction
97103
*
98-
* val feedReducer = reducerForActionType<AppState, FeedScreeAction> { state, action ->
104+
* val feedReducer = typedReducer<AppState, FeedScreeAction> { state, action ->
99105
* when(action) {
100106
* is FeedLoaded -> state.copy(feedItems = action.items)
101107
* is FeedLoadError -> state.copy(errorMsg = action.msg)
@@ -107,8 +113,8 @@ public fun <State> middleware(dispatch: (Store<State>, next: Dispatcher, action:
107113
* **or**
108114
* val store = createThreadSafeStore(rootReducer, AppState())
109115
*/
110-
public inline fun <TState, reified TAction> reducerForActionType(
111-
crossinline reducer: ReducerForActionType<TState, TAction>
116+
public inline fun <TState, reified TAction> typedReducer(
117+
crossinline reducer: TypedReducer<TState, TAction>
112118
): Reducer<TState> = { state, action ->
113119
when (action) {
114120
is TAction -> reducer(state, action)

0 commit comments

Comments
 (0)