Skip to content

Commit 328f537

Browse files
authored
enforce same thread on getState() (#34)
* same thread enforcement on store functions * update docs with Threading info
1 parent 98b20be commit 328f537

File tree

21 files changed

+967
-616
lines changed

21 files changed

+967
-616
lines changed

CHANGELOG.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
## Unreleased
2+
3+
## [0.3.0] - 2019-12-16
4+
5+
### Added
6+
- thread enforcement
7+
18
## [0.2.9] - 2019-11-23
29

310
### Changed
411
- update Kotlin to 1.3.60
5-

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,15 @@ kotlin {
4444
sourceSets {
4545
commonMain { // <--- name may vary on your project
4646
dependencies {
47-
implementation "org.reduxkotlin:redux-kotlin:0.2.9"
47+
implementation "org.reduxkotlin:redux-kotlin:0.3.0"
4848
}
4949
}
5050
}
5151
```
5252

5353
For JVM only:
5454
```
55-
implementation "org.reduxkotlin:redux-kotlin-jvm:0.2.9"
55+
implementation "org.reduxkotlin:redux-kotlin-jvm:0.3.0"
5656
```
5757

5858
Usage is very similar to JS Redux and those docs will be useful https://redux.js.org/. These docs are not an intro to Redux, and just documentation on Kotlin specific bits. For more info on Redux in general, check out https://redux.js.org/.

examples/counter/android/src/main/java/org/reduxkotlin/example/counter/MainActivity.kt

-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import android.os.Handler
55
import androidx.appcompat.app.AppCompatActivity
66
import kotlinx.android.synthetic.main.activity_main.*
77
import org.reduxkotlin.StoreSubscription
8-
import org.reduxkotlin.combineReducers
98
import org.reduxkotlin.createStore
109
import org.reduxkotlin.examples.counter.Decrement
1110
import org.reduxkotlin.examples.counter.Increment

lib/build.gradle

+19-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ apply plugin: 'kotlin-multiplatform'
77
archivesBaseName = 'redux-kotlin'
88

99
group 'org.reduxkotlin'
10-
version '0.2.9'
10+
version '0.3.0'
1111

1212
kotlin {
1313
jvm()
@@ -87,6 +87,24 @@ kotlin {
8787
implementation kotlin("stdlib-js")
8888
}
8989
}
90+
winMain {
91+
kotlin.srcDir('src/fallbackMain')
92+
}
93+
wasmMain {
94+
kotlin.srcDir('src/fallbackMain')
95+
}
96+
linArm32Main {
97+
kotlin.srcDir('src/fallbackMain')
98+
}
99+
linMips32Main {
100+
kotlin.srcDir('src/fallbackMain')
101+
}
102+
linMipsel32Main {
103+
kotlin.srcDir('src/fallbackMain')
104+
}
105+
lin64Main {
106+
kotlin.srcDir('src/fallbackMain')
107+
}
90108

91109
iosSimMain.dependsOn iosMain
92110
iosSimTest.dependsOn iosTest

lib/src/commonMain/kotlin/org/reduxkotlin/CreateStore.kt

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.reduxkotlin
22

3+
import org.reduxkotlin.utils.getThreadName
34
import org.reduxkotlin.utils.isPlainObject
45

56
/**
@@ -44,6 +45,15 @@ fun <State> createStore(
4445
var currentListeners = mutableListOf<() -> Unit>()
4546
var nextListeners = currentListeners
4647
var isDispatching = false
48+
val storeThreadName = getThreadName()
49+
fun isSameThread() = getThreadName() == storeThreadName
50+
fun checkSameThread() = check(isSameThread()) {
51+
"""You may not call the store from a thread other than the thread on which it was created.
52+
|This includes: getState(), dispatch(), subscribe(), and replaceReducer()
53+
|This store was created on: '$storeThreadName' and current
54+
|thread is '${getThreadName()}'
55+
""".trimMargin()
56+
}
4757

4858
/**
4959
* This makes a shallow copy of currentListeners so we can use
@@ -64,6 +74,7 @@ fun <State> createStore(
6474
* @returns {S} The current state tree of your application.
6575
*/
6676
fun getState(): State {
77+
checkSameThread()
6778
check(!isDispatching) {
6879
"""|You may not call store.getState() while the reducer is executing.
6980
|The reducer has already received the state as an argument.
@@ -100,6 +111,7 @@ fun <State> createStore(
100111
* @returns {StoreSubscription} A fun to remove this change listener.
101112
*/
102113
fun subscribe(listener: StoreSubscriber): StoreSubscription {
114+
checkSameThread()
103115
check(!isDispatching) {
104116
"""|You may not call store.subscribe() while the reducer is executing.
105117
|If you would like to be notified after the store has been updated,
@@ -159,6 +171,7 @@ fun <State> createStore(
159171
* return something else (for example, a Promise you can await).
160172
*/
161173
fun dispatch(action: Any): Any {
174+
checkSameThread()
162175
require(isPlainObject(action)) {
163176
"""Actions must be plain objects. Use custom middleware for async
164177
|actions.""".trimMargin()
@@ -193,6 +206,7 @@ fun <State> createStore(
193206
* @returns {void}
194207
*/
195208
fun replaceReducer(nextReducer: Reducer<State>) {
209+
checkSameThread()
196210
currentReducer = nextReducer
197211

198212
// This action has a similar effect to ActionTypes.INIT.
@@ -217,7 +231,7 @@ fun <State> createStore(
217231
// the initial state tree.
218232
dispatch(ActionTypes.INIT)
219233

220-
return object: Store<State> {
234+
return object : Store<State> {
221235
override val getState = ::getState
222236
override var dispatch: Dispatcher = ::dispatch
223237
override val subscribe = ::subscribe
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.reduxkotlin.utils
2+
3+
const val UNKNOWN_THREAD_NAME = "UNKNOWN_THREAD_NAME"
4+
5+
/**
6+
* Returns the name of the current thread.
7+
*/
8+
expect fun getThreadName(): String
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.reduxkotlin.utils
2+
3+
/**
4+
* Fallback for platforms that have not been implemented yet.
5+
* This will allow usage of ReduxKotlin, but not allow
6+
* thread enforcement.
7+
* Linux, Win, WASM
8+
*/
9+
actual fun getThreadName(): String = UNKNOWN_THREAD_NAME
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package org.reduxkotlin.utils
2+
import platform.Foundation.NSThread.Companion.currentThread
3+
4+
actual fun getThreadName(): String = currentThread.name ?: UNKNOWN_THREAD_NAME
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package org.reduxkotlin.utils
2+
3+
actual fun getThreadName() = "main"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package org.reduxkotlin.utils
2+
3+
actual fun getThreadName(): String = Thread.currentThread().name
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package org.reduxkotlin.util
2+
3+
import org.reduxkotlin.*
4+
import org.spekframework.spek2.Spek
5+
import org.spekframework.spek2.style.specification.describe
6+
import java.util.concurrent.CountDownLatch
7+
import kotlin.IllegalStateException
8+
import kotlin.test.assertNotNull
9+
import kotlin.test.assertNull
10+
11+
object ThreadUtilSpec : Spek({
12+
describe("createStore") {
13+
val store = createStore(
14+
todos, TestState(
15+
listOf(
16+
Todo(
17+
id = "1",
18+
text = "Hello"
19+
)
20+
)
21+
)
22+
)
23+
24+
it("ensure same thread on getState") {
25+
ensureSameThread { store.getState() }
26+
}
27+
it("ensure same thread on dispatch") {
28+
ensureSameThread { store.dispatch(Any()) }
29+
}
30+
it("ensure same thread on replaceReducer") {
31+
ensureSameThread { store.replaceReducer { state, action -> state } }
32+
}
33+
it("ensure same thread on subscribe") {
34+
ensureSameThread { store.subscribe { } }
35+
}
36+
}
37+
})
38+
39+
private fun ensureSameThread(getState: () -> Any) {
40+
val latch = CountDownLatch(1)
41+
var exception: java.lang.IllegalStateException? = null
42+
var state: Any? = null
43+
44+
val newThread = Thread {
45+
state = getState()
46+
}
47+
48+
newThread.setUncaughtExceptionHandler { thread, throwable ->
49+
exception = throwable as IllegalStateException
50+
latch.countDown()
51+
}
52+
newThread.start()
53+
54+
latch.await()
55+
56+
assertNotNull(exception)
57+
assertNull(state)
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package org.reduxkotlin.utils
2+
3+
import platform.Foundation.NSThread.Companion.currentThread
4+
5+
actual fun getThreadName(): String = currentThread.name ?: UNKNOWN_THREAD_NAME

website/docs/api/Store.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ The only way to change the state inside it is to dispatch an [action](../Glossar
1212

1313
A store is just a plain object that contains your current state and a 4 functions.
1414
To create it, pass your root [reducing function](../Glossary.md#reducer) to
15-
[`createStore`](createStore.md).
15+
[`createStore`](createStore.md). The store has [same thread enforcement](../introduction/threading), meaning
16+
its methods must be called from the same thread where the store was created.
1617

1718
> ##### A Note for Flux Users
1819
>

website/docs/introduction/Ecosystem.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ implement features and solve problems in your application.
1414

1515
ReduxKotlin ecosystem is currently very small. This list will be updated as needed.
1616

17-
For inspiration or examples, the JS ecosystem is quite rich and (can be explored
18-
here)[https://redux.js.org/introduction/ecosystem].
17+
For inspiration or examples, the JS ecosystem is quite rich and [can be explored
18+
here](https://redux.js.org/introduction/ecosystem).
1919

2020
**[Presenter-middleware](https://github.com/reduxkotlin/presenter-middleware)**
2121
A middleware for writing concise UI binding code and no-fuss lifecycle/subscription management.

website/docs/introduction/GettingStarted.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ kotlin {
3333
sourceSets {
3434
commonMain {
3535
dependencies {
36-
implementation "org.reduxkotlin:redux-kotlin:0.2.9"
36+
implementation "org.reduxkotlin:redux-kotlin:0.3.0"
3737
}
3838
}
3939
}
@@ -47,7 +47,7 @@ __For single platform project (i.e. just Android):__
4747

4848
```groovy
4949
dependencies {
50-
implementation "org.reduxkotlin:redux-kotlin:0.2.9"
50+
implementation "org.reduxkotlin:redux-kotlin:0.3.0"
5151
}
5252
```
5353

website/docs/introduction/Motivation.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ sharing code between Android, iOS, web.
1616

1717
Redux as a pattern for sharing code between platforms looks promising, especially in the future with
1818
Jetpack Compose and SwiftUI. For an example of shared code in a SwiftUI app look at the
19-
(MovieSwiftUI-Kotlin)[https://github.com/reduxkotlin/MovieSwiftUI-Kotlin] example.
19+
[MovieSwiftUI-Kotlin](https://github.com/reduxkotlin/MovieSwiftUI-Kotlin) example.
2020

website/docs/introduction/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
- [Motivation](Motivation.md)
44
- [Core Concepts](CoreConcepts.md)
55
- [Three Principles](ThreePrinciples.md)
6+
- [Threading](Threading.md)
67
- [Learning Resources](LearningResources.md)
78
- [Ecosystem](Ecosystem.md)
89
- [Examples](Examples.md)
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
id: threading
3+
title: Redux on Multi-threaded Platforms
4+
sidebar_label: Threading
5+
hide_title: true
6+
---
7+
8+
# Redux on Multi-threaded Platforms
9+
10+
Redux in multi-threaded environments brings additional concerns that are not present in redux
11+
for Javascript. Javascript is single threaded, so Redux.js did not have to address the issue.
12+
Android, iOS, and native do have threading, and as such attention must be paid to which threads interact with the store.
13+
14+
As of ReduxKotlin 0.3.0 there is `same thread enforcement` for the [getState](../api/store#getstate-_or_-state-property), [dispatch](../api/store#dispatchaction-any-any), [replaceReducer](../api/store#replacereducernextreducer-reducer-state-unit),
15+
and [subscribe](../api/store#subscribelistener-storesubscriber) functions on the store. This means these methods must be called from the same thread where
16+
the store was created. An `IllegalStateException` will be thrown if one of these are called from a
17+
different thread.
18+
19+
If this `same thread enforcement` was not in place invalid states and race conditions could happen.
20+
For example if `getState` was called at the same time as a dispatch, the state would represent a past
21+
state. Or 2 actions dispatched concurrently could cause an invalid state.
22+
23+
Note that this is __SAME__ thread enforcement - not __MAIN__ thread enforcement. ReduxKotlin does not
24+
force you to use the main thread, although you can if you'd like. Most mobile applications do redux on the main
25+
thread, however it could be moved to a background thread. Using a background thread could be desirable
26+
if the reducers & middleware processing produce UI effects such as dropped frames.
27+
28+
29+
Currently `same thread enforcement` is implemented for JVM, iOS, & macOS. The other platforms
30+
have do not have the enforcement in place yet.

website/i18n/en.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"localized-strings": {
44
"next": "Next",
55
"previous": "Previous",
6-
"tagline": "A Predictable State Container for Kotlin Apps",
6+
"tagline": "Redux in Kotlin supporting multiplatform - Android, iOS, Web, Native",
77
"docs": {
88
"advanced/async-actions": {
99
"title": "Async Actions",
@@ -116,6 +116,10 @@
116116
"introduction/README": {
117117
"title": "introduction/README"
118118
},
119+
"introduction/threading": {
120+
"title": "Redux on Multi-threaded Platforms",
121+
"sidebar_label": "Threading"
122+
},
119123
"introduction/three-principles": {
120124
"title": "Three Principles",
121125
"sidebar_label": "Three Principles"

website/sidebars.json

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"introduction/motivation",
66
"introduction/core-concepts",
77
"introduction/three-principles",
8+
"introduction/threading",
89
"introduction/learning-resources",
910
"introduction/ecosystem",
1011
"introduction/examples"

0 commit comments

Comments
 (0)