Skip to content

1311: Enable applying Multiple Actions on immediate dispatcher for CONFLATE_STALE_RENDERINGS #1344

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ include(
":workflow-config:config-jvm",
":workflow-core",
":workflow-runtime",
":workflow-runtime-android",
":workflow-rx2",
":workflow-testing",
":workflow-tracing",
Expand Down
16 changes: 14 additions & 2 deletions workflow-core/api/workflow-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public final class com/squareup/workflow1/BaseRenderContext$DefaultImpls {
public static synthetic fun renderChild$default (Lcom/squareup/workflow1/BaseRenderContext;Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
}

public final class com/squareup/workflow1/DeferredActionToBeApplied : com/squareup/workflow1/ActionProcessingResult {
public fun <init> (Lkotlinx/coroutines/Deferred;)V
public final fun getApplyAction ()Lkotlinx/coroutines/Deferred;
}

public final class com/squareup/workflow1/HandlerBox1 {
public field handler Lkotlin/jvm/functions/Function1;
public fun <init> ()V
Expand Down Expand Up @@ -161,6 +166,10 @@ public final class com/squareup/workflow1/PropsUpdated : com/squareup/workflow1/
public static final field INSTANCE Lcom/squareup/workflow1/PropsUpdated;
}

public final class com/squareup/workflow1/RuntimeConfigKt {
public static final fun shouldDeferFirstAction (Ljava/util/Set;)Z
}

public final class com/squareup/workflow1/RuntimeConfigOptions : java/lang/Enum {
public static final field CONFLATE_STALE_RENDERINGS Lcom/squareup/workflow1/RuntimeConfigOptions;
public static final field Companion Lcom/squareup/workflow1/RuntimeConfigOptions$Companion;
Expand Down Expand Up @@ -325,6 +334,7 @@ public abstract class com/squareup/workflow1/WorkflowAction {
public fun <init> ()V
public abstract fun apply (Lcom/squareup/workflow1/WorkflowAction$Updater;)V
public fun getDebuggingName ()Ljava/lang/String;
public fun isDeferrable ()Z
public fun toString ()Ljava/lang/String;
}

Expand Down Expand Up @@ -418,8 +428,10 @@ public final class com/squareup/workflow1/Workflows {
public static final fun action (Lcom/squareup/workflow1/StatefulWorkflow;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/WorkflowAction;
public static final fun action (Lcom/squareup/workflow1/StatelessWorkflow;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/WorkflowAction;
public static final fun action (Lcom/squareup/workflow1/StatelessWorkflow;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/WorkflowAction;
public static final fun action (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/WorkflowAction;
public static final fun action (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/WorkflowAction;
public static final fun action (Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/WorkflowAction;
public static final fun action (Lkotlin/jvm/functions/Function0;ZLkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/WorkflowAction;
public static synthetic fun action$default (Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/WorkflowAction;
public static synthetic fun action$default (Lkotlin/jvm/functions/Function0;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/WorkflowAction;
public static final fun applyTo (Lcom/squareup/workflow1/WorkflowAction;Ljava/lang/Object;Ljava/lang/Object;)Lkotlin/Pair;
public static final fun collectToSink (Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow1/Sink;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun contraMap (Lcom/squareup/workflow1/Sink;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/Sink;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,16 @@ internal fun <P, S, O> BaseRenderContext<P, S, O>.eventHandler0(
remember: Boolean,
update: Updater<P, S, O>.() -> Unit
): () -> Unit {
val handler = { actionSink.send(action("eH: $name", update)) }
val handler = {
actionSink.send(
action(
name = "eH: $name",
// Event handlers are *never* deferrable since they respond to UI input.
isDeferrable = false,
apply = update,
)
)
}
return if (remember) {
val box = remember(name) { HandlerBox0() }
box.handler = handler
Expand All @@ -34,7 +43,14 @@ internal inline fun <P, S, O, reified EventT> BaseRenderContext<P, S, O>.eventHa
remember: Boolean,
noinline update: Updater<P, S, O>.(EventT) -> Unit
): (EventT) -> Unit {
val handler = { e: EventT -> actionSink.send(action("eH: $name") { update(e) }) }
val handler = { e: EventT ->
actionSink.send(
action(
name = "eH: $name",
isDeferrable = false,
) { update(e) }
)
}
return if (remember) {
val box = remember(name, typeOf<EventT>()) { HandlerBox1<EventT>() }
box.handler = handler
Expand All @@ -56,7 +72,14 @@ internal inline fun <P, S, O, reified E1, reified E2> BaseRenderContext<P, S, O>
remember: Boolean,
noinline update: Updater<P, S, O>.(E1, E2) -> Unit
): (E1, E2) -> Unit {
val handler = { e1: E1, e2: E2 -> actionSink.send(action("eH: $name") { update(e1, e2) }) }
val handler = { e1: E1, e2: E2 ->
actionSink.send(
action(
name = "eH: $name",
isDeferrable = false,
) { update(e1, e2) }
)
}
return if (remember) {
val box = remember(name, typeOf<E1>(), typeOf<E2>()) { HandlerBox2<E1, E2>() }
box.handler = handler
Expand Down Expand Up @@ -86,7 +109,14 @@ internal inline fun <
noinline update: Updater<P, S, O>.(E1, E2, E3) -> Unit
): (E1, E2, E3) -> Unit {
val handler =
{ e1: E1, e2: E2, e3: E3 -> actionSink.send(action("eH: $name") { update(e1, e2, e3) }) }
{ e1: E1, e2: E2, e3: E3 ->
actionSink.send(
action(
name = "eH: $name",
isDeferrable = false,
) { update(e1, e2, e3) }
)
}
return if (remember) {
val box =
remember(name, typeOf<E1>(), typeOf<E2>(), typeOf<E3>()) { HandlerBox3<E1, E2, E3>() }
Expand Down Expand Up @@ -118,7 +148,12 @@ internal inline fun <
noinline update: Updater<P, S, O>.(E1, E2, E3, E4) -> Unit
): (E1, E2, E3, E4) -> Unit {
val handler = { e1: E1, e2: E2, e3: E3, e4: E4 ->
actionSink.send(action("eH: $name") { update(e1, e2, e3, e4) })
actionSink.send(
action(
name = "eH: $name",
isDeferrable = false,
) { update(e1, e2, e3, e4) }
)
}
return if (remember) {
val box = remember(
Expand Down Expand Up @@ -158,7 +193,12 @@ internal inline fun <
noinline update: Updater<P, S, O>.(E1, E2, E3, E4, E5) -> Unit
): (E1, E2, E3, E4, E5) -> Unit {
val handler = { e1: E1, e2: E2, e3: E3, e4: E4, e5: E5 ->
actionSink.send(action("eH: $name") { update(e1, e2, e3, e4, e5) })
actionSink.send(
action(
name = "eH: $name",
isDeferrable = false,
) { update(e1, e2, e3, e4, e5) }
)
}
return if (remember) {
val box = remember(
Expand Down Expand Up @@ -200,7 +240,12 @@ internal inline fun <
noinline update: Updater<P, S, O>.(E1, E2, E3, E4, E5, E6) -> Unit
): (E1, E2, E3, E4, E5, E6) -> Unit {
val handler = { e1: E1, e2: E2, e3: E3, e4: E4, e5: E5, e6: E6 ->
actionSink.send(action("eH: $name") { update(e1, e2, e3, e4, e5, e6) })
actionSink.send(
action(
name = "eH: $name",
isDeferrable = false,
) { update(e1, e2, e3, e4, e5, e6) }
)
}
return if (remember) {
val box = remember(
Expand Down Expand Up @@ -244,7 +289,12 @@ internal inline fun <
noinline update: Updater<P, S, O>.(E1, E2, E3, E4, E5, E6, E7) -> Unit
): (E1, E2, E3, E4, E5, E6, E7) -> Unit {
val handler = { e1: E1, e2: E2, e3: E3, e4: E4, e5: E5, e6: E6, e7: E7 ->
actionSink.send(action("eH: $name") { update(e1, e2, e3, e4, e5, e6, e7) })
actionSink.send(
action(
name = "eH: $name",
isDeferrable = false,
) { update(e1, e2, e3, e4, e5, e6, e7) }
)
}
return if (remember) {
val box = remember(
Expand Down Expand Up @@ -290,7 +340,12 @@ internal inline fun <
noinline update: Updater<P, S, O>.(E1, E2, E3, E4, E5, E6, E7, E8) -> Unit
): (E1, E2, E3, E4, E5, E6, E7, E8) -> Unit {
val handler = { e1: E1, e2: E2, e3: E3, e4: E4, e5: E5, e6: E6, e7: E7, e8: E8 ->
actionSink.send(action("eH: $name") { update(e1, e2, e3, e4, e5, e6, e7, e8) })
actionSink.send(
action(
name = "eH: $name",
isDeferrable = false,
) { update(e1, e2, e3, e4, e5, e6, e7, e8) }
)
}
return if (remember) {
val box = remember(
Expand Down Expand Up @@ -338,7 +393,12 @@ internal inline fun <
noinline update: Updater<P, S, O>.(E1, E2, E3, E4, E5, E6, E7, E8, E9) -> Unit
): (E1, E2, E3, E4, E5, E6, E7, E8, E9) -> Unit {
val handler = { e1: E1, e2: E2, e3: E3, e4: E4, e5: E5, e6: E6, e7: E7, e8: E8, e9: E9 ->
actionSink.send(action("eH: $name") { update(e1, e2, e3, e4, e5, e6, e7, e8, e9) })
actionSink.send(
action(
name = "eH: $name",
isDeferrable = false,
) { update(e1, e2, e3, e4, e5, e6, e7, e8, e9) }
)
}
return if (remember) {
val box = remember(
Expand Down Expand Up @@ -389,7 +449,12 @@ internal inline fun <
): (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10) -> Unit {
val handler =
{ e1: E1, e2: E2, e3: E3, e4: E4, e5: E5, e6: E6, e7: E7, e8: E8, e9: E9, e10: E10 ->
actionSink.send(action("eH: $name") { update(e1, e2, e3, e4, e5, e6, e7, e8, e9, e10) })
actionSink.send(
action(
name = "eH: $name",
isDeferrable = false,
) { update(e1, e2, e3, e4, e5, e6, e7, e8, e9, e10) }
)
}
return if (remember) {
val box = remember(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ public annotation class WorkflowExperimentalRuntime

public typealias RuntimeConfig = Set<RuntimeConfigOptions>

/**
* Whether or not we have an optimization enabled that should cause us to consider 'deferring'
* the application of the first action received after resuming from suspension in the runtime
* loop. We will only actually defer if [WorkflowAction.isDeferrable] is true for that action.
*/
@WorkflowExperimentalRuntime
public fun RuntimeConfig.shouldDeferFirstAction(): Boolean {
return contains(RuntimeConfigOptions.CONFLATE_STALE_RENDERINGS)
}

/**
* A specification of the possible Workflow Runtime options.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ internal suspend fun <
) {
suspendCancellableCoroutine<Unit> { continuation ->
val resumingAction = object : WorkflowAction<PropsT, StateT, OutputT>() {
override val isDeferrable: Boolean
get() = action.isDeferrable

// Pipe through debugging name to the original action.
override val debuggingName: String
get() = action.debuggingName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ internal class WorkerWorkflow<OutputT>(
renderState: Int,
context: RenderContext<Worker<OutputT>, Int, OutputT>
) {
val localKey = renderState.toString()
// Scope the side effect coroutine to the state value, so the worker will be re-started when
// it changes (such that doesSameWorkAs returns false above).
context.runningSideEffect(renderState.toString()) {
context.runningSideEffect(localKey) {
runWorker(renderProps, key, context.actionSink)
}
}
Expand Down Expand Up @@ -97,6 +98,11 @@ private class EmitWorkerOutputAction<P, S, O>(
override val debuggingName: String =
"EmitWorkerOutputAction(worker=$worker, key=$renderKey)"

/**
* All actions from workers are deferrable!
*/
override val isDeferrable: Boolean = true

override fun Updater.apply() {
setOutput(output)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

package com.squareup.workflow1

import com.squareup.workflow1.WorkflowAction.Companion.toString
import kotlinx.coroutines.Deferred
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.jvm.JvmOverloads
Expand Down Expand Up @@ -42,6 +42,18 @@ public abstract class WorkflowAction<PropsT, StateT, OutputT> {
*/
public open val debuggingName: String = commonUniqueClassName(this::class)

/**
* Whether or not we can wait for one extra dispatch before handling this action. This should
* *only* ever be true for actions that respond to asynchronous events like data loading.
* This should *never* be true for anything respond to UI input.
*
* Note that we *do not* mean deferred to some unknown time in the future. Functionally, this
* means that we can [kotlinx.coroutines.yield] whatever thread we are on once before running.
* Currently this only takes effect when certain optimizations are enabled, like
* [RuntimeConfigOptions.CONFLATE_STALE_RENDERINGS].
*/
public open val isDeferrable: Boolean = false

/**
* The context for calls to [WorkflowAction.apply]. Allows the action to read and change the
* [state], and to emit an [output][setOutput] value.
Expand Down Expand Up @@ -102,15 +114,17 @@ public abstract class WorkflowAction<PropsT, StateT, OutputT> {
* of this function directly, to avoid repeating its parameter types.
*
* @param name A string describing the update for debugging.
* @param isDeferrable see [WorkflowAction.isDeferrable].
* @param apply Function that defines the workflow update.
*
* @see StatelessWorkflow.action
* @see StatefulWorkflow.action
*/
public fun <PropsT, StateT, OutputT> action(
name: String,
isDeferrable: Boolean = false,
apply: WorkflowAction<PropsT, StateT, OutputT>.Updater.() -> Unit
): WorkflowAction<PropsT, StateT, OutputT> = action({ name }, apply)
): WorkflowAction<PropsT, StateT, OutputT> = action({ name }, isDeferrable, apply)

/**
* Creates a [WorkflowAction] from the [apply] lambda.
Expand All @@ -127,8 +141,11 @@ public fun <PropsT, StateT, OutputT> action(
*/
public fun <PropsT, StateT, OutputT> action(
name: () -> String,
isDeferrable: Boolean = false,
apply: WorkflowAction<PropsT, StateT, OutputT>.Updater.() -> Unit
): WorkflowAction<PropsT, StateT, OutputT> = object : WorkflowAction<PropsT, StateT, OutputT>() {
override val isDeferrable: Boolean = isDeferrable

override val debuggingName: String
get() = name()

Expand Down Expand Up @@ -180,6 +197,10 @@ public object PropsUpdated : ActionProcessingResult

public object ActionsExhausted : ActionProcessingResult

public class DeferredActionToBeApplied(
public val applyAction: Deferred<ActionProcessingResult>
) : ActionProcessingResult

/**
* Result of applying an action.
*
Expand Down
4 changes: 4 additions & 0 deletions workflow-runtime-android/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Module Workflow Runtime Android

This module is an Android library that is used to test the Workflow Runtime with Android specific
coroutine dispatchers. These are headless android-tests that run on device without UI.
Empty file.
25 changes: 25 additions & 0 deletions workflow-runtime-android/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
plugins {
id("com.android.library")
id("kotlin-android")
id("android-defaults")
id("android-ui-tests")
}

android {
namespace = "com.squareup.workflow1"
testNamespace = "$namespace.test"
}

dependencies {
api(project(":workflow-runtime"))
implementation(project(":workflow-core"))

androidTestImplementation(libs.androidx.test.core)
androidTestImplementation(libs.androidx.test.truth)
androidTestImplementation(libs.kotlin.test.core)
androidTestImplementation(libs.kotlin.test.jdk)
androidTestImplementation(libs.kotlinx.coroutines.android)
androidTestImplementation(libs.kotlinx.coroutines.core)
androidTestImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(libs.truth)
}
3 changes: 3 additions & 0 deletions workflow-runtime-android/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
POM_ARTIFACT_ID=workflow-runtime-android
POM_NAME=Workflow Runtime Android
POM_PACKAGING=aar
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application>
<activity android:name="androidx.activity.ComponentActivity"/>
</application>
</manifest>
Loading