I'd like to preface this issue by saying that I have not yet finished watching the episodes on TCA, and in fact I'm going to go back and watch them from the start very soon, so forgive me if I'm going wildly off course with this one.
Since I'm yet to come up with a well thought out description of the issue, and in fact may be prodding at something much different in scope to what my title suggests, I'll provide a concrete example: Cancellation.
There are a couple of ways through which effects can be cancelled in TCA:
-
AnyCancellable
deinitizalization: stores subscribe to effects, effects return AnyCancellable
when subscribed to. Those cancellables are held onto by the store, and if the store deinitializes so do the cancellables. AnyCancellable
performs clean up on deinit.
-
Creating "cancellable" effects: effects can be made "cancellable" by calling the cancellable(id: AnyHashable, cancelInFlight: Bool)
function on them. The id
passed to cancellable
can later be used to cancel the effect by calling the static cancel(id: AnyHashable)
function, or by calling cancellable(id: AnyHashable, cancelInFlight: Bool)
again with the same id
and cancelInFlight == true
, and then subscribing to that effect.
The second approach to effect cancellation, which is what I'd like to focus on, involves some additional state management behind the scenes which can be found in Cancellation.swift.
What stood out to me here is that we're replicating functionality that's (at least to some extent) achieved through various publisher flattening strategies. Let's suppose we have a button that when tapped, causes an API request to be made, the result of which is transformed into an action to update some state. There's also a caveat, which is that there should not be more than one API request happening at any one time.
This is simple enough to achieve in TCA: each button tap results in a new cancellable effect (created with cancelInFlight == true
) being returned from a reducer, the result being that if a button is tapped while an API request is yet to complete, it will be cancelled and a new request will take its place. We can show this using a (bad) diagram where o
denotes a value and |
denotes completion:
Button taps: ---o------------o----o-o---o---------->
API request #1: ------o|
API request #2: ----
API request #3: --
API request #4: ----
API request #5: ------o|
Request 1 gets fired and completes, 2-4 all get fired but don't complete due to cancellation caused by button taps prior to completion, and 5 gets fired and completes.
The same sequence of API calls and the resulting output could instead be achieved using switchToLatest
(flatMapLatest
in RxSwift) n.b. pseudocode that has no obvious place in TCA:
didTapButton.switchToLatest {
client.callSomeAPI()
}
Furthermore, let's suppose we want to slightly tweak the behaviour such that rather cancelling in flight API requests, we wait for them to complete before allowing new ones to be made. In the above example, we'd simply replace switchToLatest
with flatMapFirst
(that's RxSwift, it seems Combine doesn't have an equivalent yet). What about situations where we want to race inner publishers, merge them while limiting max concurrency, or throttle/debounce?
It isn't clear to me where these use cases fit into TCA.