Skip to content

Commit eeb2fa8

Browse files
Merge pull request #916 from square/zachklipp/extract-update-handler
Pass acceptOutput function to WorkflowNode constructor instead of every tick pass.
2 parents 974324c + 4f55451 commit eeb2fa8

File tree

5 files changed

+224
-62
lines changed

5 files changed

+224
-62
lines changed

kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/SubtreeManager.kt

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import kotlin.coroutines.CoroutineContext
9292
*/
9393
internal class SubtreeManager<StateT, OutputT : Any>(
9494
private val contextForChildren: CoroutineContext,
95+
private val emitActionToParent: (WorkflowAction<StateT, OutputT>) -> Any?,
9596
private val parentDiagnosticId: Long,
9697
private val diagnosticListener: WorkflowDiagnosticListener? = null,
9798
private val idCounter: IdCounter? = null
@@ -150,16 +151,9 @@ internal class SubtreeManager<StateT, OutputT : Any>(
150151
* Uses [selector] to invoke [WorkflowNode.tick] for every running child workflow this instance
151152
* is managing.
152153
*/
153-
fun <T : Any> tickChildren(
154-
selector: SelectBuilder<T?>,
155-
handler: (WorkflowAction<StateT, OutputT>) -> T?
156-
) {
154+
fun <T : Any> tickChildren(selector: SelectBuilder<T?>) {
157155
children.forEachActive { child ->
158-
child.workflowNode.tick(selector) { output ->
159-
val componentUpdate = child.acceptChildOutput(output)
160-
@Suppress("UNCHECKED_CAST")
161-
return@tick handler(componentUpdate as WorkflowAction<StateT, OutputT>)
162-
}
156+
child.workflowNode.tick(selector)
163157
}
164158
}
165159

@@ -187,16 +181,25 @@ internal class SubtreeManager<StateT, OutputT : Any>(
187181
handler: (ChildOutputT) -> WorkflowAction<StateT, OutputT>
188182
): WorkflowChildNode<ChildPropsT, ChildOutputT, StateT, OutputT> {
189183
val id = child.id(key)
184+
lateinit var node: WorkflowChildNode<ChildPropsT, ChildOutputT, StateT, OutputT>
185+
186+
fun acceptChildOutput(output: ChildOutputT): Any? {
187+
val action = node.acceptChildOutput(output)
188+
return emitActionToParent(action)
189+
}
190+
190191
val workflowNode = WorkflowNode(
191192
id,
192193
child.asStatefulWorkflow(),
193194
initialProps,
194195
snapshotCache[id],
195196
contextForChildren,
197+
::acceptChildOutput,
196198
parentDiagnosticId,
197199
diagnosticListener,
198200
idCounter
199201
)
200202
return WorkflowChildNode(child, handler, workflowNode)
203+
.also { node = it }
201204
}
202205
}

kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowLoop.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ internal open class RealWorkflowLoop : WorkflowLoop {
121121
}
122122

123123
// Tick the workflow tree.
124-
rootNode.tick(this) { it }
124+
rootNode.tick(this)
125125
}
126126
}
127127
// Compiler gets confused, and thinks both that this throw is unreachable, and without the

kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNode.kt

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import com.squareup.workflow.diagnostic.createId
2828
import com.squareup.workflow.internal.RealRenderContext.WorkerRunner
2929
import kotlinx.coroutines.CoroutineName
3030
import kotlinx.coroutines.CoroutineScope
31-
import kotlinx.coroutines.InternalCoroutinesApi
3231
import kotlinx.coroutines.Job
3332
import kotlinx.coroutines.cancel
3433
import kotlinx.coroutines.channels.Channel
@@ -40,6 +39,8 @@ import kotlin.coroutines.CoroutineContext
4039
/**
4140
* A node in a state machine tree. Manages the actual state for a given [Workflow].
4241
*
42+
* @param emitOutputToParent A function that this node will call when it needs to emit an output
43+
* value to its parent. Returns either the output to be emitted from the root workflow, or null.
4344
* @param initialState Allows unit tests to start the node from a given state, instead of calling
4445
* [StatefulWorkflow.initialState].
4546
*/
@@ -50,6 +51,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
5051
initialProps: PropsT,
5152
snapshot: ByteString?,
5253
baseContext: CoroutineContext,
54+
private val emitOutputToParent: (OutputT) -> Any? = { it },
5355
parentDiagnosticId: Long? = null,
5456
private val diagnosticListener: WorkflowDiagnosticListener? = null,
5557
private val idCounter: IdCounter? = null,
@@ -69,8 +71,9 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
6971
*/
7072
internal val diagnosticId = idCounter.createId()
7173

72-
private val subtreeManager =
73-
SubtreeManager<StateT, OutputT>(coroutineContext, diagnosticId, diagnosticListener, idCounter)
74+
private val subtreeManager = SubtreeManager<StateT, OutputT>(
75+
coroutineContext, ::applyAction, diagnosticId, diagnosticListener, idCounter
76+
)
7477

7578
private val workers = ActiveStagingList<WorkerChildNode<*, *, *>>()
7679

@@ -158,20 +161,9 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
158161
*
159162
* It is an error to call this method after calling [cancel].
160163
*/
161-
@UseExperimental(InternalCoroutinesApi::class)
162-
fun <T : Any> tick(
163-
selector: SelectBuilder<T?>,
164-
handler: (OutputT) -> T?
165-
) {
166-
fun acceptUpdate(action: WorkflowAction<StateT, OutputT>): T? {
167-
val (newState, output) = action.applyTo(state)
168-
diagnosticListener?.onWorkflowAction(diagnosticId, action, state, newState, output)
169-
state = newState
170-
return output?.let(handler)
171-
}
172-
164+
fun <T : Any> tick(selector: SelectBuilder<T?>) {
173165
// Listen for any child workflow updates.
174-
subtreeManager.tickChildren(selector, ::acceptUpdate)
166+
subtreeManager.tickChildren(selector)
175167

176168
// Listen for any subscription updates.
177169
workers.forEachActive { child ->
@@ -188,7 +180,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
188180
} else {
189181
val update = child.acceptUpdate(valueOrDone.value)
190182
@Suppress("UNCHECKED_CAST")
191-
acceptUpdate(update as WorkflowAction<StateT, OutputT>)
183+
return@onReceive applyAction(update as WorkflowAction<StateT, OutputT>)
192184
}
193185
}
194186
}
@@ -198,7 +190,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
198190
with(selector) {
199191
eventActionsChannel.onReceive { action ->
200192
diagnosticListener?.onSinkReceived(diagnosticId, action)
201-
acceptUpdate(action)
193+
return@onReceive applyAction(action)
202194
}
203195
}
204196
}
@@ -256,6 +248,18 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
256248
lastProps = newProps
257249
}
258250

251+
/**
252+
* Applies [action] to this workflow's [state] and
253+
* [emits an output to its parent][emitOutputToParent] if necessary.
254+
*/
255+
private fun <T : Any> applyAction(action: WorkflowAction<StateT, OutputT>): T? {
256+
val (newState, output) = action.applyTo(state)
257+
diagnosticListener?.onWorkflowAction(diagnosticId, action, state, newState, output)
258+
state = newState
259+
@Suppress("UNCHECKED_CAST")
260+
return output?.let(emitOutputToParent) as T?
261+
}
262+
259263
private fun <T> createWorkerNode(
260264
worker: Worker<T>,
261265
key: String,

kotlin/workflow-runtime/src/test/java/com/squareup/workflow/internal/SubtreeManagerTest.kt

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -99,17 +99,15 @@ class SubtreeManagerTest {
9999
private val context = Unconfined
100100

101101
@Test fun `render starts new child`() {
102-
val manager =
103-
SubtreeManager<String, String>(context, parentDiagnosticId = 0, diagnosticListener = null)
102+
val manager = subtreeManagerForTest<String, String>()
104103
val workflow = TestWorkflow()
105104

106105
manager.render(workflow, "props", key = "", handler = { fail() })
107106
assertEquals(1, workflow.started)
108107
}
109108

110109
@Test fun `render doesn't start existing child`() {
111-
val manager =
112-
SubtreeManager<String, String>(context, parentDiagnosticId = 0, diagnosticListener = null)
110+
val manager = subtreeManagerForTest<String, String>()
113111
val workflow = TestWorkflow()
114112
fun render() = manager.render(workflow, "props", key = "", handler = { fail() })
115113
.also { manager.commitRenderedChildren() }
@@ -121,8 +119,7 @@ class SubtreeManagerTest {
121119
}
122120

123121
@Test fun `render restarts child after tearing down`() {
124-
val manager =
125-
SubtreeManager<String, String>(context, parentDiagnosticId = 0, diagnosticListener = null)
122+
val manager = subtreeManagerForTest<String, String>()
126123
val workflow = TestWorkflow()
127124
fun render() = manager.render(workflow, "props", key = "", handler = { fail() })
128125
.also { manager.commitRenderedChildren() }
@@ -138,8 +135,7 @@ class SubtreeManagerTest {
138135
}
139136

140137
@Test fun `render throws on duplicate key`() {
141-
val manager =
142-
SubtreeManager<String, String>(context, parentDiagnosticId = 0, diagnosticListener = null)
138+
val manager = subtreeManagerForTest<String, String>()
143139
val workflow = TestWorkflow()
144140
manager.render(workflow, "props", "foo", handler = { fail() })
145141

@@ -153,8 +149,7 @@ class SubtreeManagerTest {
153149
}
154150

155151
@Test fun `render returns child rendering`() {
156-
val manager =
157-
SubtreeManager<String, String>(context, parentDiagnosticId = 0, diagnosticListener = null)
152+
val manager = subtreeManagerForTest<String, String>()
158153
val workflow = TestWorkflow()
159154

160155
val (composeProps, composeState) = manager.render(
@@ -165,8 +160,7 @@ class SubtreeManagerTest {
165160
}
166161

167162
@Test fun `tick children handles child output`() {
168-
val manager =
169-
SubtreeManager<String, String>(context, parentDiagnosticId = 0, diagnosticListener = null)
163+
val manager = subtreeManagerForTest<String, String>()
170164
val workflow = TestWorkflow()
171165
val handler: StringHandler = { output ->
172166
action { setOutput("case output:$output") }
@@ -189,8 +183,7 @@ class SubtreeManagerTest {
189183
}
190184

191185
@Test fun `render updates child's output handler`() {
192-
val manager =
193-
SubtreeManager<String, String>(context, parentDiagnosticId = 0, diagnosticListener = null)
186+
val manager = subtreeManagerForTest<String, String>()
194187
val workflow = TestWorkflow()
195188
fun render(handler: StringHandler) =
196189
manager.render(workflow, "props", key = "", handler = handler)
@@ -219,7 +212,7 @@ class SubtreeManagerTest {
219212

220213
// See https://github.com/square/workflow/issues/404
221214
@Test fun `createChildSnapshot snapshots eagerly`() {
222-
val manager = SubtreeManager<Unit, Nothing>(Unconfined, parentDiagnosticId = 0)
215+
val manager = subtreeManagerForTest<Unit, Nothing>()
223216
val workflow = SnapshotTestWorkflow()
224217
assertEquals(0, workflow.snapshots)
225218

@@ -232,7 +225,7 @@ class SubtreeManagerTest {
232225

233226
// See https://github.com/square/workflow/issues/404
234227
@Test fun `createChildSnapshot serializes lazily`() {
235-
val manager = SubtreeManager<Unit, Nothing>(Unconfined, parentDiagnosticId = 0)
228+
val manager = subtreeManagerForTest<Unit, Nothing>()
236229
val workflow = SnapshotTestWorkflow()
237230
assertEquals(0, workflow.serializes)
238231

@@ -246,11 +239,9 @@ class SubtreeManagerTest {
246239
assertEquals(1, workflow.serializes)
247240
}
248241

249-
private suspend fun <S, O : Any> SubtreeManager<S, O>.tickAction(): WorkflowAction<S, O>? {
250-
return select {
251-
tickChildren(this) { update ->
252-
return@tickChildren update
253-
}
254-
}
255-
}
242+
private suspend fun <S, O : Any> SubtreeManager<S, O>.tickAction(): WorkflowAction<S, O>? =
243+
select { tickChildren(this) }
244+
245+
private fun <S, O : Any> subtreeManagerForTest() =
246+
SubtreeManager<S, O>(context, emitActionToParent = { it }, parentDiagnosticId = 0)
256247
}

0 commit comments

Comments
 (0)