@@ -6,11 +6,14 @@ import com.squareup.workflow1.NoopWorkflowInterceptor
6
6
import com.squareup.workflow1.RenderContext
7
7
import com.squareup.workflow1.RuntimeConfig
8
8
import com.squareup.workflow1.RuntimeConfigOptions
9
+ import com.squareup.workflow1.RuntimeConfigOptions.PARTIAL_TREE_RENDERING
10
+ import com.squareup.workflow1.RuntimeConfigOptions.RENDER_ONLY_WHEN_STATE_CHANGES
9
11
import com.squareup.workflow1.StatefulWorkflow
10
12
import com.squareup.workflow1.TreeSnapshot
11
13
import com.squareup.workflow1.Workflow
12
14
import com.squareup.workflow1.WorkflowAction
13
15
import com.squareup.workflow1.WorkflowExperimentalApi
16
+ import com.squareup.workflow1.WorkflowExperimentalRuntime
14
17
import com.squareup.workflow1.WorkflowIdentifier
15
18
import com.squareup.workflow1.WorkflowInterceptor
16
19
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
@@ -33,6 +36,7 @@ import kotlinx.coroutines.launch
33
36
import kotlinx.coroutines.plus
34
37
import kotlinx.coroutines.selects.SelectBuilder
35
38
import kotlin.coroutines.CoroutineContext
39
+ import kotlin.jvm.JvmInline
36
40
37
41
/* *
38
42
* A node in a state machine tree. Manages the actual state for a given [Workflow].
@@ -44,7 +48,7 @@ import kotlin.coroutines.CoroutineContext
44
48
* hard-coded values added to worker contexts. It must not contain a [Job] element (it would violate
45
49
* structured concurrency).
46
50
*/
47
- @OptIn(WorkflowExperimentalApi ::class )
51
+ @OptIn(WorkflowExperimentalApi ::class , WorkflowExperimentalRuntime :: class )
48
52
internal class WorkflowNode <PropsT , StateT , OutputT , RenderingT >(
49
53
val id : WorkflowNodeId ,
50
54
workflow : StatefulWorkflow <PropsT , StateT , OutputT , RenderingT >,
@@ -86,9 +90,11 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
86
90
)
87
91
private val sideEffects = ActiveStagingList <SideEffectNode >()
88
92
private var lastProps: PropsT = initialProps
93
+ private var lastRendering: Box <RenderingT > = Box ()
89
94
private val eventActionsChannel =
90
95
Channel <WorkflowAction <PropsT , StateT , OutputT >>(capacity = UNLIMITED )
91
96
private var state: StateT
97
+ private var subtreeStateDidChange: Boolean = true
92
98
93
99
private val baseRenderContext = RealRenderContext (
94
100
renderer = subtreeManager,
@@ -211,12 +217,14 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
211
217
*/
212
218
private fun updateCachedWorkflowInstance (
213
219
workflow : StatefulWorkflow <PropsT , StateT , OutputT , RenderingT >
214
- ) {
220
+ ): Boolean {
215
221
if (workflow != = cachedWorkflowInstance) {
216
222
// The instance has changed.
217
223
cachedWorkflowInstance = workflow
218
224
interceptedWorkflowInstance = interceptor.intercept(cachedWorkflowInstance, this )
225
+ return true
219
226
}
227
+ return false
220
228
}
221
229
222
230
/* *
@@ -227,39 +235,59 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
227
235
workflow : StatefulWorkflow <PropsT , StateT , OutputT , RenderingT >,
228
236
props : PropsT
229
237
): RenderingT {
230
- updateCachedWorkflowInstance(workflow)
231
- updatePropsAndState(props)
238
+ val didUpdateCachedInstance = updatePropsAndState(props, workflow)
232
239
233
- baseRenderContext.unfreeze()
234
- val rendering = interceptedWorkflowInstance.render(props, state, context)
235
- baseRenderContext.freeze()
240
+ if (! runtimeConfig.contains(PARTIAL_TREE_RENDERING ) ||
241
+ ! lastRendering.isInitialized ||
242
+ subtreeStateDidChange
243
+ ) {
244
+ if (! didUpdateCachedInstance) {
245
+ // If we haven't already updated the cached instance, better do it now!
246
+ updateCachedWorkflowInstance(workflow)
247
+ }
248
+ baseRenderContext.unfreeze()
249
+ lastRendering = Box (interceptedWorkflowInstance.render(props, state, context))
250
+ baseRenderContext.freeze()
236
251
237
- workflowTracer.trace(" UpdateRuntimeTree" ) {
238
- // Tear down workflows and workers that are obsolete.
239
- subtreeManager.commitRenderedChildren()
240
- // Side effect jobs are launched lazily, since they can send actions to the sink, and can only
241
- // be started after context is frozen.
242
- sideEffects.forEachStaging { it.job.start() }
243
- sideEffects.commitStaging { it.job.cancel() }
252
+ workflowTracer.trace(" UpdateRuntimeTree" ) {
253
+ // Tear down workflows and workers that are obsolete.
254
+ subtreeManager.commitRenderedChildren()
255
+ // Side effect jobs are launched lazily, since they can send actions to the sink, and can only
256
+ // be started after context is frozen.
257
+ sideEffects.forEachStaging { it.job.start() }
258
+ sideEffects.commitStaging { it.job.cancel() }
259
+ }
260
+ // After we have rendered this subtree, we need another action in order for us to be
261
+ // considered dirty again.
262
+ subtreeStateDidChange = false
244
263
}
245
264
246
- return rendering
265
+ return lastRendering.getOrThrow()
247
266
}
248
267
268
+ /* *
269
+ * @return true if the [interceptedWorkflowInstance] has been updated, false otherwise.
270
+ */
249
271
private fun updatePropsAndState (
250
- newProps : PropsT
251
- ) {
272
+ newProps : PropsT ,
273
+ workflow : StatefulWorkflow <PropsT , StateT , OutputT , RenderingT >,
274
+ ): Boolean {
275
+ var didUpdateCachedInstance = false
252
276
if (newProps != lastProps) {
277
+ didUpdateCachedInstance = updateCachedWorkflowInstance(workflow)
253
278
val newState = interceptedWorkflowInstance.onPropsChanged(lastProps, newProps, state)
254
279
state = newState
280
+ subtreeStateDidChange = true
255
281
}
256
282
lastProps = newProps
283
+ return didUpdateCachedInstance
257
284
}
258
285
259
286
/* *
260
287
* Applies [action] to this workflow's [state] and then passes the resulting [ActionApplied]
261
288
* via [emitAppliedActionToParent] to the parent, with additional information as to whether or
262
289
* not this action has changed the current node's state.
290
+ *
263
291
*/
264
292
private fun applyAction (
265
293
action : WorkflowAction <PropsT , StateT , OutputT >,
@@ -272,7 +300,13 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
272
300
// Changing state is sticky, we pass it up if it ever changed.
273
301
stateChanged = actionApplied.stateChanged || (childResult?.stateChanged ? : false )
274
302
)
275
- return if (actionApplied.output != null ) {
303
+ // Our state changed or one of our children's state changed.
304
+ subtreeStateDidChange = aggregateActionApplied.stateChanged
305
+ return if (actionApplied.output != null ||
306
+ runtimeConfig.contains(PARTIAL_TREE_RENDERING )
307
+ ) {
308
+ // If we are using the optimization, always return to the parent, so we carry a path that
309
+ // notes that the subtree did change all the way to the root.
276
310
emitAppliedActionToParent(aggregateActionApplied)
277
311
} else {
278
312
aggregateActionApplied
@@ -289,4 +323,17 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
289
323
SideEffectNode (key, job)
290
324
}
291
325
}
326
+
327
+ @JvmInline
328
+ internal value class Box <T >(private val _value : Any? = Uninitialized ) {
329
+ val isInitialized: Boolean get() = _value != = Uninitialized
330
+
331
+ @Suppress(" UNCHECKED_CAST" )
332
+ fun getOrThrow (): T {
333
+ check(isInitialized)
334
+ return _value as T
335
+ }
336
+ }
337
+
338
+ internal object Uninitialized
292
339
}
0 commit comments