generated from yumemi-inc/multiplatform-library-template
-
Notifications
You must be signed in to change notification settings - Fork 2
Closed
Description
Summary
Add an optional policy to control what happens to queued dispatch calls when the state class changes.
Motivation
Different apps need different behavior:
- Keep queued actions (current behavior)
- Drop stale queued actions on state-class transition
Making either behavior unconditional is problematic, so this should be configurable.
Proposed API
// tart-core/src/commonMain/kotlin/io/yumemi/tart/core/PendingDispatchPolicy.kt
enum class PendingDispatchPolicy {
KEEP_ALL,
DROP_ON_STATE_CLASS_TRANSITION,
}
// tart-core/src/commonMain/kotlin/io/yumemi/tart/core/StoreBuilder.kt
@TartStoreDsl
class StoreBuilder<S : State, A : Action, E : Event> {
fun pendingDispatchPolicy(policy: PendingDispatchPolicy)
}Semantics
-
KEEP_ALL:- Preserve current behavior
- Do not cancel queued
dispatchjobs
-
DROP_ON_STATE_CLASS_TRANSITION:- Only when
state::class != nextState::class, cancel queueddispatchjobs - Keep the currently running
dispatchalive - Limit cancellation target to
dispatchscope children only (do not affect collectors or state-enter scopes)
- Only when
Example
val store = Store<AppState, AppAction, AppEvent>(AppState.Initial) {
pendingDispatchPolicy(PendingDispatchPolicy.DROP_ON_STATE_CLASS_TRANSITION)
state<AppState.Initial> {
action<AppAction.Start> { nextState(AppState.Loading) }
}
state<AppState.Loading> {
action<AppAction.Done> { nextState(AppState.Main) }
}
}Test Plan
- Default (
KEEP_ALL) keeps queued dispatches DROP_ON_STATE_CLASS_TRANSITIONdrops queued dispatches on class transition- Same-state-class updates do not drop queued dispatches
- Running dispatch is not cancelled mid-transition
StoreImpl Changes (Concrete Example)
internal abstract class StoreImpl<S : State, A : Action, E : Event> : Store<S, A, E> {
protected abstract val pendingDispatchPolicy: PendingDispatchPolicy
private val dispatchScope by lazy {
CoroutineScope(coroutineScope.coroutineContext + SupervisorJob(coroutineScope.coroutineContext[Job]))
}
final override fun dispatch(action: A) {
dispatchScope.launch {
mutex.withLock {
initializeIfNeeded()
onActionDispatched(currentState, action)
}
}
}
private suspend fun onActionDispatched(state: S, action: A) {
val nextState = processActionDispatch(state, action)
if (state::class != nextState::class) {
maybeClearPendingDispatchJobs(state, nextState)
processStateExit(state)
}
if (state != nextState) processStateChange(state, nextState)
if (state::class != nextState::class) onStateEntered(nextState)
}
private suspend fun onStateChanged(state: S, nextState: S) {
if (state::class != nextState::class) {
maybeClearPendingDispatchJobs(state, nextState)
processStateExit(state)
}
if (state != nextState) processStateChange(state, nextState)
if (state::class != nextState::class) onStateEntered(nextState)
}
private suspend fun onStateEntered(state: S, inErrorHandling: Boolean = false) {
val nextState = processStateEnter(state)
if (state::class != nextState::class) {
maybeClearPendingDispatchJobs(state, nextState)
processStateExit(state)
}
if (state != nextState) processStateChange(state, nextState)
if (state::class != nextState::class) onStateEntered(nextState, inErrorHandling)
}
private suspend fun onErrorOccurred(state: S, throwable: Throwable) {
val nextState = processError(state, throwable)
if (state::class != nextState::class) {
maybeClearPendingDispatchJobs(state, nextState)
processStateExit(state)
}
if (state != nextState) processStateChange(state, nextState)
if (state::class != nextState::class) onStateEntered(nextState, inErrorHandling = true)
}
private suspend fun maybeClearPendingDispatchJobs(state: S, nextState: S) {
val shouldClear = when (pendingDispatchPolicy) {
PendingDispatchPolicy.KEEP_ALL -> false
PendingDispatchPolicy.DROP_ON_STATE_CLASS_TRANSITION -> state::class != nextState::class
}
if (shouldClear) clearPendingDispatchJobs()
}
private suspend fun clearPendingDispatchJobs() {
val currentJob = currentCoroutineContext()[Job]
val dispatchScopeJob = dispatchScope.coroutineContext[Job] ?: return
dispatchScopeJob.children
.filter { it != currentJob && it.isActive }
.forEach { it.cancel() }
}
}Notes:
maybeClearPendingDispatchJobs(...)should be called at every state-class transition entry point (onActionDispatched,onStateChanged,onStateEntered,onErrorOccurred) so behavior is consistent regardless of transition source.clearPendingDispatchJobs()must only target children ofdispatchScope.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels