Skip to content

Commit c49ef58

Browse files
Merge pull request #68 from square/zachklipp/reorder-runtime-output
Emit output before next render pass.
2 parents 686bf96 + b084b16 commit c49ef58

File tree

2 files changed

+37
-4
lines changed

2 files changed

+37
-4
lines changed

workflow-runtime/src/main/java/com/squareup/workflow/RenderWorkflow.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,10 @@ fun <PropsT, OutputT, RenderingT> renderWorkflowIn(
158158
// It might look weird to start by consuming the output before getting the rendering below,
159159
// but remember the first render pass already occurred above, before this coroutine was even
160160
// launched.
161-
val output = runner.nextOutput()
161+
runner.nextOutput()
162+
?.let { onOutput(it.value) }
162163

163-
// After receiving an output, the next render pass must be done before emitting that output,
164-
// so that the workflow states appear consistent to observers of the outputs and renderings.
165164
renderingsAndSnapshots.value = runner.nextRendering()
166-
output?.let { onOutput(it.value) }
167165
}
168166
}
169167

workflow-runtime/src/test/java/com/squareup/workflow/RenderWorkflowInTest.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import kotlinx.coroutines.channels.Channel
2626
import kotlinx.coroutines.flow.MutableStateFlow
2727
import kotlinx.coroutines.flow.collect
2828
import kotlinx.coroutines.flow.consumeAsFlow
29+
import kotlinx.coroutines.flow.launchIn
30+
import kotlinx.coroutines.flow.onEach
2931
import kotlinx.coroutines.isActive
3032
import kotlinx.coroutines.launch
3133
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -363,5 +365,38 @@ class RenderWorkflowInTest {
363365
assertFalse(scope.isActive)
364366
}
365367

368+
@Test fun `output is emitted before next render pass`() {
369+
val outputTrigger = CompletableDeferred<String>()
370+
// A workflow whose state and rendering is the last output that it emitted.
371+
val workflow = Workflow.stateful<Unit, String, String, String>(
372+
initialState = { "{no output}" },
373+
render = { _, state ->
374+
runningWorker(Worker.from { outputTrigger.await() }) { output ->
375+
action {
376+
setOutput(output)
377+
nextState = output
378+
}
379+
}
380+
return@stateful state
381+
}
382+
)
383+
val scope = TestCoroutineScope()
384+
val events = mutableListOf<String>()
385+
renderWorkflowIn(workflow, scope, MutableStateFlow(Unit)) { events += "output($it)" }
386+
.onEach { events += "rendering(${it.rendering})" }
387+
.launchIn(scope)
388+
assertEquals(listOf("rendering({no output})"), events)
389+
390+
outputTrigger.complete("output")
391+
assertEquals(
392+
listOf(
393+
"rendering({no output})",
394+
"output(output)",
395+
"rendering(output)"
396+
),
397+
events
398+
)
399+
}
400+
366401
private class ExpectedException : RuntimeException()
367402
}

0 commit comments

Comments
 (0)