Skip to content

Commit 717d484

Browse files
988: Cache RenderContext per instance
For the InterceptedRenderContext and for the StatelessWorkflow.RenderContext.
1 parent 8e40b8a commit 717d484

File tree

2 files changed

+65
-12
lines changed

2 files changed

+65
-12
lines changed

workflow-core/src/commonMain/kotlin/com/squareup/workflow1/StatelessWorkflow.kt

+37-9
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
package com.squareup.workflow1
55

6-
import kotlin.LazyThreadSafetyMode.NONE
76
import kotlin.jvm.JvmMultifileClass
87
import kotlin.jvm.JvmName
98

@@ -33,11 +32,43 @@ public abstract class StatelessWorkflow<in PropsT, out OutputT, out RenderingT>
3332
) : BaseRenderContext<@UnsafeVariance PropsT, Nothing, @UnsafeVariance OutputT> by
3433
baseContext as BaseRenderContext<PropsT, Nothing, OutputT>
3534

36-
@Suppress("UNCHECKED_CAST")
37-
private val statefulWorkflow = Workflow.stateful<PropsT, Unit, OutputT, RenderingT>(
38-
initialState = { Unit },
39-
render = { props, _ -> render(props, RenderContext(this, this@StatelessWorkflow)) }
40-
)
35+
private val statefulWorkflow: StatefulWorkflow<PropsT, Unit, OutputT, RenderingT> =
36+
object : StatefulWorkflow<PropsT, Unit, OutputT, RenderingT>() {
37+
// We want to cache the render context so that we don't have to recreate it each time
38+
// render() is called.
39+
private var cachedStatelessRenderContext:
40+
StatelessWorkflow<PropsT, OutputT, RenderingT>.RenderContext? = null
41+
42+
// We must know if the RenderContext we are passed (which is a StatefulWorkflow.RenderContext)
43+
// has changed, so keep track of it.
44+
private var canonicalStatefulRenderContext:
45+
StatefulWorkflow<PropsT, Unit, OutputT, RenderingT>.RenderContext? = null
46+
47+
override fun initialState(
48+
props: PropsT,
49+
snapshot: Snapshot?
50+
) = Unit
51+
52+
override fun render(
53+
renderProps: PropsT,
54+
renderState: Unit,
55+
context: RenderContext
56+
): RenderingT {
57+
// The `RenderContext` used *might* change - primarily in the case of our tests. E.g., The
58+
// `RenderTester` uses a special NoOp context to render twice to test for idempotency.
59+
// In order to support a changed render context but keep caching, we check to see if the
60+
// instance passed in has changed.
61+
if (cachedStatelessRenderContext == null || context !== canonicalStatefulRenderContext) {
62+
// Recreate it if the StatefulWorkflow.RenderContext we are passed has changed.
63+
cachedStatelessRenderContext = RenderContext(context, this@StatelessWorkflow)
64+
}
65+
canonicalStatefulRenderContext = context
66+
// Pass the StatelessWorkflow.RenderContext to our StatelessWorkflow.
67+
return render(renderProps, cachedStatelessRenderContext!!)
68+
}
69+
70+
override fun snapshotState(state: Unit): Snapshot? = null
71+
}
4172

4273
/**
4374
* Called at least once any time one of the following things happens:
@@ -69,9 +100,6 @@ public abstract class StatelessWorkflow<in PropsT, out OutputT, out RenderingT>
69100
/**
70101
* Satisfies the [Workflow] interface by wrapping `this` in a [StatefulWorkflow] with `Unit`
71102
* state.
72-
*
73-
* This method is called a few times per instance, but we don't need to allocate a new
74-
* [StatefulWorkflow] every time, so we store it in a private property.
75103
*/
76104
final override fun asStatefulWorkflow(): StatefulWorkflow<PropsT, *, OutputT, RenderingT> =
77105
statefulWorkflow

workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt

+28-3
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,9 @@ public object NoopWorkflowInterceptor : WorkflowInterceptor
268268
/**
269269
* Returns a [StatefulWorkflow] that will intercept all calls to [workflow] via this
270270
* [WorkflowInterceptor].
271+
*
272+
* This is called once for each instance/session of a Workflow being intercepted. So we cache the
273+
* render context for re-use within that [WorkflowSession].
271274
*/
272275
@OptIn(WorkflowExperimentalApi::class)
273276
internal fun <P, S, O, R> WorkflowInterceptor.intercept(
@@ -277,6 +280,16 @@ internal fun <P, S, O, R> WorkflowInterceptor.intercept(
277280
workflow
278281
} else {
279282
object : SessionWorkflow<P, S, O, R>() {
283+
284+
// Render context that we are passed.
285+
private var canonicalRenderContext: StatefulWorkflow<P, S, O, R>.RenderContext? = null
286+
287+
// Render context interceptor that we are passed.
288+
private var canonicalRenderContextInterceptor: RenderContextInterceptor<P, S, O>? = null
289+
290+
// Cache of the intercepted render context.
291+
private var cachedInterceptedRenderContext: StatefulWorkflow<P, S, O, R>.RenderContext? = null
292+
280293
override fun initialState(
281294
props: P,
282295
snapshot: Snapshot?,
@@ -298,9 +311,21 @@ internal fun <P, S, O, R> WorkflowInterceptor.intercept(
298311
renderState,
299312
context,
300313
proceed = { props, state, interceptor ->
301-
val interceptedContext = interceptor?.let { InterceptedRenderContext(context, it) }
302-
?: context
303-
workflow.render(props, state, RenderContext(interceptedContext, this))
314+
// The `RenderContext` used *might* change - primarily in the case of our tests. E.g., The
315+
// `RenderTester` uses a special NoOp context to render twice to test for idempotency.
316+
// In order to support a changed render context but keep caching, we check to see if the
317+
// instance passed in has changed.
318+
if (cachedInterceptedRenderContext == null || canonicalRenderContext !== context ||
319+
canonicalRenderContextInterceptor != interceptor
320+
) {
321+
val interceptedRenderContext = interceptor?.let { InterceptedRenderContext(context, it) }
322+
?: context
323+
cachedInterceptedRenderContext = RenderContext(interceptedRenderContext, this)
324+
}
325+
canonicalRenderContext = context
326+
canonicalRenderContextInterceptor = interceptor
327+
// Use the intercepted RenderContext for rendering.
328+
workflow.render(props, state, cachedInterceptedRenderContext!!)
304329
},
305330
session = workflowSession,
306331
)

0 commit comments

Comments
 (0)