Skip to content

Commit

Permalink
Merge pull request #36 from respawn-app/2.2.2-rc
Browse files Browse the repository at this point in the history
2.2.2-rc
  • Loading branch information
Nek-12 authored Dec 21, 2023
2 parents 78601c6 + 244c297 commit 5c306c8
Show file tree
Hide file tree
Showing 22 changed files with 238 additions and 173 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:Suppress("DEPRECATION")

package pro.respawn.flowmvi.android.compose.preview

import androidx.compose.runtime.Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ sealed interface CounterIntent : MVIIntent {

sealed interface CounterAction : MVIAction {

data object ShowErrorMessage : CounterAction
data class ShowErrorMessage(val message: String?) : CounterAction
data object ShowLambdaMessage : CounterAction
data object GoBack : CounterAction
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.koin.core.parameter.parametersOf
import pro.respawn.flowmvi.api.IntentReceiver
import pro.respawn.flowmvi.compose.preview.StateProvider
import pro.respawn.flowmvi.compose.dsl.subscribe
import pro.respawn.flowmvi.compose.preview.EmptyReceiver
import pro.respawn.flowmvi.compose.preview.StateProvider
import pro.respawn.flowmvi.sample.CounterAction.GoBack
import pro.respawn.flowmvi.sample.CounterAction.ShowErrorMessage
import pro.respawn.flowmvi.sample.CounterAction.ShowLambdaMessage
Expand Down Expand Up @@ -57,7 +57,7 @@ fun ComposeScreen(onBack: () -> Unit) {
// consume() block will only be called when a new action is emitted (independent of recompositions)
when (action) {
is ShowLambdaMessage -> scaffoldState.snackbar(context.getString(R.string.lambda_message))
is ShowErrorMessage -> scaffoldState.snackbar(context.getString(R.string.error_message))
is ShowErrorMessage -> scaffoldState.snackbar(context.getString(R.string.error_message, action.message))
is GoBack -> onBack()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,10 @@ class CounterContainer(
)
val undoRedo = undoRedo(10)
recover {
launch {
if (it is IllegalArgumentException)
action(ShowErrorMessage)
else updateState {
CounterState.Error(it)
}
if (it is IllegalArgumentException)
action(ShowErrorMessage(it.message))
else updateState {
CounterState.Error(it)
}
null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ class CounterActivity :
// Handle any side effects of the UI layer here
override fun consume(action: CounterAction) {
when (action) {
is ShowErrorMessage -> Snackbar.make(binding.root, R.string.error_message, Snackbar.LENGTH_SHORT).show()
is ShowErrorMessage -> Snackbar.make(
binding.root,
getString(R.string.error_message, action.message),
Snackbar.LENGTH_SHORT
).show()
is ShowLambdaMessage -> Snackbar.make(binding.root, R.string.lambda_message, Snackbar.LENGTH_SHORT).show()
is CounterAction.GoBack -> finish()
}
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<resources>
<string name="app_name">FlowMVI Sample</string>
<string name="error_message">onException invoked</string>
<string name="error_message">recovered from error: %1$s</string>
<string name="counter_button_label">Increment counter</string>
<string name="counter_template">Counter: %d</string>
<string name="started_processing">Started processing your intent</string>
Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ object Config {

const val majorRelease = 2
const val minorRelease = 2
const val patch = 1
const val patch = 2
const val postfix = "rc"
const val versionName = "$majorRelease.$minorRelease.$patch-$postfix"
const val url = "https://github.com/respawn-app/FlowMVI"
Expand Down
36 changes: 0 additions & 36 deletions core/src/commonMain/kotlin/pro/respawn/flowmvi/api/Recoverable.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package pro.respawn.flowmvi.api

/**
* An exception that has happened in the [Store] that cannot be recovered from.
* This is either an exception resulting from developer errors (such as unhandled intents),
* or an exception while trying to recover from another exception (which is prohibited).
* You may also use this to bypass store plugins handling this particular exception.
*/
public class UnrecoverableException(
override val cause: Exception? = null,
override val message: String? = null,
) : IllegalStateException(message, cause)
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@file:Suppress("FunctionName")

package pro.respawn.flowmvi.exceptions

import pro.respawn.flowmvi.api.UnrecoverableException

internal fun NonSuspendingSubscriberException() = UnrecoverableException(
message = """
You have subscribed to the store, but your subscribe() block has returned early (without throwing a
CancellationException). When you subscribe, make sure to continue collecting values from the store until the Job
Returned from the subscribe() is cancelled as you likely don't want to stop being subscribed to the store
(i.e. complete the subscription job on your own).
"""
.trimIndent(),
cause = null,
)

internal fun UnhandledIntentException() = UnrecoverableException(
message = """
An intent has not been handled after calling all plugins.
You likely don't want this to happen because intents are supposed to be acted upon.
Make sure you have at least one plugin that handles intents, such as reducePlugin().
"""
.trimIndent(),
cause = null,
)

internal fun RecursiveRecoverException(cause: Exception) = UnrecoverableException(
message = """
Recursive recover detected, which means you have thrown in a recover plugin or in the `onException` block.
Please never throw while recovering from exceptions, or that will result in an infinite loop.
""".trimIndent(),
cause = cause
)

internal fun UnhandledStoreException(cause: Exception) = UnrecoverableException(
message = """
Store has run all its plugins (exception handlers) but the exception was not handled by any of them.
""".trimIndent(),
cause = cause,
)
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ import pro.respawn.flowmvi.api.ActionShareBehavior
import pro.respawn.flowmvi.api.DelicateStoreApi
import pro.respawn.flowmvi.api.MVIAction

internal fun <A : MVIAction> actionModule(
behavior: ActionShareBehavior,
): ActionModule<A> = when (behavior) {
is ActionShareBehavior.Distribute -> DistributingModule(behavior.buffer, behavior.overflow)
is ActionShareBehavior.Restrict -> ConsumingModule(behavior.buffer, behavior.overflow)
is ActionShareBehavior.Share -> SharedModule(behavior.replay, behavior.buffer, behavior.overflow)
is ActionShareBehavior.Disabled -> ThrowingModule()
}

internal interface ActionModule<A : MVIAction> : ActionProvider<A>, ActionReceiver<A>

internal abstract class ChannelActionModule<A : MVIAction>(
Expand Down Expand Up @@ -80,12 +89,3 @@ internal class ThrowingModule<A : MVIAction> : ActionModule<A> {
private const val ActionsDisabledMessage = "Actions are disabled for this store"
}
}

internal fun <A : MVIAction> actionModule(
behavior: ActionShareBehavior,
): ActionModule<A> = when (behavior) {
is ActionShareBehavior.Distribute -> DistributingModule(behavior.buffer, behavior.overflow)
is ActionShareBehavior.Restrict -> ConsumingModule(behavior.buffer, behavior.overflow)
is ActionShareBehavior.Share -> SharedModule(behavior.replay, behavior.buffer, behavior.overflow)
is ActionShareBehavior.Disabled -> ThrowingModule()
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ import kotlinx.coroutines.yield
import pro.respawn.flowmvi.api.IntentReceiver
import pro.respawn.flowmvi.api.MVIIntent

internal interface IntentModule<I : MVIIntent> : IntentReceiver<I> {

suspend fun awaitIntents(onIntent: suspend (intent: I) -> Unit)
}

internal fun <I : MVIIntent> intentModule(
parallel: Boolean,
capacity: Int,
overflow: BufferOverflow,
): IntentModule<I> = IntentModuleImpl(parallel, capacity, overflow)

internal interface IntentModule<I : MVIIntent> : IntentReceiver<I> {

suspend fun awaitIntents(onIntent: suspend (intent: I) -> Unit)
}

private class IntentModuleImpl<I : MVIIntent>(
private val parallel: Boolean,
capacity: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import pro.respawn.flowmvi.api.MVIAction
import pro.respawn.flowmvi.api.MVIIntent
import pro.respawn.flowmvi.api.MVIState
import pro.respawn.flowmvi.api.PipelineContext
import pro.respawn.flowmvi.api.Recoverable
import pro.respawn.flowmvi.api.StateReceiver
import kotlin.coroutines.CoroutineContext

Expand All @@ -39,12 +38,13 @@ internal inline fun <S : MVIState, I : MVIIntent, A : MVIAction, T> T.launchPipe
crossinline onAction: suspend PipelineContext<S, I, A>.(action: A) -> Unit,
crossinline onTransformState: suspend PipelineContext<S, I, A>.(transform: suspend S.() -> S) -> Unit,
onStart: PipelineContext<S, I, A>.() -> Unit,
): Job where T : IntentReceiver<I>, T : StateReceiver<S>, T : Recoverable<S, I, A> = object :
): Job where T : IntentReceiver<I>, T : StateReceiver<S>, T : RecoverModule<S, I, A> = object :
IntentReceiver<I> by this,
StateReceiver<S> by this,
PipelineContext<S, I, A>,
ActionReceiver<A> {

override fun toString(): String = "${name.orEmpty()}PipelineContext"
override val key = PipelineContext // recoverable should be separate.
private val job = SupervisorJob(parent.coroutineContext[Job]).apply {
invokeOnCompletion {
Expand All @@ -55,8 +55,8 @@ internal inline fun <S : MVIState, I : MVIIntent, A : MVIAction, T> T.launchPipe
}
}
}
private val handler = PipelineExceptionHandler(this)
private val pipelineName = CoroutineName("${name.orEmpty()}PipelineContext")
private val handler = PipelineExceptionHandler()
private val pipelineName = CoroutineName(toString())
override val coroutineContext: CoroutineContext = parent.coroutineContext + pipelineName + job + handler + this
override suspend fun updateState(transform: suspend S.() -> S) = onTransformState(transform)
override suspend fun action(action: A) = onAction(action)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import pro.respawn.flowmvi.api.PipelineContext
import pro.respawn.flowmvi.api.StorePlugin
import pro.respawn.flowmvi.plugins.AbstractStorePlugin

internal class PluginModule<S : MVIState, I : MVIIntent, A : MVIAction>(
internal fun <S : MVIState, I : MVIIntent, A : MVIAction> pluginModule(
plugins: Set<StorePlugin<S, I, A>>
): StorePlugin<S, I, A> = PluginModule(plugins)

private class PluginModule<S : MVIState, I : MVIIntent, A : MVIAction>(
private val plugins: Set<StorePlugin<S, I, A>>,
) : AbstractStorePlugin<S, I, A>(null) {

Expand All @@ -30,7 +34,3 @@ internal class PluginModule<S : MVIState, I : MVIIntent, A : MVIAction>(
block: StorePlugin<S, I, A>.(R) -> R?
) = plugins.fold<_, R?>(initial) { acc, it -> it.block(acc ?: return@plugins acc) }
}

internal fun <S : MVIState, I : MVIIntent, A : MVIAction> pluginModule(
plugins: Set<StorePlugin<S, I, A>>
): StorePlugin<S, I, A> = PluginModule(plugins)
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package pro.respawn.flowmvi.modules

import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import pro.respawn.flowmvi.api.MVIAction
import pro.respawn.flowmvi.api.MVIIntent
import pro.respawn.flowmvi.api.MVIState
import pro.respawn.flowmvi.api.PipelineContext
import pro.respawn.flowmvi.api.Store
import pro.respawn.flowmvi.api.UnrecoverableException
import pro.respawn.flowmvi.exceptions.RecursiveRecoverException
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.coroutineContext

/**
* An entity that can [recover] from exceptions happening during its lifecycle. Most often, a [Store]
*/
@Suppress("FUN_INTERFACE_WITH_SUSPEND_FUNCTION") // https://youtrack.jetbrains.com/issue/KTIJ-7642
internal fun interface RecoverModule<S : MVIState, I : MVIIntent, A : MVIAction> : CoroutineContext.Element {

override val key: CoroutineContext.Key<*> get() = RecoverModule

/**
* Recover from an exception in the given context.
*/
suspend fun PipelineContext<S, I, A>.recover(e: Exception)

/**
* Run [block] catching any exceptions and invoking [recover]. This will add this [RecoverModule] key to the coroutine
* context of the [recover] block.
*/
suspend fun PipelineContext<S, I, A>.catch(block: suspend () -> Unit): Unit = try {
withContext(this@RecoverModule) { block() }
} catch (expected: Exception) {
when {
expected is CancellationException || expected is UnrecoverableException -> throw expected
alreadyRecovered() -> throw RecursiveRecoverException(expected)
else -> recover(expected)
}
}

@Suppress("FunctionName")
fun PipelineContext<S, I, A>.PipelineExceptionHandler() = CoroutineExceptionHandler { ctx, e ->
when {
e !is Exception || e is CancellationException -> throw e
e is UnrecoverableException -> throw e.unwrapRecursion()
ctx.alreadyRecovered -> throw e
// add Recoverable to the coroutine context
// and handle the exception asynchronously to allow suspending inside recover
// Do NOT use the "ctx" parameter here, as that coroutine context is already invalid and will not launch
else -> launch(this@RecoverModule) { recover(e) }.invokeOnCompletion { cause ->
if (cause != null && cause !is CancellationException) throw cause
}
}
}

companion object : CoroutineContext.Key<RecoverModule<*, *, *>>
}

private tailrec fun UnrecoverableException.unwrapRecursion(): Exception = when (val cause = cause) {
null -> this
this -> this // cause is the same exception
is UnrecoverableException -> cause.unwrapRecursion()
is CancellationException -> throw cause
else -> cause
}

internal suspend fun alreadyRecovered() = coroutineContext.alreadyRecovered

private val CoroutineContext.alreadyRecovered get() = this[RecoverModule] != null

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import pro.respawn.flowmvi.api.StateProvider
import pro.respawn.flowmvi.api.StateReceiver
import pro.respawn.flowmvi.util.withReentrantLock

internal interface StateModule<S : MVIState> : StateReceiver<S>, StateProvider<S>

internal fun <S : MVIState> stateModule(initial: S): StateModule<S> = StateModuleImpl(initial)

internal interface StateModule<S : MVIState> : StateReceiver<S>, StateProvider<S>

private class StateModuleImpl<S : MVIState>(initial: S) : StateModule<S> {

private val _states = MutableStateFlow(initial)
Expand Down
Loading

0 comments on commit 5c306c8

Please sign in to comment.