-
Notifications
You must be signed in to change notification settings - Fork 47
RF-9688 - [refactor]: Remove ReactiveSwift from Workflow public interface #360
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
bd84d2e
to
13f4761
Compare
WorkflowUIReactiveSwift/Sources/WorkflowHostingController+OutputSignal.swift
Outdated
Show resolved
Hide resolved
Workflow/Sources/WorkflowHost.swift
Outdated
|
||
/// Represents the `Rendering` produced by the root workflow in the hierarchy. New `Rendering` values are produced | ||
/// as state transitions occur within the hierarchy. | ||
public let rendering: Property<WorkflowType.Rendering> | ||
public let rendering: ReadOnlyCurrentValueSubject<WorkflowType.Rendering, Never> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion:
@Published private(set) var rendering: WorkflowType.Rendering`
public var renderingPublisher: AnyPublisher<WorkflowType.Rendering, Never> {
$rendering.eraseToAnyPublisher()
}
private let rendering: CurrentValueSubject<Rendering, Never>
var renderingPublisher: AnyPublisher<Rendering, Never> {
rendering.eraseToAnyPublisher()
}
There's likely a few ways we could go about this without creating a custom subject. It's worth noting that @Published
is part of Combine and use-able anywhere as well. There's a bit of utility that comes with that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AnyPublisher
isn't backward compatible with what we have now for rendering since you can't ask for the current value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops I read private
not private(set)
. rendering
has to be Public to keep clients working.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, the above with @Published
should make it essentially a public read only version of CurrentValueSubject
.
I sort of figured with the AnyPublisher
but figured I'd post it anyways just in case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I've gathered reading in the swift forums setting the value on CurrentValueSubject
is thread safe. I don't about @Published
. I'd have to check on that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pushed up a branch off of this branch to try using @Published
for render. https://github.com/square/workflow-swift/compare/markj/reactive_swift_removal...markj/published_rendering?expand=1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
definitely prefer hiding the implementation details of the 'observable' thing that is exposed publicly. what does the property wrapper buy us exactly (vs, say, just exposing rendering
as a read-only public property, and managing the publicly-observable value separately)? do we want to lean into that as the binding mechanism, or implement the logic manually so we could change the guts if needed.
also, doesn't @Published
have some specific behaviors about when observers are notified vs when the stored value is changed (like if you read the stored value in a sink
callback it won't yet be updated or something)? is that something that matters?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Originally I was trying to maintain the same behavior we had before where there was a rendering
property you could ask the value
of. This led to the issue that CurrentValueSubject
would expose setting the value which we did not want to do. That's why I came up with ReadOnlyCurrentValueSubject
.
With that api getting the rendering value would be identical to now so none of that code would need to change only where you subscribe to changes of the rendering which is actually not very many places.
The @Published
scenario basically had the same behavior as a CurrentValueSubject
but you can easily make the setter private.
I agree @Published
isn't buying us much over a read only property for rendering
and a renderingPublisher
AnyPublisher
property that are implemented internally by something like a CurrentValueSubject
. I'll explore that scenario.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we have consensus that we don't like the current ReadOnlySubject
approach I will push up changes on this branch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the implementation to have a read only rendering and a renderingPublisher property.
import ReactiveSwift | ||
import Workflow | ||
|
||
extension WorkflowOutputPublisher { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: Is this just a duplicated version of
import Combine
import Foundation
import ReactiveSwift
extension Publisher {
/// Create a ReactiveSignal
public func toSignal() -> Signal<Output, Failure> {
Signal.unserialized { observer, lifetime in
let cancellable = self.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
observer.sendCompleted()
case .failure(let error):
observer.send(error: error)
}
},
receiveValue: { value in
observer.send(value: value)
}
)
lifetime.observeEnded {
cancellable.cancel()
}
}
}
}
In our ReactiveSwiftCombineBridging
library?
Do we have a way of collapsing these if so - or is this more of a temporary transitional item needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't reference anything in register in Workflow. I don't think it would be worth adding a new public library just to share that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No it wouldn't, but we also need to be careful about fragmented/duplicated public utilities. If this was internal
and guaranteed to be 100% unique, I might be more ok with it. Both are public and almost 1-1 duplications meaning fragmentation and potential collisions at some point.
Is this just temporary though as ReactiveSwift is being removed from Workflow or is this a more or less long term duplicated utility?
For the consuming app, I do imagine outputPublisher.toSignal()
would work all the same leaving us with outputPublisher.toSignal()
and/or outputPublisher.output
essentially producing the same thing. I would imagine ouptuPublisher.output
could be internal to Workflow and outputPublisher.toSignal()
would be the application utility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have a lot of usage of the current output
signal in the codebase so this keeps all those working by just adding an import statement.
I guess it's as temporary as we have consumers wanting a ReactiveSwift Signal. After we have the combine publisher in the consumers can definitely switch to using that and we could remove this extension.
The overall goal of this change is remove ReactiveSwift without breaking or having to change all the workflows in register.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool - totally get it given it's a migration path strategy.
f7e5fc3
to
59bcc8b
Compare
public protocol WorkflowOutputPublisher { | ||
associatedtype Output | ||
|
||
var outputPublisher: AnyPublisher<Output, Never> { get } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this protocol necessary? what's the benefit over just extending the concrete type directly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It allows us to add the output signal on both WorkflowHost
and WorkflowHostingController
by just extending the protocol in WorkflowReactiveSwift. This was a suggestion in Andrew's feedback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah gotcha (sorry i missed the existing convo). so the tradeoff here is adding a 'public' protocol to which we really only want 2 specific things to conform so that we don't need to make Workflow
or WorkflowUI
depend on ReactiveSwift
– is that right? mostly out of curiosity – is it possible to define the protocol with package
visibility? that seems like it might be a slightly more accurate definition (assuming it works and integrates successfully into the monorepo).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Before I had extensions on WorkflowHost
and WorkflowHostingController
which required a new module just to put the extension on WorkflowHostingController
since I could not put it in WorkflowReactiveSwift
since it does not know about WorkflowUI
. Andrew's idea was use a protocol and drop the extension in WorkflowReactiveSwift
so we don't need to have an additional module and the reactive swift stuff stays in WorkflowReactiveSwift
.
I can look into the package
visibility to see if that is possible.
96bbd04
to
7ce7d31
Compare
} | ||
|
||
/// A Publisher containing rendering events produced by the root workflow in the hierarchy. | ||
public var renderingPublisher: AnyPublisher<WorkflowType.Rendering, Never> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any thoughts on using some Publisher<Rendering, Never>
vs AnyPublisher
here (and in general where we return Publishers)? in theory the former lets us not have to do the erasure (though then client code could in theory cast it to recover the concrete type in some cases... unless we still erased it before returning). if we don't return AnyPublisher
then i guess any client code that wishes to store the value as a concrete type would have the burden of doing that...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer AnyPublisher
as it hides the type better and it's the standard Combine way of returning a type erased publisher. I know any Publisher
doesn't work as you can't call most of operators on it. some Publisher
does not seem to have that issue though.
// @testable | ||
let rootNode: WorkflowNode<WorkflowType> | ||
|
||
private let mutableRendering: MutableProperty<WorkflowType.Rendering> | ||
private let renderingSubject: CurrentValueSubject<WorkflowType.Rendering, Never> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
slightly inclined to split the storage for the property vs the observer, since that will give us the most control over when the 'outside world' sees the update. e.g.
var rendering: Rendering
let renderingSubject = PassThroughSubject<Rendering, Never>()
any thoughts on that? i guess one thing that would be different is that the old way (and presumably using a CVS) would have some baked-in synchronization mechanism over the underlying value. in theory this stuff should be main-thread-only though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We currently are using a ReactiveSwift MutableProperty which does have an internal lock. Yes CurrentValueSubject does have some baked in serialization mechanism albeit without Apple documentation on what that is. Since we can't enforce someone reading the rendering value on the main thread I'm inclined to use the CurrentValueSubject since it's about as close as we can get as a Combine version of what we have now.
If we split the storage for the property out we could always lock around getting/setting it so I'm not against doing that but I think CurrentValueSubject should work for how we are using it.
public var output: Signal<Output, Never> { | ||
Signal.unserialized { observer, lifetime in | ||
let cancellable = outputPublisher.sink( | ||
receiveCompletion: { completion in | ||
switch completion { | ||
case .finished: | ||
observer.sendCompleted() | ||
|
||
case .failure(let error): | ||
observer.send(error: error) | ||
} | ||
}, | ||
receiveValue: { value in | ||
observer.send(value: value) | ||
} | ||
) | ||
lifetime.observeEnded { | ||
cancellable.cancel() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's plan on testing this fairly thoroughly. i'm never certain what the exact lifetime semantics are for the ReactiveSwift stuff personally...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. This has been in use in the ReactiveSwift bridging module for a while.
public protocol WorkflowOutputPublisher { | ||
associatedtype Output | ||
|
||
var outputPublisher: AnyPublisher<Output, Never> { get } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah gotcha (sorry i missed the existing convo). so the tradeoff here is adding a 'public' protocol to which we really only want 2 specific things to conform so that we don't need to make Workflow
or WorkflowUI
depend on ReactiveSwift
– is that right? mostly out of curiosity – is it possible to define the protocol with package
visibility? that seems like it might be a slightly more accurate definition (assuming it works and integrates successfully into the monorepo).
let's also include in the commit message something that clearly indicates this is a breaking change to the public API, a la the guidelines specified via 'conventional commits' |
BREAKING CHANGE: This changes the public interface of WorkflowHost and WorkflowHostingController. The rendering property is now a read only property of the Rendering There is a new renderingPublisher property for a Combine publisher for renderings The output Signal property has been removed and moved to an extension in WorkflowReactiveSwift There is a new outputPublisher property for a Combine publisher for output
7ce7d31
to
99238f0
Compare
I've updated the commit message |
This is a breaking change to Workflow that removes ReactiveSwift from the public interface of Workflow and Workflow UI.
There are 3 changes:
WorkflowHost
rendering
is now a read only property of the latest rendering.WorkflowHost
renderingPublisher
is a Combine publisher that publishes renderings. In searching the register code base there were very few places we were using aSignal
/SignalProducer
from therendering
property. The plan would be to change those consumers to userenderingPublisher
andsink
.WorkflowHost
output
has been renamed tooutputPublisher
. It is now a CombinePublisher
that can be used to subscribe to Workflow output. Per Andrew's suggestion I added a new protocolWorkflowOutputPubisher
that exposes theoutputPublisher
. Theoutput
property is used in a lot of places in register. To fix those places I added an extension onWorkflowOutputPubisher
in WorkflowReactiveSwift that re-exposesoutput
as aSignal
. All the places that useoutput
will just need to import WorkflowReactiveSwift and they will continue to work.WorkflowHostingController
in WorkflowUI now implementsWorkflowOutputPublisher
. Consumers usingoutput
will just need to import WorkflowReactiveSwift to continue to work.Note: Even though this removes ReactiveSwift as a dependency from the Workflow and WorkflowUI targets the Workflow Package has to continue to have ReactiveSwift as a dependency since it's used by WorkflowReactiveSwift. But because register uses bazel if you import Workflow/WorkflowUI in your module it does not import (directly or transitively) ReactiveSwift.
Checklist