Skip to content

Commit

Permalink
Merge pull request #1221 from square/ray/give-me-convenience-or-give-…
Browse files Browse the repository at this point in the history
…me-death

`safeAction`, `safeEventHandler`
  • Loading branch information
rjrjr authored Sep 25, 2024
2 parents 5e1c32b + 7071aa5 commit 6395d0b
Show file tree
Hide file tree
Showing 4 changed files with 442 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import com.squareup.sample.gameworkflow.SyncState.SAVING
import com.squareup.workflow1.Snapshot
import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.Workflow
import com.squareup.workflow1.action
import com.squareup.workflow1.runningWorker
import com.squareup.workflow1.rx2.asWorker
import com.squareup.workflow1.ui.Screen
Expand Down Expand Up @@ -88,8 +87,12 @@ class RealRunGameWorkflow(
namePrompt = NewGameScreen(
renderState.defaultXName,
renderState.defaultOName,
onCancel = context.eventHandler { setOutput(CanceledStart) },
onStartGame = context.eventHandler { x, o -> state = Playing(PlayerInfo(x, o)) }
onCancel = context.safeEventHandler<NewGame> {
setOutput(CanceledStart)
},
onStartGame = context.safeEventHandler<NewGame, String, String> { _, x, o ->
state = Playing(PlayerInfo(x, o))
}
)
)
}
Expand Down Expand Up @@ -119,15 +122,11 @@ class RealRunGameWorkflow(
message = "Do you really want to concede the game?",
positive = "I Quit",
negative = "No",
confirmQuit = context.eventHandler {
(state as? MaybeQuitting)?.let { oldState ->
state = MaybeQuittingForSure(oldState.playerInfo, oldState.completedGame)
}
confirmQuit = context.safeEventHandler<MaybeQuitting> { oldState ->
state = MaybeQuittingForSure(oldState.playerInfo, oldState.completedGame)
},
continuePlaying = context.eventHandler {
(state as? MaybeQuitting)?.let { oldState ->
state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn)
}
continuePlaying = context.safeEventHandler<MaybeQuitting> { oldState ->
state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn)
}
)
)
Expand All @@ -142,15 +141,11 @@ class RealRunGameWorkflow(
message = "Really?",
positive = "Yes!!",
negative = "Sigh, no",
confirmQuit = context.eventHandler {
(state as? MaybeQuittingForSure)?.let { oldState ->
state = GameOver(oldState.playerInfo, oldState.completedGame)
}
confirmQuit = context.safeEventHandler<MaybeQuittingForSure> { oldState ->
state = GameOver(oldState.playerInfo, oldState.completedGame)
},
continuePlaying = context.eventHandler {
(state as? MaybeQuittingForSure)?.let { oldState ->
state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn)
}
continuePlaying = context.safeEventHandler<MaybeQuittingForSure> { oldState ->
state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn)
}
)
)
Expand All @@ -169,43 +164,37 @@ class RealRunGameWorkflow(
renderState,
onTrySaveAgain = context.trySaveAgain(),
onPlayAgain = context.playAgain(),
onExit = context.eventHandler { setOutput(FinishedPlaying) }
onExit = context.safeEventHandler<GameOver> { setOutput(FinishedPlaying) }
)
)
}
}

private fun stopPlaying(game: CompletedGame) = action {
val oldState = state as Playing
private fun stopPlaying(game: CompletedGame) = safeAction<Playing>("stopPlaying") { oldState ->
state = when (game.ending) {
Quitted -> MaybeQuitting(oldState.playerInfo, game)
else -> GameOver(oldState.playerInfo, game)
}
}

private fun handleLogGame(result: GameLog.LogResult) = action {
val oldState = state as GameOver
private fun handleLogGame(result: GameLog.LogResult) = safeAction<GameOver> { oldState ->
state = when (result) {
TRY_LATER -> oldState.copy(syncState = SAVE_FAILED)
LOGGED -> oldState.copy(syncState = SAVED)
}
}

private fun RenderContext.playAgain() = eventHandler {
(state as? GameOver)?.let { oldState ->
val (x, o) = oldState.playerInfo
state = NewGame(x, o)
}
private fun RenderContext.playAgain() = safeEventHandler<GameOver> { oldState ->
val (x, o) = oldState.playerInfo
state = NewGame(x, o)
}

private fun RenderContext.trySaveAgain() = eventHandler {
(state as? GameOver)?.let { oldState ->
check(oldState.syncState == SAVE_FAILED) {
"Should only fire trySaveAgain in syncState $SAVE_FAILED, " +
"was ${oldState.syncState}"
}
state = oldState.copy(syncState = SAVING)
private fun RenderContext.trySaveAgain() = safeEventHandler<GameOver> { oldState ->
check(oldState.syncState == SAVE_FAILED) {
"Should only fire trySaveAgain in syncState $SAVE_FAILED, " +
"was ${oldState.syncState}"
}
state = oldState.copy(syncState = SAVING)
}

override fun snapshotState(state: RunGameState): Snapshot = state.toSnapshot()
Expand Down
1 change: 1 addition & 0 deletions workflow-core/api/workflow-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ public final class com/squareup/workflow1/Snapshots {
public abstract class com/squareup/workflow1/StatefulWorkflow : com/squareup/workflow1/IdCacheable, com/squareup/workflow1/Workflow {
public fun <init> ()V
public final fun asStatefulWorkflow ()Lcom/squareup/workflow1/StatefulWorkflow;
public final fun defaultOnFailedCast (Ljava/lang/String;Lkotlin/reflect/KClass;Ljava/lang/Object;)V
public fun getCachedIdentifier ()Lcom/squareup/workflow1/WorkflowIdentifier;
public abstract fun initialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;)Ljava/lang/Object;
public fun initialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlinx/coroutines/CoroutineScope;)Ljava/lang/Object;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,45 @@ public interface BaseRenderContext<out PropsT, StateT, in OutputT> {
* given [update] function, and immediately passes it to [actionSink]. Handy for
* attaching event handlers to renderings.
*
* It is important to understand that the [update] lambda you provide here
* may not run synchronously. This function and its overloads provide a short cut
* that lets you replace this snippet:
*
* return SomeScreen(
* onClick = {
* context.actionSink.send(
* action { state = SomeNewState }
* }
* }
* )
*
* with this:
*
* return SomeScreen(
* onClick = context.eventHandler { state = SomeNewState }
* )
*
* Notice how your [update] function is passed to the [actionSink][BaseRenderContext.actionSink]
* to be eventually executed as the body of a [WorkflowAction]. If several actions get stacked
* up at once (think about accidental rapid taps on a button), that could take a while.
*
* If you require something to happen the instant a UI action happens, [eventHandler]
* is the wrong choice. You'll want to write your own call to `actionSink.send`:
*
* return SomeScreen(
* onClick = {
* // This happens immediately.
* MyAnalytics.log("SomeScreen was clicked")
*
* context.actionSink.send(
* action {
* // This happens eventually.
* state = SomeNewState
* }
* }
* }
* )
*
* @param name A string describing the update, included in the action's [toString]
* as a debugging aid
* @param update Function that defines the workflow update.
Expand Down
Loading

0 comments on commit 6395d0b

Please sign in to comment.