Skip to content

Commit cad6331

Browse files
Introduce WorkflowInterceptor.
See the documentation on the `WorkflowInterceptor` interface for an explanation of what this type is and how it can be used. It is intended to replace `WorkflowDiagnosticListener`, as well as the custom double-rendering behavior used to verify render idempotency in testing infrastructure. Closes #33.
1 parent 5b73ee7 commit cad6331

19 files changed

+1490
-80
lines changed

Diff for: workflow-runtime/api/workflow-runtime.api

+45-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
public final class com/squareup/workflow/LaunchWorkflowKt {
22
public static final fun launchWorkflowIn (Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow/Workflow;Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow/Snapshot;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
33
public static synthetic fun launchWorkflowIn$default (Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow/Workflow;Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow/Snapshot;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/lang/Object;
4-
public static final fun renderWorkflowIn (Lcom/squareup/workflow/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Lcom/squareup/workflow/TreeSnapshot;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow;
5-
public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Lcom/squareup/workflow/TreeSnapshot;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow;
4+
public static final fun renderWorkflowIn (Lcom/squareup/workflow/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Lcom/squareup/workflow/TreeSnapshot;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Ljava/util/List;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/StateFlow;
5+
public static synthetic fun renderWorkflowIn$default (Lcom/squareup/workflow/Workflow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/StateFlow;Lcom/squareup/workflow/TreeSnapshot;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Ljava/util/List;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow;
6+
}
7+
8+
public final class com/squareup/workflow/NoopWorkflowInterceptor : com/squareup/workflow/WorkflowInterceptor {
9+
public static final field INSTANCE Lcom/squareup/workflow/NoopWorkflowInterceptor;
10+
public fun onInitialState (Ljava/lang/Object;Lcom/squareup/workflow/Snapshot;Lkotlin/jvm/functions/Function2;Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
11+
public fun onPropsChanged (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
12+
public fun onRender (Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow/RenderContext;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
13+
public fun onSessionStarted (Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;)V
14+
public fun onSnapshotState (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow/Snapshot;
615
}
716

817
public final class com/squareup/workflow/RenderingAndSnapshot {
@@ -18,6 +27,17 @@ public final class com/squareup/workflow/RenderingAndSnapshot {
1827
public fun toString ()Ljava/lang/String;
1928
}
2029

30+
public class com/squareup/workflow/SimpleLoggingWorkflowInterceptor : com/squareup/workflow/WorkflowInterceptor {
31+
public fun <init> ()V
32+
protected fun logBegin (Ljava/lang/String;)V
33+
protected fun logEnd (Ljava/lang/String;)V
34+
public fun onInitialState (Ljava/lang/Object;Lcom/squareup/workflow/Snapshot;Lkotlin/jvm/functions/Function2;Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
35+
public fun onPropsChanged (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
36+
public fun onRender (Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow/RenderContext;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
37+
public fun onSessionStarted (Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;)V
38+
public fun onSnapshotState (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow/Snapshot;
39+
}
40+
2141
public final class com/squareup/workflow/TreeSnapshot {
2242
public static final field Companion Lcom/squareup/workflow/TreeSnapshot$Companion;
2343
public fun equals (Ljava/lang/Object;)Z
@@ -31,6 +51,29 @@ public final class com/squareup/workflow/TreeSnapshot$Companion {
3151
public final fun parse (Lokio/ByteString;)Lcom/squareup/workflow/TreeSnapshot;
3252
}
3353

54+
public abstract interface class com/squareup/workflow/WorkflowInterceptor {
55+
public abstract fun onInitialState (Ljava/lang/Object;Lcom/squareup/workflow/Snapshot;Lkotlin/jvm/functions/Function2;Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
56+
public abstract fun onPropsChanged (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
57+
public abstract fun onRender (Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow/RenderContext;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
58+
public abstract fun onSessionStarted (Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;)V
59+
public abstract fun onSnapshotState (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow/Snapshot;
60+
}
61+
62+
public final class com/squareup/workflow/WorkflowInterceptor$DefaultImpls {
63+
public static fun onInitialState (Lcom/squareup/workflow/WorkflowInterceptor;Ljava/lang/Object;Lcom/squareup/workflow/Snapshot;Lkotlin/jvm/functions/Function2;Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
64+
public static fun onPropsChanged (Lcom/squareup/workflow/WorkflowInterceptor;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
65+
public static fun onRender (Lcom/squareup/workflow/WorkflowInterceptor;Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow/RenderContext;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
66+
public static fun onSessionStarted (Lcom/squareup/workflow/WorkflowInterceptor;Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;)V
67+
public static fun onSnapshotState (Lcom/squareup/workflow/WorkflowInterceptor;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow/Snapshot;
68+
}
69+
70+
public abstract interface class com/squareup/workflow/WorkflowInterceptor$WorkflowSession {
71+
public abstract fun getIdentifier ()Lcom/squareup/workflow/WorkflowIdentifier;
72+
public abstract fun getParent ()Lcom/squareup/workflow/WorkflowInterceptor$WorkflowSession;
73+
public abstract fun getRenderKey ()Ljava/lang/String;
74+
public abstract fun getSessionId ()J
75+
}
76+
3477
public final class com/squareup/workflow/WorkflowSession {
3578
public fun <init> (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;)V
3679
public synthetic fun <init> (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;ILkotlin/jvm/internal/DefaultConstructorMarker;)V

Diff for: workflow-runtime/src/main/java/com/squareup/workflow/LaunchWorkflow.kt

+11-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import com.squareup.workflow.diagnostic.WorkflowDiagnosticListener
1919
import com.squareup.workflow.internal.RealWorkflowLoop
2020
import com.squareup.workflow.internal.WorkflowLoop
2121
import com.squareup.workflow.internal.WorkflowRunner
22+
import com.squareup.workflow.internal.chained
2223
import com.squareup.workflow.internal.id
2324
import com.squareup.workflow.internal.unwrapCancellationCause
2425
import kotlinx.coroutines.CancellationException
@@ -198,6 +199,11 @@ fun <PropsT, OutputT : Any, RenderingT, RunnerT> launchWorkflowIn(
198199
* An optional [WorkflowDiagnosticListener] that will receive all diagnostic events from the
199200
* runtime.
200201
*
202+
* @param interceptors
203+
* An optional list of [WorkflowInterceptor]s that will wrap every workflow rendered by the runtime.
204+
* Interceptors will be invoked in 0-to-`length` order: the interceptor at index 0 will process the
205+
* workflow first, then the interceptor at index 1, etc.
206+
*
201207
* @param onOutput
202208
* A function that will be called whenever the root workflow emits an [OutputT]. This is a suspend
203209
* function, and is invoked synchronously within the runtime: if it suspends, the workflow runtime
@@ -216,11 +222,15 @@ fun <PropsT, OutputT : Any, RenderingT> renderWorkflowIn(
216222
props: StateFlow<PropsT>,
217223
initialSnapshot: TreeSnapshot = TreeSnapshot.NONE,
218224
diagnosticListener: WorkflowDiagnosticListener? = null,
225+
interceptors: List<WorkflowInterceptor> = emptyList(),
219226
onOutput: suspend (OutputT) -> Unit
220227
): StateFlow<RenderingAndSnapshot<RenderingT>> {
228+
val chainedInterceptor = interceptors.chained()
229+
221230
// The runtime started event must be emitted before any other events.
222231
diagnosticListener?.onRuntimeStarted(scope, workflow.id().typeDebugString)
223-
val runner = WorkflowRunner(scope, workflow, props, initialSnapshot, diagnosticListener)
232+
val runner =
233+
WorkflowRunner(scope, workflow, props, initialSnapshot, diagnosticListener, chainedInterceptor)
224234

225235
fun emitRuntimeStopped(cause: Throwable? = null) {
226236
// Any time the runtime needs to be stopped, we need to first cancel the root node's scope and
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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
17+
18+
import com.squareup.workflow.WorkflowInterceptor.WorkflowSession
19+
import kotlinx.coroutines.CoroutineScope
20+
import kotlinx.coroutines.Job
21+
22+
/**
23+
* A [WorkflowInterceptor] that just prints all method calls using [println].
24+
*/
25+
@OptIn(ExperimentalWorkflowApi::class)
26+
open class SimpleLoggingWorkflowInterceptor : WorkflowInterceptor {
27+
override fun onSessionStarted(
28+
workflowScope: CoroutineScope,
29+
session: WorkflowSession
30+
) {
31+
logBegin("onInstanceStarted($workflowScope, $session)")
32+
workflowScope.coroutineContext[Job]!!.invokeOnCompletion {
33+
logEnd("onInstanceStarted($session)")
34+
}
35+
}
36+
37+
override fun <P, S> onInitialState(
38+
props: P,
39+
snapshot: Snapshot?,
40+
proceed: (P, Snapshot?) -> S,
41+
session: WorkflowSession
42+
): S = logMethod("onInitialState", props, snapshot, session) {
43+
proceed(props, snapshot)
44+
}
45+
46+
override fun <P, S> onPropsChanged(
47+
old: P,
48+
new: P,
49+
state: S,
50+
proceed: (P, P, S) -> S,
51+
session: WorkflowSession
52+
): S = logMethod("onPropsChanged", old, new, state, session) {
53+
proceed(old, new, state)
54+
}
55+
56+
override fun <P, S, O : Any, R> onRender(
57+
props: P,
58+
state: S,
59+
context: RenderContext<S, O>,
60+
proceed: (P, S, RenderContext<S, O>) -> R,
61+
session: WorkflowSession
62+
): R = logMethod("onRender", props, state, session) {
63+
proceed(props, state, context)
64+
}
65+
66+
override fun <S> onSnapshotState(
67+
state: S,
68+
proceed: (S) -> Snapshot,
69+
session: WorkflowSession
70+
): Snapshot = logMethod("onSnapshotState", state, session) {
71+
proceed(state)
72+
}
73+
74+
private inline fun <T> logMethod(
75+
name: String,
76+
vararg args: Any?,
77+
block: () -> T
78+
): T {
79+
val text = "$name(${args.joinToString()})"
80+
logBegin(text)
81+
return block().also {
82+
logEnd("$text = $it")
83+
}
84+
}
85+
86+
/**
87+
* Called with descriptions of every event. Default implementation just calls [kotlin.io.println].
88+
*/
89+
protected open fun logBegin(text: String) {
90+
println("START| $text")
91+
}
92+
93+
/**
94+
* Called with descriptions of every event. Default implementation just calls [kotlin.io.println].
95+
*/
96+
protected open fun logEnd(text: String) {
97+
println(" END| $text")
98+
}
99+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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
17+
18+
import com.squareup.workflow.WorkflowInterceptor.WorkflowSession
19+
import kotlinx.coroutines.CoroutineScope
20+
21+
/**
22+
* Provides hooks into the workflow runtime that can be used to instrument or modify the behavior
23+
* of workflows.
24+
*
25+
* This interface's methods mirror the methods of [StatefulWorkflow]. It also has one additional
26+
* method, [onSessionStarted], that is notified when a workflow is started. Each method returns the
27+
* same thing as the corresponding method on [StatefulWorkflow], and receives the same parameters
28+
* as well as two extra parameters:
29+
*
30+
* - **`proceed`** – A function that _exactly_ mirrors the corresponding function on
31+
* [StatefulWorkflow], accepting the same parameters and returning the same thing. An interceptor
32+
* can call this function to run the actual workflow, but it may also decide to not call it at
33+
* all, or call it multiple times.
34+
* - **`session`** – A [WorkflowSession] object that can be queried for information about the
35+
* workflow being intercepted.
36+
*
37+
* All methods have default no-op implementations.
38+
*
39+
* ## Workflow sessions
40+
*
41+
* A single workflow may be rendered by different parents at the same time, or the same parent at
42+
* different, disjoint times. Each continuous sequence of renderings of a particular workflow type,
43+
* with the same key passed to [RenderContext.renderChild], is called an "session" of that
44+
* workflow. The workflow's [StatefulWorkflow.initialState] method will be called at the start of
45+
* the session, and its state will be maintained by the runtime until the session is finished.
46+
* Each session is identified by the [WorkflowSession] object passed into the corresponding method
47+
* in a [WorkflowInterceptor].
48+
*
49+
* In addition to the [WorkflowIdentifier] of the type of the workflow being rendered, this object
50+
* also knows the [key][WorkflowSession.renderKey] used to render the workflow and the
51+
* [WorkflowSession] of the [parent][WorkflowSession.parent] workflow that is rendering it.
52+
*
53+
* Each session is also assigned a numerical ID that uniquely identifies the session over the
54+
* life of the entire runtime. This value will remain constant as long as the workflow's parent is
55+
* rendering it, and then it will never be used again. If this workflow stops being rendered, and
56+
* then starts again, the value will be different.
57+
*/
58+
@ExperimentalWorkflowApi
59+
interface WorkflowInterceptor {
60+
61+
/**
62+
* Called when the session is starting, before [onInitialState].
63+
*
64+
* @param workflowScope The [CoroutineScope] that will be used for any side effects the workflow
65+
* runs, as well as the parent for any workflows it renders.
66+
*/
67+
fun onSessionStarted(
68+
workflowScope: CoroutineScope,
69+
session: WorkflowSession
70+
) = Unit
71+
72+
/**
73+
* Intercepts calls to [StatefulWorkflow.initialState].
74+
*/
75+
fun <P, S> onInitialState(
76+
props: P,
77+
snapshot: Snapshot?,
78+
proceed: (P, Snapshot?) -> S,
79+
session: WorkflowSession
80+
): S = proceed(props, snapshot)
81+
82+
/**
83+
* Intercepts calls to [StatefulWorkflow.onPropsChanged].
84+
*/
85+
fun <P, S> onPropsChanged(
86+
old: P,
87+
new: P,
88+
state: S,
89+
proceed: (P, P, S) -> S,
90+
session: WorkflowSession
91+
): S = proceed(old, new, state)
92+
93+
/**
94+
* Intercepts calls to [StatefulWorkflow.render].
95+
*/
96+
fun <P, S, O : Any, R> onRender(
97+
props: P,
98+
state: S,
99+
context: RenderContext<S, O>,
100+
proceed: (P, S, RenderContext<S, O>) -> R,
101+
session: WorkflowSession
102+
): R = proceed(props, state, context)
103+
104+
/**
105+
* Intercepts calls to [StatefulWorkflow.snapshotState].
106+
*/
107+
fun <S> onSnapshotState(
108+
state: S,
109+
proceed: (S) -> Snapshot,
110+
session: WorkflowSession
111+
): Snapshot = proceed(state)
112+
113+
/**
114+
* Information about the session of a workflow in the runtime that a [WorkflowInterceptor] method
115+
* is intercepting.
116+
*/
117+
@ExperimentalWorkflowApi
118+
interface WorkflowSession {
119+
/** The [WorkflowIdentifier] that represents the type of this workflow. */
120+
val identifier: WorkflowIdentifier
121+
122+
/**
123+
* The string key argument that was passed to [RenderContext.renderChild] to render this
124+
* workflow.
125+
*/
126+
val renderKey: String
127+
128+
/**
129+
* A unique value that identifies the currently-running session of this workflow in the
130+
* runtime. See the documentation on [WorkflowInterceptor] for more information about what this
131+
* value represents.
132+
*/
133+
val sessionId: Long
134+
135+
/** The parent [WorkflowSession] of this workflow, or null if this is the root workflow. */
136+
val parent: WorkflowSession?
137+
}
138+
}
139+
140+
/** A [WorkflowInterceptor] that does not intercept anything. */
141+
@ExperimentalWorkflowApi
142+
object NoopWorkflowInterceptor : WorkflowInterceptor
143+
144+
/**
145+
* Returns a [StatefulWorkflow] that will intercept all calls to [workflow] via this
146+
* [WorkflowInterceptor].
147+
*/
148+
@OptIn(ExperimentalWorkflowApi::class)
149+
internal fun <P, S, O : Any, R> WorkflowInterceptor.intercept(
150+
workflow: StatefulWorkflow<P, S, O, R>,
151+
workflowSession: WorkflowSession
152+
): StatefulWorkflow<P, S, O, R> = if (this === NoopWorkflowInterceptor) {
153+
workflow
154+
} else {
155+
object : StatefulWorkflow<P, S, O, R>() {
156+
override fun initialState(
157+
props: P,
158+
snapshot: Snapshot?
159+
): S = onInitialState(props, snapshot, workflow::initialState, workflowSession)
160+
161+
override fun onPropsChanged(
162+
old: P,
163+
new: P,
164+
state: S
165+
): S = onPropsChanged(old, new, state, workflow::onPropsChanged, workflowSession)
166+
167+
override fun render(
168+
props: P,
169+
state: S,
170+
context: RenderContext<S, O>
171+
): R = onRender(props, state, context, workflow::render, workflowSession)
172+
173+
override fun snapshotState(state: S): Snapshot =
174+
onSnapshotState(state, workflow::snapshotState, workflowSession)
175+
176+
override fun toString(): String = "InterceptedWorkflow($workflow, $this@intercept)"
177+
}
178+
}

0 commit comments

Comments
 (0)