Skip to content

Commit bf8451f

Browse files
Add onSessionCancelled
1 parent 6e73174 commit bf8451f

File tree

13 files changed

+432
-21
lines changed

13 files changed

+432
-21
lines changed

workflow-runtime/api/workflow-runtime.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ public final class com/squareup/workflow1/NoopWorkflowInterceptor : com/squareup
55
public fun onRender (Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/BaseRenderContext;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
66
public fun onRenderAndSnapshot (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/RenderingAndSnapshot;
77
public fun onRuntimeUpdate (Lcom/squareup/workflow1/WorkflowInterceptor$RuntimeUpdate;)V
8+
public fun onSessionCancelled (Ljava/util/concurrent/CancellationException;Ljava/util/List;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V
89
public fun onSessionStarted (Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V
910
public fun onSnapshotState (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/Snapshot;
1011
public fun onSnapshotStateWithChildren (Lkotlin/jvm/functions/Function0;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/TreeSnapshot;
@@ -34,6 +35,7 @@ public class com/squareup/workflow1/SimpleLoggingWorkflowInterceptor : com/squar
3435
public fun onRender (Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/BaseRenderContext;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
3536
public fun onRenderAndSnapshot (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/RenderingAndSnapshot;
3637
public fun onRuntimeUpdate (Lcom/squareup/workflow1/WorkflowInterceptor$RuntimeUpdate;)V
38+
public fun onSessionCancelled (Ljava/util/concurrent/CancellationException;Ljava/util/List;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V
3739
public fun onSessionStarted (Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V
3840
public fun onSnapshotState (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/Snapshot;
3941
public fun onSnapshotStateWithChildren (Lkotlin/jvm/functions/Function0;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/TreeSnapshot;
@@ -58,6 +60,7 @@ public abstract interface class com/squareup/workflow1/WorkflowInterceptor {
5860
public abstract fun onRender (Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/BaseRenderContext;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
5961
public abstract fun onRenderAndSnapshot (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/RenderingAndSnapshot;
6062
public abstract fun onRuntimeUpdate (Lcom/squareup/workflow1/WorkflowInterceptor$RuntimeUpdate;)V
63+
public abstract fun onSessionCancelled (Ljava/util/concurrent/CancellationException;Ljava/util/List;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V
6164
public abstract fun onSessionStarted (Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V
6265
public abstract fun onSnapshotState (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/Snapshot;
6366
public abstract fun onSnapshotStateWithChildren (Lkotlin/jvm/functions/Function0;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/TreeSnapshot;
@@ -69,6 +72,7 @@ public final class com/squareup/workflow1/WorkflowInterceptor$DefaultImpls {
6972
public static fun onRender (Lcom/squareup/workflow1/WorkflowInterceptor;Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/BaseRenderContext;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object;
7073
public static fun onRenderAndSnapshot (Lcom/squareup/workflow1/WorkflowInterceptor;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/RenderingAndSnapshot;
7174
public static fun onRuntimeUpdate (Lcom/squareup/workflow1/WorkflowInterceptor;Lcom/squareup/workflow1/WorkflowInterceptor$RuntimeUpdate;)V
75+
public static fun onSessionCancelled (Lcom/squareup/workflow1/WorkflowInterceptor;Ljava/util/concurrent/CancellationException;Ljava/util/List;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V
7276
public static fun onSessionStarted (Lcom/squareup/workflow1/WorkflowInterceptor;Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V
7377
public static fun onSnapshotState (Lcom/squareup/workflow1/WorkflowInterceptor;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/Snapshot;
7478
public static fun onSnapshotStateWithChildren (Lcom/squareup/workflow1/WorkflowInterceptor;Lkotlin/jvm/functions/Function0;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/TreeSnapshot;

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ package com.squareup.workflow1
22

33
import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor
44
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
5+
import kotlinx.coroutines.CancellationException
56
import kotlinx.coroutines.CoroutineScope
6-
import kotlinx.coroutines.Job
77

88
/**
99
* A [WorkflowInterceptor] that just prints all method calls using [log].
@@ -14,9 +14,14 @@ public open class SimpleLoggingWorkflowInterceptor : WorkflowInterceptor {
1414
session: WorkflowSession
1515
) {
1616
invokeSafely("logBeforeMethod") { logBeforeMethod("onInstanceStarted", session) }
17-
workflowScope.coroutineContext[Job]!!.invokeOnCompletion {
18-
invokeSafely("logAfterMethod") { logAfterMethod("onInstanceStarted", session) }
19-
}
17+
}
18+
19+
override fun <P, S, O> onSessionCancelled(
20+
cause: CancellationException?,
21+
droppedActions: List<WorkflowAction<P, S, O>>,
22+
session: WorkflowSession
23+
) {
24+
invokeSafely("logAfterMethod") { logAfterMethod("onInstanceStarted", session) }
2025
}
2126

2227
override fun <P, S> onInitialState(

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.squareup.workflow1
22

33
import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor
44
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
5+
import kotlinx.coroutines.CancellationException
56
import kotlinx.coroutines.CoroutineScope
67
import kotlinx.coroutines.Job
78
import kotlin.coroutines.CoroutineContext
@@ -75,6 +76,19 @@ public interface WorkflowInterceptor {
7576
session: WorkflowSession
7677
): Unit = Unit
7778

79+
/**
80+
* Called when the session is ending, when the Workflow's [CoroutineScope] is being cancelled.
81+
*
82+
* @param cause The cause of the cancellation if non-null.
83+
* @param droppedActions Any actions that were queued in this node's channel at the time of
84+
* cancellation.
85+
*/
86+
public fun <P, S, O> onSessionCancelled(
87+
cause: CancellationException?,
88+
droppedActions: List<WorkflowAction<P, S, O>>,
89+
session: WorkflowSession
90+
): Unit = Unit
91+
7892
/**
7993
* Intercepts calls to [StatefulWorkflow.initialState].
8094
*/

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import com.squareup.workflow1.WorkflowInterceptor
1111
import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor
1212
import com.squareup.workflow1.WorkflowInterceptor.RuntimeUpdate
1313
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
14+
import kotlinx.coroutines.CancellationException
1415
import kotlinx.coroutines.CoroutineScope
1516
import kotlin.reflect.KType
1617

@@ -32,6 +33,20 @@ internal class ChainedWorkflowInterceptor(
3233
interceptors.forEach { it.onSessionStarted(workflowScope, session) }
3334
}
3435

36+
override fun <P, S, O> onSessionCancelled(
37+
cause: CancellationException?,
38+
droppedActions: List<WorkflowAction<P, S, O>>,
39+
session: WorkflowSession
40+
) {
41+
interceptors.forEach {
42+
it.onSessionCancelled(
43+
cause = cause,
44+
droppedActions = droppedActions,
45+
session = session
46+
)
47+
}
48+
}
49+
3550
override fun <P, S> onInitialState(
3651
props: P,
3752
snapshot: Snapshot?,

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,10 +265,22 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
265265
/**
266266
* Cancels this state machine host, and any coroutines started as children of it.
267267
*
268-
* This must be called when the caller will no longer call [registerTreeActionSelectors]. It is an error to call [registerTreeActionSelectors]
269-
* after calling this method.
268+
* This must be called when the caller will no longer call [registerTreeActionSelectors].
269+
* It is an error to call [registerTreeActionSelectors] after calling this method.
270270
*/
271271
fun cancel(cause: CancellationException? = null) {
272+
val hangingActions = mutableListOf<WorkflowAction<PropsT, StateT, OutputT>>()
273+
// This will only be non-null if there is an action buffered and ready.
274+
var nextAction = eventActionsChannel.tryReceive().getOrNull()
275+
while (nextAction != null) {
276+
hangingActions.add(nextAction)
277+
nextAction = eventActionsChannel.tryReceive().getOrNull()
278+
}
279+
interceptor.onSessionCancelled(
280+
cause = cause,
281+
droppedActions = hangingActions,
282+
session = this
283+
)
272284
coroutineContext.cancel(cause)
273285
lastRendering = NullableInitBox()
274286
}

workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/SimpleLoggingWorkflowInterceptorTest.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,21 @@ internal class SimpleLoggingWorkflowInterceptorTest {
1919
interceptor.onSessionStarted(scope, TestWorkflowSession)
2020
scope.cancel()
2121

22-
assertEquals(ErrorLoggingInterceptor.EXPECTED_ERRORS, interceptor.errors)
22+
// Only the first, since we don't get cancellation directly from the scope cancellation.
23+
// For that we use onSessionCancelled()
24+
assertEquals(listOf(ErrorLoggingInterceptor.EXPECTED_ERRORS.first()), interceptor.errors)
25+
}
26+
27+
@Test fun onSessionCancelled_handles_logging_exceptions() {
28+
val interceptor = ErrorLoggingInterceptor()
29+
interceptor.onSessionCancelled<Unit, Unit, Nothing>(
30+
cause = null,
31+
droppedActions = emptyList(),
32+
session = TestWorkflowSession
33+
)
34+
35+
// Only the second error, since onSessionCancelled only calls logAfterMethod
36+
assertEquals(listOf(ErrorLoggingInterceptor.EXPECTED_ERRORS.last()), interceptor.errors)
2337
}
2438

2539
@Test fun onInitialState_handles_logging_exceptions() {

workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/WorkflowInterceptorTest.kt

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,63 @@ internal class WorkflowInterceptorTest {
175175
intercepted.render("props", "string", RenderContext(fakeContext, workflow))
176176
}
177177

178+
@Test fun intercept_intercepts_onSessionCancelled() {
179+
val recorder = RecordingWorkflowInterceptor()
180+
val session = object : WorkflowSession {
181+
override val identifier: WorkflowIdentifier = TestWorkflow.identifier
182+
override val renderKey: String = ""
183+
override val sessionId: Long = 0
184+
override val parent: WorkflowSession? = null
185+
override val runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG
186+
override val workflowTracer: WorkflowTracer? = null
187+
override val runtimeContext: CoroutineContext = EmptyCoroutineContext
188+
}
189+
190+
recorder.onSessionCancelled<String, String, String>(
191+
cause = null,
192+
droppedActions = emptyList(),
193+
session = session
194+
)
195+
196+
// SimpleLoggingWorkflowInterceptor logs "onInstanceStarted" for onSessionCancelled
197+
assertEquals(
198+
listOf("END|onInstanceStarted"),
199+
recorder.consumeEventNames()
200+
)
201+
}
202+
203+
@Test fun intercept_passes_dropped_actions_to_onSessionCancelled() {
204+
var capturedDroppedActions: List<WorkflowAction<*, *, *>>? = null
205+
val interceptor = object : WorkflowInterceptor {
206+
override fun <P, S, O> onSessionCancelled(
207+
cause: kotlinx.coroutines.CancellationException?,
208+
droppedActions: List<WorkflowAction<P, S, O>>,
209+
session: WorkflowSession
210+
) {
211+
capturedDroppedActions = droppedActions
212+
}
213+
}
214+
val session = object : WorkflowSession {
215+
override val identifier: WorkflowIdentifier = TestWorkflow.identifier
216+
override val renderKey: String = ""
217+
override val sessionId: Long = 0
218+
override val parent: WorkflowSession? = null
219+
override val runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG
220+
override val workflowTracer: WorkflowTracer? = null
221+
override val runtimeContext: CoroutineContext = EmptyCoroutineContext
222+
}
223+
val testAction = action<String, String, String>("TestAction") { state = "modified" }
224+
225+
interceptor.onSessionCancelled(
226+
cause = null,
227+
droppedActions = listOf(testAction),
228+
session = session
229+
)
230+
231+
assertEquals(1, capturedDroppedActions!!.size)
232+
assertEquals("TestAction", capturedDroppedActions!![0].debuggingName)
233+
}
234+
178235
private val Workflow<*, *, *>.session: WorkflowSession
179236
get() = object : WorkflowSession {
180237
override val identifier: WorkflowIdentifier = this@session.identifier

workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/internal/ChainedWorkflowInterceptorTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ internal class ChainedWorkflowInterceptorTest {
7070
session: WorkflowSession
7171
) {
7272
events += "started1"
73+
// We can't use onSessionCancelled because this is completed when the coroutine from
74+
// launch() below finishes, so onSessionCancelled is never called by the runtime.
7375
workflowScope.coroutineContext[Job]!!.invokeOnCompletion {
7476
events += "cancelled1"
7577
}

0 commit comments

Comments
 (0)