Skip to content

Commit 7bb3536

Browse files
GUWT step 1: Implement runningSideEffect API
First part of Kotlin implementation of #1021.
1 parent 6365b25 commit 7bb3536

File tree

12 files changed

+457
-49
lines changed

12 files changed

+457
-49
lines changed

workflow-core/api/workflow-core.api

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public abstract interface class com/squareup/workflow/RenderContext {
2727
public abstract fun makeActionSink ()Lcom/squareup/workflow/Sink;
2828
public abstract fun onEvent (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1;
2929
public abstract fun renderChild (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
30+
public abstract fun runningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
3031
public abstract fun runningWorker (Lcom/squareup/workflow/Worker;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
3132
}
3233

workflow-core/src/main/java/com/squareup/workflow/RenderContext.kt

+15
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,21 @@ interface RenderContext<StateT, in OutputT : Any> {
116116
key: String = "",
117117
handler: (T) -> WorkflowAction<StateT, OutputT>
118118
)
119+
120+
/**
121+
* Ensures [sideEffect] is running with the given [key].
122+
*
123+
* @param key The string key that is used to distinguish between side effects.
124+
* @param sideEffect A suspending lambda that will be started the first time this method called
125+
* with the given key, and
126+
* [cancelled](https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html)
127+
* after it is no longer called with the given key (or this workflow is torn down, whichever
128+
* happens first).
129+
*/
130+
fun runningSideEffect(
131+
key: String,
132+
sideEffect: suspend () -> Unit
133+
)
119134
}
120135

121136
/**

workflow-runtime/api/workflow-runtime.api

+6-1
Original file line numberDiff line numberDiff line change
@@ -235,13 +235,14 @@ public final class com/squareup/workflow/diagnostic/WorkflowUpdateDebugInfo$Sour
235235
}
236236

237237
public final class com/squareup/workflow/internal/RealRenderContext : com/squareup/workflow/RenderContext, com/squareup/workflow/Sink {
238-
public fun <init> (Lcom/squareup/workflow/internal/RealRenderContext$Renderer;Lcom/squareup/workflow/internal/RealRenderContext$WorkerRunner;Lkotlinx/coroutines/channels/SendChannel;)V
238+
public fun <init> (Lcom/squareup/workflow/internal/RealRenderContext$Renderer;Lcom/squareup/workflow/internal/RealRenderContext$WorkerRunner;Lcom/squareup/workflow/internal/RealRenderContext$SideEffectRunner;Lkotlinx/coroutines/channels/SendChannel;)V
239239
public final fun freeze ()V
240240
public fun getActionSink ()Lcom/squareup/workflow/Sink;
241241
public fun makeActionSink ()Lcom/squareup/workflow/Sink;
242242
public fun onEvent (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow/EventHandler;
243243
public synthetic fun onEvent (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1;
244244
public fun renderChild (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
245+
public fun runningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
245246
public fun runningWorker (Lcom/squareup/workflow/Worker;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
246247
public fun send (Lcom/squareup/workflow/WorkflowAction;)V
247248
public synthetic fun send (Ljava/lang/Object;)V
@@ -251,6 +252,10 @@ public abstract interface class com/squareup/workflow/internal/RealRenderContext
251252
public abstract fun render (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
252253
}
253254

255+
public abstract interface class com/squareup/workflow/internal/RealRenderContext$SideEffectRunner {
256+
public abstract fun runningSideEffect (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
257+
}
258+
254259
public abstract interface class com/squareup/workflow/internal/RealRenderContext$WorkerRunner {
255260
public abstract fun runningWorker (Lcom/squareup/workflow/Worker;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
256261
}

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

+16
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import kotlinx.coroutines.channels.SendChannel
3333
class RealRenderContext<StateT, OutputT : Any>(
3434
private val renderer: Renderer<StateT, OutputT>,
3535
private val workerRunner: WorkerRunner<StateT, OutputT>,
36+
private val sideEffectRunner: SideEffectRunner,
3637
private val eventActionsChannel: SendChannel<WorkflowAction<StateT, OutputT>>
3738
) : RenderContext<StateT, OutputT>, Sink<WorkflowAction<StateT, OutputT>> {
3839

@@ -53,6 +54,13 @@ class RealRenderContext<StateT, OutputT : Any>(
5354
)
5455
}
5556

57+
interface SideEffectRunner {
58+
fun runningSideEffect(
59+
key: String,
60+
sideEffect: suspend () -> Unit
61+
)
62+
}
63+
5664
/**
5765
* False during the current render call, set to true once this node is finished rendering.
5866
*
@@ -104,6 +112,14 @@ class RealRenderContext<StateT, OutputT : Any>(
104112
workerRunner.runningWorker(worker, key, handler)
105113
}
106114

115+
override fun runningSideEffect(
116+
key: String,
117+
sideEffect: suspend () -> Unit
118+
) {
119+
checkNotFrozen()
120+
sideEffectRunner.runningSideEffect(key, sideEffect)
121+
}
122+
107123
/**
108124
* Freezes this context so that any further calls to this context will throw.
109125
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2020 Square Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.squareup.workflow.internal
17+
18+
import com.squareup.workflow.internal.InlineLinkedList.InlineListNode
19+
import kotlinx.coroutines.Job
20+
21+
/**
22+
* TODO write documentation
23+
*/
24+
internal class SideEffectNode(
25+
val key: String,
26+
val job: Job
27+
) : InlineListNode<SideEffectNode> {
28+
29+
override var nextListNode: SideEffectNode? = null
30+
}

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

+38-2
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,18 @@ import com.squareup.workflow.applyTo
2525
import com.squareup.workflow.diagnostic.IdCounter
2626
import com.squareup.workflow.diagnostic.WorkflowDiagnosticListener
2727
import com.squareup.workflow.diagnostic.createId
28+
import com.squareup.workflow.internal.RealRenderContext.SideEffectRunner
2829
import com.squareup.workflow.internal.RealRenderContext.WorkerRunner
2930
import kotlinx.coroutines.CancellationException
3031
import kotlinx.coroutines.CoroutineName
3132
import kotlinx.coroutines.CoroutineScope
33+
import kotlinx.coroutines.CoroutineStart.LAZY
3234
import kotlinx.coroutines.Job
3335
import kotlinx.coroutines.cancel
3436
import kotlinx.coroutines.channels.Channel
3537
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
38+
import kotlinx.coroutines.launch
39+
import kotlinx.coroutines.plus
3640
import kotlinx.coroutines.selects.SelectBuilder
3741
import okio.ByteString
3842
import kotlin.coroutines.CoroutineContext
@@ -63,7 +67,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
6367
private val idCounter: IdCounter? = null,
6468
initialState: StateT? = null,
6569
private val workerContext: CoroutineContext = EmptyCoroutineContext
66-
) : CoroutineScope, WorkerRunner<StateT, OutputT> {
70+
) : CoroutineScope, WorkerRunner<StateT, OutputT>, SideEffectRunner {
6771

6872
/**
6973
* Context that has a job that will live as long as this node.
@@ -84,6 +88,8 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
8488

8589
private val workers = ActiveStagingList<WorkerChildNode<*, *, *>>()
8690

91+
private val sideEffects = ActiveStagingList<SideEffectNode>()
92+
8793
private var state: StateT
8894

8995
private var lastProps: PropsT = initialProps
@@ -144,7 +150,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
144150
key: String,
145151
handler: (T) -> WorkflowAction<StateT, OutputT>
146152
) {
147-
// Prevent duplicate workflows with the same key.
153+
// Prevent duplicate workers with the same key.
148154
workers.forEachStaging {
149155
require(!(it.matches(worker, key))) {
150156
"Expected keys to be unique for $worker: key=$key"
@@ -159,6 +165,21 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
159165
stagedWorker.setHandler(handler)
160166
}
161167

168+
override fun runningSideEffect(
169+
key: String,
170+
sideEffect: suspend () -> Unit
171+
) {
172+
// Prevent duplicate side effects with the same key.
173+
sideEffects.forEachStaging {
174+
require(key != it.key) { "Expected side effect keys to be unique: $key" }
175+
}
176+
177+
sideEffects.retainOrCreate(
178+
predicate = { key == it.key },
179+
create = { createSideEffectNode(key, sideEffect) }
180+
)
181+
}
182+
162183
/**
163184
* Gets the next [output][OutputT] from the state machine.
164185
*
@@ -229,6 +250,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
229250
val context = RealRenderContext(
230251
renderer = subtreeManager,
231252
workerRunner = this,
253+
sideEffectRunner = this,
232254
eventActionsChannel = eventActionsChannel
233255
)
234256
diagnosticListener?.onBeforeWorkflowRendered(diagnosticId, props, state)
@@ -239,6 +261,10 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
239261
// Tear down workflows and workers that are obsolete.
240262
subtreeManager.commitRenderedChildren()
241263
workers.commitStaging { it.channel.cancel() }
264+
// Side effect jobs are launched lazily, since they can send actions to the sink, and must not
265+
// be started until after context is frozen.
266+
sideEffects.forEachStaging { it.job.start() }
267+
sideEffects.commitStaging { it.job.cancel() }
242268

243269
return rendering
244270
}
@@ -282,6 +308,16 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
282308
return WorkerChildNode(worker, key, workerChannel, handler = handler)
283309
}
284310

311+
private fun createSideEffectNode(
312+
key: String,
313+
sideEffect: suspend () -> Unit
314+
): SideEffectNode {
315+
// workerContext is guaranteed to not contain a Job.
316+
val scope = this + CoroutineName("sideEffect[$key] for $id") + workerContext
317+
val job = scope.launch(start = LAZY) { sideEffect() }
318+
return SideEffectNode(key, job)
319+
}
320+
285321
private fun ByteString.restoreState(): Snapshot? {
286322
val (snapshotToRestoreFrom, childSnapshots) = parseTreeSnapshot(this)
287323
subtreeManager.restoreChildrenFromSnapshots(childSnapshots)

0 commit comments

Comments
 (0)