-
Notifications
You must be signed in to change notification settings - Fork 432
Enable Action executing state feedback to the state property.
#400
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
Conversation
d110566 to
abc9160
Compare
Sources/Action.swift
Outdated
|
|
||
| state.availability = .enabledExecuting | ||
| isExecuting.value = true | ||
| isEnabled.value = false |
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.
Why is isEnabled false if availability is enabledExecuting?
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.
Action s are disabled when executing.
Sources/Action.swift
Outdated
| let isExecuting = MutableProperty(false) | ||
| let isEnabled = MutableProperty(true) | ||
| self.isExecuting = Property(capturing: isExecuting) | ||
| self.isEnabled = Property(capturing: isEnabled) |
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.
Can these be derived from actionState instead of duplicating that information in separate properties?
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.
The signal of ActionState is deliberately ignored, since Signal doesn't support recursion with value events.
actionState deadlocks when one does isEnabled <~ isExecuting and both are derived from it, i.e. the current implementation.
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.
In 4acaf81, the read-only isEnabled is once again derived since we are permitting only state <~ isExecuting here, where state is the state of the Action that flips its availability (isEnabled).
Sources/Action.swift
Outdated
| state.availability = .disabledExecuting | ||
|
|
||
| default: | ||
| break |
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.
This default shouldn't be 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.
There are eight combinations in total, and only four need to be acted on.
Sources/Action.swift
Outdated
| executeClosure = { state, input in execute(state as! State.Value, input) } | ||
| // `isExecuting` and `isEnabled` should use their own locks, so that legitimate | ||
| // feedbacks would not deadlock. | ||
| let initial = (availability: Availability.enabledIdle, value: state.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.
Can you make this a struct instead of a typealias?
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.
Done in 4acaf81.
Sources/Action.swift
Outdated
| case enabledIdle | ||
| case enabledExecuting | ||
| case disabledIdle | ||
| case disabledExecuting |
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.
This is really modeling two independent boolean states. Can we separate these and add them to ActionState individually? I think that will simplify the code a bit as well.
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.
The problem is that isEnabled and isExecuting are not independent. They together form a finite state machine, and IMO named cases are easier to understand than a switch with tuples of booleans.
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.
Changed in 4acaf81 anyway.
Sources/Action.swift
Outdated
| /// In other words, this sends every value from every unit of work that the `Action` | ||
| /// executes. | ||
| public let values: Signal<Output, NoError> | ||
| public private(set) lazy var values: Signal<Output, NoError> = { [unowned self] in |
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.
Can we make this lazy change in a separate PR? I'm not sold on it, and it's not essential to this PR.
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.
Reverted in 4acaf81.
abbf01d to
4b6792e
Compare
4b6792e to
4acaf81
Compare
| self.isEnabled = actionState.map { $0.isEnabled } | ||
|
|
||
| let isExecuting = MutableProperty(false) | ||
| self.isExecuting = Property(capturing: isExecuting) |
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.
Can this be self.isExecuting = actionState.map { $0.isExecuting }?
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.
The point of giving it its own backing is that isExecuting calls out to the observers outside the signal of actionState, so that when actionState is modified reactively & synchronously it doesn't deadlock.
However, it does seem violating exclusivity of access so it might need a change anyway. We might need to add guards in MutableProperty to prevent nested modifications too, or rely on the dynamic endorcement in Swift 4.
| self.isExecuting = Property(capturing: isExecuting) | ||
|
|
||
| // Associate the state property with the created `Action`. | ||
| lifetime.observeEnded { _ = state } |
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.
This comment is describing what this line does. Can you update it to describe why it needs to be associated?
Sources/Action.swift
Outdated
| state = MutableProperty(initial) | ||
| self.execute = { action, input in | ||
| return SignalProducer { observer, lifetime in | ||
| func tryStart() -> State.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.
This is only called once. Maybe take the code out of the function?
let state = actionState.modify { state in
...
}
guard let state = state else {
...It should be done after the modifier of `ActionState` returns and the `inout` reference is semantically written back, in order not to violate the exclusivity of access.
|
|
||
| self.isEnabled = state.map { $0.isEnabled }.skipRepeats() | ||
| self.isExecuting = state.map { $0.isExecuting }.skipRepeats() | ||
| let latestState: State.Value? = actionState.modify(didSet: didSet) { state in |
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.
This didSet seem pointless. The function is only called here, right after notifiesExecutionState is set to false—so it will always do nothing. It seem like both didSet() and notifiesExecutionState can be removed?
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.
notifiesExecutionState is set to true when the current execution attempt succeeds and starts. didSet is invoked after the trailing closure modifying actionState.
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.
🤦🏼♂️
|
|
||
| self.isEnabled = state.map { $0.isEnabled }.skipRepeats() | ||
| self.isExecuting = state.map { $0.isExecuting }.skipRepeats() | ||
| let latestState: State.Value? = actionState.modify(didSet: didSet) { state in |
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.
🤦🏼♂️
Related: #309.
There are legitimate uses of
isExecutingfeedback to theActionstate property. For example, we may link the availability of multiple actions together, so that all disables when one executes.This PR refactors the internal of
Actionso that such use cases are permitted. Legitimate infinite feedback loops would still result in deadlocks.Example:
Alternative/Follow-up
Could we have some kind of "action group"s that determine the availability at a coarser granularity?