Skip to content

Commit 00011b8

Browse files
WIP
1 parent 6f59dc0 commit 00011b8

File tree

5 files changed

+64
-63
lines changed

5 files changed

+64
-63
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 & 17 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,
@@ -70,7 +72,9 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
7072
internal val diagnosticId = idCounter.createId()
7173

7274
private val subtreeManager =
73-
SubtreeManager<StateT, OutputT>(coroutineContext, diagnosticId, diagnosticListener, idCounter)
75+
SubtreeManager<StateT, OutputT>(
76+
coroutineContext, ::applyAction, diagnosticId, diagnosticListener, idCounter
77+
)
7478

7579
private val workers = ActiveStagingList<WorkerChildNode<*, *, *>>()
7680

@@ -158,20 +162,9 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
158162
*
159163
* It is an error to call this method after calling [cancel].
160164
*/
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-
165+
fun <T : Any> tick(selector: SelectBuilder<T?>) {
173166
// Listen for any child workflow updates.
174-
subtreeManager.tickChildren(selector, ::acceptUpdate)
167+
subtreeManager.tickChildren(selector)
175168

176169
// Listen for any subscription updates.
177170
workers.forEachActive { child ->
@@ -188,7 +181,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
188181
} else {
189182
val update = child.acceptUpdate(valueOrDone.value)
190183
@Suppress("UNCHECKED_CAST")
191-
acceptUpdate(update as WorkflowAction<StateT, OutputT>)
184+
return@onReceive applyAction(update as WorkflowAction<StateT, OutputT>)
192185
}
193186
}
194187
}
@@ -198,7 +191,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
198191
with(selector) {
199192
eventActionsChannel.onReceive { action ->
200193
diagnosticListener?.onSinkReceived(diagnosticId, action)
201-
acceptUpdate(action)
194+
return@onReceive applyAction(action)
202195
}
203196
}
204197
}
@@ -256,6 +249,18 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
256249
lastProps = newProps
257250
}
258251

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

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

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -165,15 +165,15 @@ class WorkflowNodeTest {
165165
return ""
166166
}
167167
}
168-
val node = WorkflowNode(workflow.id(), workflow, "", null, context)
168+
val node = WorkflowNode(workflow.id(), workflow, "", null, context, { "tick:$it" })
169169
node.render(workflow, "")
170170

171171
sink.send("event")
172172

173173
val result = runBlocking {
174174
withTimeout(10) {
175175
select<String?> {
176-
node.tick(this) { "tick:$it" }
176+
node.tick(this)
177177
}
178178
}
179179
}
@@ -200,22 +200,22 @@ class WorkflowNodeTest {
200200
return ""
201201
}
202202
}
203-
val node = WorkflowNode(workflow.id(), workflow, "", null, context)
203+
val node = WorkflowNode(workflow.id(), workflow, "", null, context, { "tick:$it" })
204204
node.render(workflow, "")
205205

206206
sink.send("event")
207207
sink.send("event2")
208208

209209
val result = runBlocking {
210210
withTimeout(10) {
211-
List(2) { i ->
211+
List(2) {
212212
select<String?> {
213-
node.tick(this) { "tick$i:$it" }
213+
node.tick(this)
214214
}
215215
}
216216
}
217217
}
218-
assertEquals(listOf("tick0:event", "tick1:event2"), result)
218+
assertEquals(listOf("tick:event", "tick:event2"), result)
219219
}
220220

221221
@Test fun `send allows subsequent events on same rendering`() {
@@ -312,7 +312,7 @@ class WorkflowNodeTest {
312312
try {
313313
withTimeout(1) {
314314
select<String?> {
315-
node.tick(this) { it }
315+
node.tick(this)
316316
}
317317
}
318318
fail("Expected exception")
@@ -324,7 +324,7 @@ class WorkflowNodeTest {
324324

325325
withTimeout(1) {
326326
select<String?> {
327-
node.tick(this) { it }
327+
node.tick(this)
328328
}
329329
}
330330
}
@@ -379,7 +379,7 @@ class WorkflowNodeTest {
379379
// This tick will process the event handler, it won't close the channel yet.
380380
withTimeout(1) {
381381
select<String?> {
382-
node.tick(this) { it }
382+
node.tick(this)
383383
}
384384
}
385385

@@ -739,7 +739,8 @@ class WorkflowNodeTest {
739739
snapshot = null,
740740
baseContext = Unconfined,
741741
parentDiagnosticId = 42,
742-
diagnosticListener = listener
742+
diagnosticListener = listener,
743+
emitOutputToParent = { null }
743744
)
744745
node.render(workflow.asStatefulWorkflow(), "new props")
745746
listener.consumeEvents()
@@ -749,7 +750,7 @@ class WorkflowNodeTest {
749750
// update.
750751
launch(start = UNDISPATCHED) {
751752
select<String?> {
752-
node.tick(this) { null }
753+
node.tick(this)
753754
}
754755
}
755756
yield()
@@ -795,7 +796,8 @@ class WorkflowNodeTest {
795796
snapshot = null,
796797
baseContext = Unconfined,
797798
parentDiagnosticId = 42,
798-
diagnosticListener = listener
799+
diagnosticListener = listener,
800+
emitOutputToParent = { null }
799801
)
800802
val rendering = node.render(workflow.asStatefulWorkflow(), "new props")
801803
listener.consumeEvents()
@@ -805,7 +807,7 @@ class WorkflowNodeTest {
805807
// update.
806808
launch(start = UNDISPATCHED) {
807809
select<String?> {
808-
node.tick(this) { null }
810+
node.tick(this)
809811
}
810812
}
811813
yield()

0 commit comments

Comments
 (0)