Skip to content

Commit c77a88d

Browse files
Merge pull request #876 from square/sedwards/fix-output-ordering
Fix onOutput ordering w/ Conflated Renderings Changes
2 parents c6bdd54 + 239e712 commit c77a88d

File tree

2 files changed

+60
-14
lines changed

2 files changed

+60
-14
lines changed

workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,10 @@ public fun <PropsT, OutputT, RenderingT> renderWorkflowIn(
134134
}
135135
)
136136

137-
suspend fun <PropsT, OutputT, RenderingT> renderAndEmitOutput(
138-
runner: WorkflowRunner<PropsT, OutputT, RenderingT>,
137+
suspend fun <OutputT> sendOutput(
139138
actionResult: ActionProcessingResult?,
140139
onOutput: suspend (OutputT) -> Unit
141-
): RenderingAndSnapshot<RenderingT> {
142-
val nextRenderAndSnapshot = runner.nextRendering()
143-
140+
) {
144141
when (actionResult) {
145142
is WorkflowOutput<*> -> {
146143
@Suppress("UNCHECKED_CAST")
@@ -150,8 +147,6 @@ public fun <PropsT, OutputT, RenderingT> renderWorkflowIn(
150147
}
151148
else -> {} // no -op
152149
}
153-
154-
return nextRenderAndSnapshot
155150
}
156151

157152
scope.launch {
@@ -166,14 +161,13 @@ public fun <PropsT, OutputT, RenderingT> renderWorkflowIn(
166161
// we don't surprise anyone with an unexpected rendering pass. Show's over, go home.
167162
if (!isActive) return@launch
168163

169-
// If the action did produce an Output, we send it immediately after the render pass.
170-
nextRenderAndSnapshot = renderAndEmitOutput(runner, actionResult, onOutput)
164+
nextRenderAndSnapshot = runner.nextRendering()
171165

172166
if (runtimeConfig == ConflateStaleRenderings) {
173-
// With this runtime modification, we do not pass renderings we know to be stale. This
174-
// means that we may be calling onOutput out of sync with the update of the UI. Output
175-
// is an event though, and should always occur immediately - i.e. it cannot be stale.
176-
while (actionResult != ActionsExhausted) {
167+
// Only null will allow us to continue processing actions and conflating stale renderings.
168+
// If this is not null, then we had an Output and we want to send it with the Rendering
169+
// (stale or not).
170+
while (actionResult == null) {
177171
// We have more actions we can process, so this rendering is stale.
178172
actionResult = runner.processAction(waitForAnAction = false)
179173

@@ -182,12 +176,14 @@ public fun <PropsT, OutputT, RenderingT> renderWorkflowIn(
182176
// If no actions processed, then no new rendering needed.
183177
if (actionResult == ActionsExhausted) break
184178

185-
nextRenderAndSnapshot = renderAndEmitOutput(runner, actionResult, onOutput)
179+
nextRenderAndSnapshot = runner.nextRendering()
186180
}
187181
}
188182

189183
// Pass on to the UI.
190184
renderingsAndSnapshots.value = nextRenderAndSnapshot
185+
// And emit the Output.
186+
sendOutput(actionResult, onOutput)
191187
}
192188
}
193189

workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,56 @@ class RenderWorkflowInTest {
328328
}
329329
}
330330

331+
@Test fun `onOutput called after rendering emitted`() {
332+
runtimeTestRunner.runParametrizedTest(
333+
paramSource = runtimeOptions,
334+
before = ::setup,
335+
) { runtimeConfig: RuntimeConfig ->
336+
val trigger = Channel<String>()
337+
val workflow = Workflow.stateful<String, String, String>(
338+
initialState = "initial",
339+
render = { renderState ->
340+
runningWorker(
341+
trigger.consumeAsFlow()
342+
.asWorker()
343+
) {
344+
action {
345+
state = it
346+
setOutput(it)
347+
}
348+
}
349+
renderState
350+
}
351+
)
352+
353+
val emittedRenderings = mutableListOf<String>()
354+
val receivedOutputs = mutableListOf<String>()
355+
val renderings = renderWorkflowIn(
356+
workflow = workflow,
357+
scope = testScope,
358+
props = MutableStateFlow(Unit),
359+
runtimeConfig = runtimeConfig
360+
) { it: String ->
361+
receivedOutputs += it
362+
assertTrue(emittedRenderings.contains(it))
363+
}
364+
assertTrue(receivedOutputs.isEmpty())
365+
366+
val scope = CoroutineScope(Unconfined)
367+
scope.launch {
368+
renderings.collect { rendering: RenderingAndSnapshot<String> ->
369+
emittedRenderings += rendering.rendering
370+
}
371+
}
372+
373+
trigger.trySend("foo").isSuccess
374+
375+
trigger.trySend("bar").isSuccess
376+
377+
scope.cancel()
378+
}
379+
}
380+
331381
@Test fun `onOutput is not called when no output emitted`() {
332382
runtimeTestRunner.runParametrizedTest(
333383
paramSource = runtimeOptions,

0 commit comments

Comments
 (0)