Skip to content

Commit 0189780

Browse files
authored
Merge pull request #174 from yumemi-inc/codex/pending-action-policy
Add pending action policy and clear pending actions API
2 parents db39e2b + 956a05d commit 0189780

7 files changed

Lines changed: 216 additions & 28 deletions

File tree

README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ It keeps surrounding helper layers intentionally small, so dependencies and feat
5555
- [Multiple states and transitions](#multiple-states-and-transitions)
5656
- [Error handling](#error-handling)
5757
- [Asynchronous Work](#asynchronous-work)
58-
- [Cancel Pending Actions](#cancel-pending-actions)
58+
- [Clear Pending Actions](#clear-pending-actions)
5959
- [Alternative DSL Forms](#alternative-dsl-forms)
6060
- [Specifying coroutineContext](#specifying-coroutinecontext)
6161
- [Specifying CoroutineDispatchers](#specifying-coroutinedispatchers)
@@ -418,19 +418,21 @@ This pattern lets your *Store* react to external data changes automatically, suc
418418
Coroutines started by `launch{}` are automatically cancelled when the *State* changes to a different *State*, making it easy to manage resources and subscriptions.
419419
In `action{}`, `launch{}` is tied to the *State* active at action start.
420420
421-
### Cancel Pending Actions
421+
### Clear Pending Actions
422422
423-
If you need to discard already queued `dispatch()` calls at a specific point, call `cancelPendingActions()` inside `enter{}`, `action{}`, `exit{}`, `error{}`, or inside `transaction{}` from a launched coroutine.
423+
By default, Tart clears already queued actions when the store exits the current state and enters a different state variant.
424+
To keep queued actions across state exits, set `pendingActionPolicy(PendingActionPolicy.KEEP)`.
424425
425426
```kt
426-
state<MyState.Active> {
427-
action<MyAction.Finish> {
428-
cancelPendingActions()
429-
nextState(MyState.Done)
430-
}
427+
val store = Store<MyState, MyAction, MyEvent>(MyState.Initial) {
428+
// ...
429+
430+
pendingActionPolicy(PendingActionPolicy.KEEP)
431431
}
432432
```
433433
434+
Regardless of the configured `PendingActionPolicy`, you can still discard already queued actions at a specific point by calling `clearPendingActions()` inside `enter{}`, `action{}`, `exit{}`, `error{}`, or inside `transaction{}` from a launched coroutine.
435+
434436
### Alternative DSL Forms
435437
436438
Some DSL APIs are just alternative forms of existing APIs:
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.yumemi.tart.core
2+
3+
/**
4+
* Controls how queued actions are handled when the store exits the current state.
5+
*/
6+
enum class PendingActionPolicy {
7+
/**
8+
* Clears queued actions after a transition to a different state variant is committed.
9+
* The currently running store work keeps running.
10+
*/
11+
CLEAR_ON_STATE_EXIT,
12+
13+
/**
14+
* Keeps queued actions unless they are cleared explicitly from DSL scopes.
15+
*/
16+
KEEP,
17+
}

tart-core/src/commonMain/kotlin/io/yumemi/tart/core/StoreBuilder.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class StoreBuilder<S : State, A : Action, E : Event> internal constructor() {
1313
private var storeCoroutineContext: CoroutineContext = EmptyCoroutineContext + Dispatchers.Default
1414
private var storeStateSaver: StateSaver<S> = StateSaver.Noop()
1515
private var storeExceptionHandler: ExceptionHandler = ExceptionHandler.Unhandled
16+
private var storePendingActionPolicy: PendingActionPolicy = PendingActionPolicy.CLEAR_ON_STATE_EXIT
1617
private var storeMiddlewares: MutableList<Middleware<S, A, E>> = mutableListOf()
1718

1819
/**
@@ -51,6 +52,15 @@ class StoreBuilder<S : State, A : Action, E : Event> internal constructor() {
5152
storeExceptionHandler = exceptionHandler
5253
}
5354

55+
/**
56+
* Sets how queued actions are handled when the store exits the current state.
57+
*
58+
* @param policy The pending action policy to use
59+
*/
60+
fun pendingActionPolicy(policy: PendingActionPolicy) {
61+
storePendingActionPolicy = policy
62+
}
63+
5464
/**
5565
* Adds a single middleware instance to the store.
5666
*
@@ -342,6 +352,7 @@ class StoreBuilder<S : State, A : Action, E : Event> internal constructor() {
342352
override val coroutineContext: CoroutineContext = storeCoroutineContext
343353
override val stateSaver: StateSaver<S> = storeStateSaver
344354
override val exceptionHandler: ExceptionHandler = storeExceptionHandler
355+
override val pendingActionPolicy: PendingActionPolicy = storePendingActionPolicy
345356
override val middlewares: List<Middleware<S, A, E>> = storeMiddlewares
346357
override val onEnter: suspend EnterScope<S, A, E, S>.() -> Unit = this@StoreBuilder.onEnter
347358
override val onAction: suspend ActionScope<S, A, E, S>.() -> Unit = this@StoreBuilder.onAction

tart-core/src/commonMain/kotlin/io/yumemi/tart/core/StoreImpl.kt

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ internal abstract class StoreImpl<S : State, A : Action, E : Event> : Store<S, A
6868

6969
protected abstract val exceptionHandler: ExceptionHandler
7070

71+
protected abstract val pendingActionPolicy: PendingActionPolicy
72+
7173
protected abstract val middlewares: List<Middleware<S, A, E>>
7274

7375
protected abstract val onEnter: suspend EnterScope<S, A, E, S>.() -> Unit
@@ -169,6 +171,9 @@ internal abstract class StoreImpl<S : State, A : Action, E : Event> : Store<S, A
169171

170172
if (state != nextState) {
171173
processStateChange(state, nextState)
174+
if (state::class != nextState::class) {
175+
clearPendingActionsOnStateExitIfNeeded()
176+
}
172177
}
173178

174179
if (state::class != nextState::class) {
@@ -188,6 +193,9 @@ internal abstract class StoreImpl<S : State, A : Action, E : Event> : Store<S, A
188193

189194
if (state != nextState) {
190195
processStateChange(state, nextState)
196+
if (state::class != nextState::class) {
197+
clearPendingActionsOnStateExitIfNeeded()
198+
}
191199
}
192200

193201
if (state::class != nextState::class) {
@@ -209,6 +217,9 @@ internal abstract class StoreImpl<S : State, A : Action, E : Event> : Store<S, A
209217

210218
if (state != nextState) {
211219
processStateChange(state, nextState)
220+
if (state::class != nextState::class) {
221+
clearPendingActionsOnStateExitIfNeeded()
222+
}
212223
}
213224

214225
if (state::class != nextState::class) {
@@ -233,6 +244,9 @@ internal abstract class StoreImpl<S : State, A : Action, E : Event> : Store<S, A
233244

234245
if (state != nextState) {
235246
processStateChange(state, nextState)
247+
if (state::class != nextState::class) {
248+
clearPendingActionsOnStateExitIfNeeded()
249+
}
236250
}
237251

238252
if (state::class != nextState::class) {
@@ -259,7 +273,7 @@ internal abstract class StoreImpl<S : State, A : Action, E : Event> : Store<S, A
259273
newState = block()
260274
}
261275

262-
override fun cancelPendingActions() {
276+
override fun clearPendingActions() {
263277
clearPendingDispatchJobs()
264278
}
265279

@@ -300,7 +314,7 @@ internal abstract class StoreImpl<S : State, A : Action, E : Event> : Store<S, A
300314
newState = block()
301315
}
302316

303-
override fun cancelPendingActions() {
317+
override fun clearPendingActions() {
304318
clearPendingDispatchJobs()
305319
}
306320

@@ -372,7 +386,7 @@ internal abstract class StoreImpl<S : State, A : Action, E : Event> : Store<S, A
372386
newState = block()
373387
}
374388

375-
override fun cancelPendingActions() {
389+
override fun clearPendingActions() {
376390
clearPendingDispatchJobs()
377391
}
378392

@@ -427,7 +441,7 @@ internal abstract class StoreImpl<S : State, A : Action, E : Event> : Store<S, A
427441
newState = block()
428442
}
429443

430-
override fun cancelPendingActions() {
444+
override fun clearPendingActions() {
431445
clearPendingDispatchJobs()
432446
}
433447

@@ -463,7 +477,7 @@ internal abstract class StoreImpl<S : State, A : Action, E : Event> : Store<S, A
463477
object : ExitScope<S, E, S> {
464478
override val state = state
465479

466-
override fun cancelPendingActions() {
480+
override fun clearPendingActions() {
467481
clearPendingDispatchJobs()
468482
}
469483

@@ -506,7 +520,7 @@ internal abstract class StoreImpl<S : State, A : Action, E : Event> : Store<S, A
506520
newState = block()
507521
}
508522

509-
override fun cancelPendingActions() {
523+
override fun clearPendingActions() {
510524
clearPendingDispatchJobs()
511525
}
512526

@@ -526,6 +540,12 @@ internal abstract class StoreImpl<S : State, A : Action, E : Event> : Store<S, A
526540
processMiddleware { afterEventEmit(state, event) }
527541
}
528542

543+
private fun clearPendingActionsOnStateExitIfNeeded() {
544+
if (pendingActionPolicy == PendingActionPolicy.CLEAR_ON_STATE_EXIT) {
545+
clearPendingDispatchJobs()
546+
}
547+
}
548+
529549
private fun clearPendingDispatchJobs() {
530550
val currentJob = activeDispatchJob
531551
val dispatchScopeJob = dispatchScope.coroutineContext[Job] ?: return

tart-core/src/commonMain/kotlin/io/yumemi/tart/core/StoreScope.kt

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ interface EnterScope<S : State, A : Action, E : Event, S2 : S> : StoreScope {
3737
fun nextStateBy(block: () -> S)
3838

3939
/**
40-
* Cancels actions that are already queued behind the currently executing store work.
40+
* Clears actions that are already queued behind the currently executing store work.
4141
* The action/transaction currently in progress keeps running.
4242
*/
43-
fun cancelPendingActions()
43+
fun clearPendingActions()
4444

4545
/**
4646
* Emits an event from the enter handler.
@@ -118,10 +118,10 @@ interface EnterScope<S : State, A : Action, E : Event, S2 : S> : StoreScope {
118118
fun nextStateBy(block: () -> S)
119119

120120
/**
121-
* Cancels actions that are already queued behind the currently executing store work.
121+
* Clears actions that are already queued behind the currently executing store work.
122122
* The action/transaction currently in progress keeps running.
123123
*/
124-
fun cancelPendingActions()
124+
fun clearPendingActions()
125125

126126
/**
127127
* Emits an event from the transaction.
@@ -146,10 +146,10 @@ interface ExitScope<S : State, E : Event, S2 : S> : StoreScope {
146146
val state: S2
147147

148148
/**
149-
* Cancels actions that are already queued behind the currently executing store work.
149+
* Clears actions that are already queued behind the currently executing store work.
150150
* The action/transaction currently in progress keeps running.
151151
*/
152-
fun cancelPendingActions()
152+
fun clearPendingActions()
153153

154154
/**
155155
* Emits an event from the exit handler.
@@ -193,10 +193,10 @@ interface ActionScope<S : State, A : Action, E : Event, S2 : S> : StoreScope {
193193
fun nextStateBy(block: () -> S)
194194

195195
/**
196-
* Cancels actions that are already queued behind the currently executing store work.
196+
* Clears actions that are already queued behind the currently executing store work.
197197
* The action/transaction currently in progress keeps running.
198198
*/
199-
fun cancelPendingActions()
199+
fun clearPendingActions()
200200

201201
/**
202202
* Emits an event from the action handler.
@@ -284,10 +284,10 @@ interface ActionScope<S : State, A : Action, E : Event, S2 : S> : StoreScope {
284284
fun nextStateBy(block: () -> S)
285285

286286
/**
287-
* Cancels actions that are already queued behind the currently executing store work.
287+
* Clears actions that are already queued behind the currently executing store work.
288288
* The action/transaction currently in progress keeps running.
289289
*/
290-
fun cancelPendingActions()
290+
fun clearPendingActions()
291291

292292
/**
293293
* Emits an event from the transaction.
@@ -333,10 +333,10 @@ interface ErrorScope<S : State, E : Event, S2 : S, T : Throwable> : StoreScope {
333333
fun nextStateBy(block: () -> S)
334334

335335
/**
336-
* Cancels actions that are already queued behind the currently executing store work.
336+
* Clears actions that are already queued behind the currently executing store work.
337337
* The action/transaction currently in progress keeps running.
338338
*/
339-
fun cancelPendingActions()
339+
fun clearPendingActions()
340340

341341
/**
342342
* Emits an event from the error handler.

tart-core/src/commonTest/kotlin/io/yumemi/tart/core/StorePendingActionCancellationTest.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,13 @@ class StorePendingActionCancellationTest {
3333
): Store<AppState, AppAction, Nothing> {
3434
return Store(AppState.Active()) {
3535
coroutineContext(testDispatcher)
36+
pendingActionPolicy(PendingActionPolicy.KEEP)
3637

3738
state<AppState.Active> {
3839
action<AppAction.HoldAndCancel>(testDispatcher) {
3940
onHoldAndCancelStarted?.complete(Unit)
4041
delay(100)
41-
cancelPendingActions()
42+
clearPendingActions()
4243
onHoldAndCancelCompleted?.complete(Unit)
4344
nextState(state.copy(value = state.value + 100))
4445
}
@@ -48,7 +49,7 @@ class StorePendingActionCancellationTest {
4849
transaction(testDispatcher) {
4950
onTransactionStarted?.complete(Unit)
5051
delay(100)
51-
cancelPendingActions()
52+
clearPendingActions()
5253
onTransactionCompleted?.complete(Unit)
5354
nextState(state.copy(value = state.value + 100))
5455
}

0 commit comments

Comments
 (0)