Skip to content

Commit 4601a04

Browse files
With CSR Yield before applying first Action if deferrable
A deferrable action is one that can wait a main thread post - such as from an async trigger (Worker). Not a UI action. To allow other actions to queue up, so they can be drained.
1 parent 9dcac88 commit 4601a04

File tree

14 files changed

+637
-107
lines changed

14 files changed

+637
-107
lines changed

workflow-core/api/workflow-core.api

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ public final class com/squareup/workflow1/BaseRenderContext$DefaultImpls {
3535
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;
3636
}
3737

38+
public final class com/squareup/workflow1/DeferredActionToBeApplied : com/squareup/workflow1/ActionProcessingResult {
39+
public fun <init> (Lkotlinx/coroutines/Deferred;)V
40+
public final fun getApplyAction ()Lkotlinx/coroutines/Deferred;
41+
}
42+
3843
public final class com/squareup/workflow1/HandlerBox1 {
3944
public field handler Lkotlin/jvm/functions/Function1;
4045
public fun <init> ()V
@@ -161,6 +166,10 @@ public final class com/squareup/workflow1/PropsUpdated : com/squareup/workflow1/
161166
public static final field INSTANCE Lcom/squareup/workflow1/PropsUpdated;
162167
}
163168

169+
public final class com/squareup/workflow1/RuntimeConfigKt {
170+
public static final fun shouldDeferFirstAction (Ljava/util/Set;)Z
171+
}
172+
164173
public final class com/squareup/workflow1/RuntimeConfigOptions : java/lang/Enum {
165174
public static final field CONFLATE_STALE_RENDERINGS Lcom/squareup/workflow1/RuntimeConfigOptions;
166175
public static final field Companion Lcom/squareup/workflow1/RuntimeConfigOptions$Companion;
@@ -325,6 +334,7 @@ public abstract class com/squareup/workflow1/WorkflowAction {
325334
public fun <init> ()V
326335
public abstract fun apply (Lcom/squareup/workflow1/WorkflowAction$Updater;)V
327336
public fun getDebuggingName ()Ljava/lang/String;
337+
public fun isDeferrable ()Z
328338
public fun toString ()Ljava/lang/String;
329339
}
330340

@@ -418,8 +428,10 @@ public final class com/squareup/workflow1/Workflows {
418428
public static final fun action (Lcom/squareup/workflow1/StatefulWorkflow;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/WorkflowAction;
419429
public static final fun action (Lcom/squareup/workflow1/StatelessWorkflow;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/WorkflowAction;
420430
public static final fun action (Lcom/squareup/workflow1/StatelessWorkflow;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/WorkflowAction;
421-
public static final fun action (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/WorkflowAction;
422-
public static final fun action (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/WorkflowAction;
431+
public static final fun action (Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/WorkflowAction;
432+
public static final fun action (Lkotlin/jvm/functions/Function0;ZLkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/WorkflowAction;
433+
public static synthetic fun action$default (Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/WorkflowAction;
434+
public static synthetic fun action$default (Lkotlin/jvm/functions/Function0;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/WorkflowAction;
423435
public static final fun applyTo (Lcom/squareup/workflow1/WorkflowAction;Ljava/lang/Object;Ljava/lang/Object;)Lkotlin/Pair;
424436
public static final fun collectToSink (Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow1/Sink;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
425437
public static final fun contraMap (Lcom/squareup/workflow1/Sink;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/Sink;

workflow-core/src/commonMain/kotlin/com/squareup/workflow1/HandlerBox.kt

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,16 @@ internal fun <P, S, O> BaseRenderContext<P, S, O>.eventHandler0(
1212
remember: Boolean,
1313
update: Updater<P, S, O>.() -> Unit
1414
): () -> Unit {
15-
val handler = { actionSink.send(action("eH: $name", update)) }
15+
val handler = {
16+
actionSink.send(
17+
action(
18+
name = "eH: $name",
19+
// Event handlers are *never* deferrable since they respond to UI input.
20+
isDeferrable = false,
21+
apply = update,
22+
)
23+
)
24+
}
1625
return if (remember) {
1726
val box = remember(name) { HandlerBox0() }
1827
box.handler = handler
@@ -34,7 +43,14 @@ internal inline fun <P, S, O, reified EventT> BaseRenderContext<P, S, O>.eventHa
3443
remember: Boolean,
3544
noinline update: Updater<P, S, O>.(EventT) -> Unit
3645
): (EventT) -> Unit {
37-
val handler = { e: EventT -> actionSink.send(action("eH: $name") { update(e) }) }
46+
val handler = { e: EventT ->
47+
actionSink.send(
48+
action(
49+
name = "eH: $name",
50+
isDeferrable = false,
51+
) { update(e) }
52+
)
53+
}
3854
return if (remember) {
3955
val box = remember(name, typeOf<EventT>()) { HandlerBox1<EventT>() }
4056
box.handler = handler
@@ -56,7 +72,14 @@ internal inline fun <P, S, O, reified E1, reified E2> BaseRenderContext<P, S, O>
5672
remember: Boolean,
5773
noinline update: Updater<P, S, O>.(E1, E2) -> Unit
5874
): (E1, E2) -> Unit {
59-
val handler = { e1: E1, e2: E2 -> actionSink.send(action("eH: $name") { update(e1, e2) }) }
75+
val handler = { e1: E1, e2: E2 ->
76+
actionSink.send(
77+
action(
78+
name = "eH: $name",
79+
isDeferrable = false,
80+
) { update(e1, e2) }
81+
)
82+
}
6083
return if (remember) {
6184
val box = remember(name, typeOf<E1>(), typeOf<E2>()) { HandlerBox2<E1, E2>() }
6285
box.handler = handler
@@ -86,7 +109,14 @@ internal inline fun <
86109
noinline update: Updater<P, S, O>.(E1, E2, E3) -> Unit
87110
): (E1, E2, E3) -> Unit {
88111
val handler =
89-
{ e1: E1, e2: E2, e3: E3 -> actionSink.send(action("eH: $name") { update(e1, e2, e3) }) }
112+
{ e1: E1, e2: E2, e3: E3 ->
113+
actionSink.send(
114+
action(
115+
name = "eH: $name",
116+
isDeferrable = false,
117+
) { update(e1, e2, e3) }
118+
)
119+
}
90120
return if (remember) {
91121
val box =
92122
remember(name, typeOf<E1>(), typeOf<E2>(), typeOf<E3>()) { HandlerBox3<E1, E2, E3>() }
@@ -118,7 +148,12 @@ internal inline fun <
118148
noinline update: Updater<P, S, O>.(E1, E2, E3, E4) -> Unit
119149
): (E1, E2, E3, E4) -> Unit {
120150
val handler = { e1: E1, e2: E2, e3: E3, e4: E4 ->
121-
actionSink.send(action("eH: $name") { update(e1, e2, e3, e4) })
151+
actionSink.send(
152+
action(
153+
name = "eH: $name",
154+
isDeferrable = false,
155+
) { update(e1, e2, e3, e4) }
156+
)
122157
}
123158
return if (remember) {
124159
val box = remember(
@@ -158,7 +193,12 @@ internal inline fun <
158193
noinline update: Updater<P, S, O>.(E1, E2, E3, E4, E5) -> Unit
159194
): (E1, E2, E3, E4, E5) -> Unit {
160195
val handler = { e1: E1, e2: E2, e3: E3, e4: E4, e5: E5 ->
161-
actionSink.send(action("eH: $name") { update(e1, e2, e3, e4, e5) })
196+
actionSink.send(
197+
action(
198+
name = "eH: $name",
199+
isDeferrable = false,
200+
) { update(e1, e2, e3, e4, e5) }
201+
)
162202
}
163203
return if (remember) {
164204
val box = remember(
@@ -200,7 +240,12 @@ internal inline fun <
200240
noinline update: Updater<P, S, O>.(E1, E2, E3, E4, E5, E6) -> Unit
201241
): (E1, E2, E3, E4, E5, E6) -> Unit {
202242
val handler = { e1: E1, e2: E2, e3: E3, e4: E4, e5: E5, e6: E6 ->
203-
actionSink.send(action("eH: $name") { update(e1, e2, e3, e4, e5, e6) })
243+
actionSink.send(
244+
action(
245+
name = "eH: $name",
246+
isDeferrable = false,
247+
) { update(e1, e2, e3, e4, e5, e6) }
248+
)
204249
}
205250
return if (remember) {
206251
val box = remember(
@@ -244,7 +289,12 @@ internal inline fun <
244289
noinline update: Updater<P, S, O>.(E1, E2, E3, E4, E5, E6, E7) -> Unit
245290
): (E1, E2, E3, E4, E5, E6, E7) -> Unit {
246291
val handler = { e1: E1, e2: E2, e3: E3, e4: E4, e5: E5, e6: E6, e7: E7 ->
247-
actionSink.send(action("eH: $name") { update(e1, e2, e3, e4, e5, e6, e7) })
292+
actionSink.send(
293+
action(
294+
name = "eH: $name",
295+
isDeferrable = false,
296+
) { update(e1, e2, e3, e4, e5, e6, e7) }
297+
)
248298
}
249299
return if (remember) {
250300
val box = remember(
@@ -290,7 +340,12 @@ internal inline fun <
290340
noinline update: Updater<P, S, O>.(E1, E2, E3, E4, E5, E6, E7, E8) -> Unit
291341
): (E1, E2, E3, E4, E5, E6, E7, E8) -> Unit {
292342
val handler = { e1: E1, e2: E2, e3: E3, e4: E4, e5: E5, e6: E6, e7: E7, e8: E8 ->
293-
actionSink.send(action("eH: $name") { update(e1, e2, e3, e4, e5, e6, e7, e8) })
343+
actionSink.send(
344+
action(
345+
name = "eH: $name",
346+
isDeferrable = false,
347+
) { update(e1, e2, e3, e4, e5, e6, e7, e8) }
348+
)
294349
}
295350
return if (remember) {
296351
val box = remember(
@@ -338,7 +393,12 @@ internal inline fun <
338393
noinline update: Updater<P, S, O>.(E1, E2, E3, E4, E5, E6, E7, E8, E9) -> Unit
339394
): (E1, E2, E3, E4, E5, E6, E7, E8, E9) -> Unit {
340395
val handler = { e1: E1, e2: E2, e3: E3, e4: E4, e5: E5, e6: E6, e7: E7, e8: E8, e9: E9 ->
341-
actionSink.send(action("eH: $name") { update(e1, e2, e3, e4, e5, e6, e7, e8, e9) })
396+
actionSink.send(
397+
action(
398+
name = "eH: $name",
399+
isDeferrable = false,
400+
) { update(e1, e2, e3, e4, e5, e6, e7, e8, e9) }
401+
)
342402
}
343403
return if (remember) {
344404
val box = remember(
@@ -389,7 +449,12 @@ internal inline fun <
389449
): (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10) -> Unit {
390450
val handler =
391451
{ e1: E1, e2: E2, e3: E3, e4: E4, e5: E5, e6: E6, e7: E7, e8: E8, e9: E9, e10: E10 ->
392-
actionSink.send(action("eH: $name") { update(e1, e2, e3, e4, e5, e6, e7, e8, e9, e10) })
452+
actionSink.send(
453+
action(
454+
name = "eH: $name",
455+
isDeferrable = false,
456+
) { update(e1, e2, e3, e4, e5, e6, e7, e8, e9, e10) }
457+
)
393458
}
394459
return if (remember) {
395460
val box = remember(

workflow-core/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ public annotation class WorkflowExperimentalRuntime
1919

2020
public typealias RuntimeConfig = Set<RuntimeConfigOptions>
2121

22+
/**
23+
* Whether or not we have an optimization enabled that should cause us to consider 'deferring'
24+
* the application of the first action received after resuming from suspension in the runtime
25+
* loop. We will only actually defer if [WorkflowAction.isDeferrable] is true for that action.
26+
*/
27+
@WorkflowExperimentalRuntime
28+
public fun RuntimeConfig.shouldDeferFirstAction(): Boolean {
29+
return contains(RuntimeConfigOptions.CONFLATE_STALE_RENDERINGS)
30+
}
31+
2232
/**
2333
* A specification of the possible Workflow Runtime options.
2434
*/

workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Sink.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ internal suspend fun <
9999
) {
100100
suspendCancellableCoroutine<Unit> { continuation ->
101101
val resumingAction = object : WorkflowAction<PropsT, StateT, OutputT>() {
102+
override val isDeferrable: Boolean
103+
get() = action.isDeferrable
104+
102105
// Pipe through debugging name to the original action.
103106
override val debuggingName: String
104107
get() = action.debuggingName

workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,10 @@ internal class WorkerWorkflow<OutputT>(
5959
renderState: Int,
6060
context: RenderContext<Worker<OutputT>, Int, OutputT>
6161
) {
62+
val localKey = renderState.toString()
6263
// Scope the side effect coroutine to the state value, so the worker will be re-started when
6364
// it changes (such that doesSameWorkAs returns false above).
64-
context.runningSideEffect(renderState.toString()) {
65+
context.runningSideEffect(localKey) {
6566
runWorker(renderProps, key, context.actionSink)
6667
}
6768
}
@@ -97,6 +98,11 @@ private class EmitWorkerOutputAction<P, S, O>(
9798
override val debuggingName: String =
9899
"EmitWorkerOutputAction(worker=$worker, key=$renderKey)"
99100

101+
/**
102+
* All actions from workers are deferrable!
103+
*/
104+
override val isDeferrable: Boolean = true
105+
100106
override fun Updater.apply() {
101107
setOutput(output)
102108
}

workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkflowAction.kt

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
package com.squareup.workflow1
55

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

45+
/**
46+
* Whether or not we can wait for one extra dispatch before handling this action. This should
47+
* *only* ever be true for actions that respond to asynchronous events like data loading.
48+
* This should *never* be true for anything respond to UI input.
49+
*
50+
* Note that we *do not* mean deferred to some unknown time in the future. Functionally, this
51+
* means that we can [kotlinx.coroutines.yield] whatever thread we are on once before running.
52+
* Currently this only takes effect when certain optimizations are enabled, like
53+
* [RuntimeConfigOptions.CONFLATE_STALE_RENDERINGS].
54+
*/
55+
public open val isDeferrable: Boolean = false
56+
4557
/**
4658
* The context for calls to [WorkflowAction.apply]. Allows the action to read and change the
4759
* [state], and to emit an [output][setOutput] value.
@@ -102,15 +114,17 @@ public abstract class WorkflowAction<PropsT, StateT, OutputT> {
102114
* of this function directly, to avoid repeating its parameter types.
103115
*
104116
* @param name A string describing the update for debugging.
117+
* @param isDeferrable see [WorkflowAction.isDeferrable].
105118
* @param apply Function that defines the workflow update.
106119
*
107120
* @see StatelessWorkflow.action
108121
* @see StatefulWorkflow.action
109122
*/
110123
public fun <PropsT, StateT, OutputT> action(
111124
name: String,
125+
isDeferrable: Boolean = false,
112126
apply: WorkflowAction<PropsT, StateT, OutputT>.Updater.() -> Unit
113-
): WorkflowAction<PropsT, StateT, OutputT> = action({ name }, apply)
127+
): WorkflowAction<PropsT, StateT, OutputT> = action({ name }, isDeferrable, apply)
114128

115129
/**
116130
* Creates a [WorkflowAction] from the [apply] lambda.
@@ -127,8 +141,11 @@ public fun <PropsT, StateT, OutputT> action(
127141
*/
128142
public fun <PropsT, StateT, OutputT> action(
129143
name: () -> String,
144+
isDeferrable: Boolean = false,
130145
apply: WorkflowAction<PropsT, StateT, OutputT>.Updater.() -> Unit
131146
): WorkflowAction<PropsT, StateT, OutputT> = object : WorkflowAction<PropsT, StateT, OutputT>() {
147+
override val isDeferrable: Boolean = isDeferrable
148+
132149
override val debuggingName: String
133150
get() = name()
134151

@@ -180,6 +197,10 @@ public object PropsUpdated : ActionProcessingResult
180197

181198
public object ActionsExhausted : ActionProcessingResult
182199

200+
public class DeferredActionToBeApplied(
201+
public val applyAction: Deferred<ActionProcessingResult>
202+
) : ActionProcessingResult
203+
183204
/**
184205
* Result of applying an action.
185206
*

0 commit comments

Comments
 (0)