Skip to content

Conversation

jamieQ
Copy link
Contributor

@jamieQ jamieQ commented May 20, 2025

this BREAKING change updates the WorkflowAction API to pass through a new parameter in the apply() method that allows reading values off the corresponding node's current Workflow instance. the new method signature is:

func apply(toState state: inout State, context: ApplyContext<WorkflowType>) -> Output?

which mirrors the render() API, but uses a new ApplyContext parameter to expose access to the runtime-managed data. the public API for ApplyContext exposes a read-only subscript for accessing Workflow values via keypath:

public struct ApplyContext<WorkflowType: Workflow> {
    public subscript<Value>(
        workflowValue keyPath: KeyPath<WorkflowType, Value>
    ) -> Value 
}

for reviewing purposes, i'd suggest using the 'commit filter' function to exclude the commit labeled 'AUTOMATED' as that one is essentially just noise where a bunch of new parameters were added.

also, there's definitely some more documentation to be written here, but i've currently punted on that somewhat. do feel free to point out places you'd like to see anything specific though.

@jamieQ jamieQ force-pushed the jquadri/action-apply-api-change branch 2 times, most recently from 6446a18 to e05bd71 Compare May 21, 2025 16:30
@jamieQ jamieQ force-pushed the jquadri/action-apply-api-change branch from e05bd71 to 2d260c7 Compare May 21, 2025 16:51
@@ -27,14 +27,22 @@ public protocol WorkflowAction<WorkflowType> {
///
/// - Returns: An optional output event for the workflow. If an output event is returned, it will be passed up
/// the workflow hierarchy to this workflow's parent.
func apply(toState state: inout WorkflowType.State) -> WorkflowType.Output?
func apply(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any thoughts around offering both the old and the new functions to avoid changes to lots of files?

I.e., if you added the new one, along with a default impl that called the old one, could we get away with only changing existing action types that need access to the context?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was explored a few times in the past and i believe the conclusion was that it would be better to 'rip the bandaid off' so to speak. IMO the primary problem with trying to be source compatible is that you end up having to expose sort of redundant protocol requirements. yes, you can default the one with the new param to forward to the one without it, but it's still sort of an odd API. i think ideally you'd want them to be mutually exclusive, but i'm not sure that's expressible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that being said – if you've got alternate ideas, happy to hear them!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jamie has already explored ways to automate a large part of this migration, so I am fine with making the breaking change. It's why we have semver!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that makes sense - thanks for sharing the previous exploration!

Comment on lines +30 to +37
func apply(
toState state: inout WorkflowType.State,
context: ApplyContext<WorkflowType>
) -> WorkflowType.Output?
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering if there's some clever way to introduce this in a non-breaking way with protocol extensions (to ease the initial integration/migration) but I don't think there is... Throwing it out there in case someone smarter than me sees a way to make it work.

Copy link
Contributor

@square-tomb square-tomb May 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that out here, and it works, but as @jamieQ mentioned above requires confusingly exposing a second protocol for actions.

@jamieQ jamieQ changed the title [WIP]: update WorkflowAction API to expose access to Workflow instance values [breaking][feat]: update WorkflowAction API to expose access to Workflow instance values May 21, 2025
@jamieQ jamieQ marked this pull request as ready for review May 21, 2025 17:50
@jamieQ jamieQ requested a review from a team as a code owner May 21, 2025 17:50
Copy link
Collaborator

@mjohnson12 mjohnson12 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much better than having to store stuff in State that is not state. 👍

@watt watt self-requested a review May 21, 2025 20:04
*/

/// Internal utility protocol so that `WorkflowTesting` can provide an alternate implementation
protocol ApplyContextType<WorkflowType> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm no longer certain this abstraction is needed. i mostly copied it from the analogous thing done for RenderTester, but unlike in that case i don't think the testing infra relies on 'real' internals to apply actions.

Copy link
Collaborator

@watt watt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've got several questions but no big blockers. Thanks for tackling this!

I also noticed you're adding several TODO comments, where it's not entirely clear if you were intending to address them with this PR or if you're just marking new tech debt.

@@ -44,7 +44,7 @@ public struct StateMutationSink<WorkflowType: Workflow> {
/// - update: The `State` mutation to perform.
public func send(_ update: @escaping (inout WorkflowType.State) -> Void) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thoughts on exposing the ApplyContext to StateMutationSink?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think that probably makes sense to do, but would prefer to tackle as a follow-up for scope-management reasons, unless you feel this will immediately be desired. i think it's a straightforward plumbing change, and i may have had it implemented at one point but removed it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have any need for it immediately, was just curious.

}

public subscript<Value>(
workflowValue keyPath: KeyPath<WorkflowType, Value>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided not to use dynamic property lookup?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, for now anyway. this will make callsites more verbose, but also easier to locate in the source code, and somewhat more explicit about what's going on.

func concreteApplyContextInvalidatedAfterUse() async throws {
var escapedContext: ApplyContext<EscapingContextWorkflow>?
let onApply = { (context: ApplyContext<EscapingContextWorkflow>) in
#expect(context[workflowValue: \.property] == 42)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the first #expect-based tests in Workflow, I believe. Just to confirm, did you check that failures are caught in CI?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the callout. yes, i did a 'canary' run yesterday with a test that should have failed CI, and it seemed to behave as expected

@jamieQ jamieQ changed the title [breaking][feat]: update WorkflowAction API to expose access to Workflow instance values [BREAKING CHANGE]: update WorkflowAction API to expose access to Workflow instance values May 22, 2025
@jamieQ jamieQ force-pushed the jquadri/action-apply-api-change branch from 86c2e57 to 9063748 Compare May 27, 2025 18:16
@jamieQ jamieQ merged commit 876e575 into main May 30, 2025
8 checks passed
@jamieQ jamieQ deleted the jquadri/action-apply-api-change branch May 30, 2025 15:24
@jamieQ jamieQ restored the jquadri/action-apply-api-change branch May 30, 2025 15:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants