Cancelling Effect subscriptions

Hey, I wasn't able to find any better title for this topic, so I went with the simples one. I've noticed that one of my top-level reducers receives multiplied amount of the same action according to the amount of times sheet has been presented on the screen... I know it sounds ridiculous, so let me describe it thoroughly.

I have a top-level set of components (devicesReducer, DevicesAction etc.). They're responsible for handling communication with BLE sensors, getting data from them and everything related to this connectivity. In my top View I have a Button which shows sheet in which I start scanning for the nearby devices and also I can select there device (If found) to be stored for the future. So every time this sheet is being presented I trigger an action called startScanning which returns following Effect:

return environment.manager
                .startScanning(deviceType: .FeverDevice)
                .receive(on: environment.mainQueue)
                .catchToEffect()
                .map(DevicesAction.discoveredDevice)

And this is the .discoverDevice:

case let .discoveredDevice(.success(discoveredDevice)):
            print(discoveredDevice)
            return .none

Everything abovementioned works fine, so sheet is being presented and after a while, I can see something like being printed in the console:

<DeviceToolUseClass: 0x282fc60c0>

I close the sheet, stop scanning and trigger the same scenario once again - tap button, sheet is being presented and starts scanning for the nearby devices, but this time as a result I can see:

<DeviceToolUseClass: 0x28284d700>
<DeviceToolUseClass: 0x28284d700>

I guess the important part is that results are duplicated. Not two different object, but exactly the same thing multiplicated by the amount of times sheet's been on the screen.

This situation repeats and each time I can see more devices being discovered and number always reflect the count of times sheet has been presented. I've checked my Manager class which is being passed through Environemnt and it triggers send new device only once (It's a PassthroughSubject), so at this stage everything's fine. Duplication begins when I reach TCA layer. I've put print() on my Publisher and it shows me like a new subscription is being created each time I open sheet. Can I somehow cancel old subscriptions?

I'm losing my sanity right now because everything I'm sure this implementation should work just fine.

1 Like

I've only skimmed through your post, but are you aware of cancellable on Effect?

2 Likes

I was not and most likely this is what I'm looking for. Thanks for pointing me to this @klop!


Edit:

Yes, indeed, after adding .cancellable(id: DeviceScannId(), cancelInFlight: true) to my Effect everything works as expected.

2 Likes

Yeah, in general, long-living effects need to be torn down eventually, either using the .cancellable mechanism or having an explicit .destroy (or something like that) endpoint on your dependency for tearing things down.

It's worth noting that cancelInFlight will tear down the previous effect before starting a new one, but if there's another action that should just stop the effect (and not start a new one), then you need to also send an explicit .cancel(id:) effect to do that.

Also, if you are using Effect.run in your dependency to represent this long-living effect, then be sure to clean up resources in the Cancellable returned.

Hope that makes sense!

3 Likes

@mbrandonw yeah, first part makes sense, but anything about run... I think I’m not there, yet :wink: Still, I have what I needed to solve my blocker and that’s the most important part right now.

I still have some troubles understanding Effects and how to make them play nice when being derived from Publishers - especially when it comes to error handling.