Skip to content

Commit 001d0e9

Browse files
Cancel effects when root store deallocates (#3613)
* Cancel effects when root store deinits. * Add test for uncached stores. * wip * wip * wip * wip --------- Co-authored-by: Stephen Celis <[email protected]>
1 parent 8ffc4df commit 001d0e9

File tree

4 files changed

+65
-30
lines changed

4 files changed

+65
-30
lines changed

ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved

+17-17
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
"kind" : "remoteSourceControl",
1616
"location" : "https://github.com/pointfreeco/swift-case-paths",
1717
"state" : {
18-
"revision" : "bc92c4b27f9a84bfb498cdbfdf35d5a357e9161f",
19-
"version" : "1.5.6"
18+
"revision" : "19b7263bacb9751f151ec0c93ec816fe1ef67c7b",
19+
"version" : "1.6.1"
2020
}
2121
},
2222
{
@@ -60,8 +60,8 @@
6060
"kind" : "remoteSourceControl",
6161
"location" : "https://github.com/pointfreeco/swift-dependencies",
6262
"state" : {
63-
"revision" : "85f89f5d0ce5a18945f65371d40ca997da85a41a",
64-
"version" : "1.6.3"
63+
"revision" : "121a428c505c01c4ce02d5ada1c8fc3da93afce9",
64+
"version" : "1.8.0"
6565
}
6666
},
6767
{
@@ -87,17 +87,17 @@
8787
"kind" : "remoteSourceControl",
8888
"location" : "https://github.com/pointfreeco/swift-identified-collections",
8989
"state" : {
90-
"revision" : "2f5ab6e091dd032b63dacbda052405756010dc3b",
91-
"version" : "1.1.0"
90+
"revision" : "322d9ffeeba85c9f7c4984b39422ec7cc3c56597",
91+
"version" : "1.1.1"
9292
}
9393
},
9494
{
9595
"identity" : "swift-macro-testing",
9696
"kind" : "remoteSourceControl",
9797
"location" : "https://github.com/pointfreeco/swift-macro-testing",
9898
"state" : {
99-
"revision" : "20c1a8f3b624fb5d1503eadcaa84743050c350f4",
100-
"version" : "0.5.2"
99+
"revision" : "0b80a098d4805a21c412b65f01ffde7b01aab2fa",
100+
"version" : "0.6.0"
101101
}
102102
},
103103
{
@@ -114,26 +114,26 @@
114114
"kind" : "remoteSourceControl",
115115
"location" : "https://github.com/pointfreeco/swift-perception",
116116
"state" : {
117-
"revision" : "8d52279b9809ef27eabe7d5420f03734528f19da",
118-
"version" : "1.4.1"
117+
"revision" : "21811d6230a625fa0f2e6ffa85be857075cc02c4",
118+
"version" : "1.5.0"
119119
}
120120
},
121121
{
122122
"identity" : "swift-sharing",
123123
"kind" : "remoteSourceControl",
124124
"location" : "https://github.com/pointfreeco/swift-sharing",
125125
"state" : {
126-
"revision" : "c5ea46f0712cd3b639e2c7d6bf3f193116e0ff8d",
127-
"version" : "2.0.2"
126+
"revision" : "2c840cf2ae0526ad6090e7796c4e13d9a2339f4a",
127+
"version" : "2.3.3"
128128
}
129129
},
130130
{
131131
"identity" : "swift-snapshot-testing",
132132
"kind" : "remoteSourceControl",
133-
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
133+
"location" : "https://github.com/pointfreeco/swift-snapshot-testing.git",
134134
"state" : {
135-
"revision" : "42a086182681cf661f5c47c9b7dc3931de18c6d7",
136-
"version" : "1.17.6"
135+
"revision" : "b2d4cb30735f4fbc3a01963a9c658336dd21e9ba",
136+
"version" : "1.18.1"
137137
}
138138
},
139139
{
@@ -159,8 +159,8 @@
159159
"kind" : "remoteSourceControl",
160160
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
161161
"state" : {
162-
"revision" : "a3f634d1a409c7979cabc0a71b3f26ffa9fc8af1",
163-
"version" : "1.4.3"
162+
"revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4",
163+
"version" : "1.5.2"
164164
}
165165
}
166166
],

Package.resolved

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"originHash" : "6727aa1791df9992e75965cc70f604fddc462c64604478ad596f7f96230963a6",
2+
"originHash" : "4cc63b23e996f494117d890d6db8517dfc65a86023208ff4ad39d9c2b09ee033",
33
"pins" : [
44
{
55
"identity" : "combine-schedulers",
@@ -105,8 +105,8 @@
105105
"kind" : "remoteSourceControl",
106106
"location" : "https://github.com/pointfreeco/swift-navigation",
107107
"state" : {
108-
"revision" : "e28911721538fa0c2439e92320bad13e3200866f",
109-
"version" : "2.2.3"
108+
"revision" : "db6bc9dbfed001f21e6728fd36413d9342c235b4",
109+
"version" : "2.3.0"
110110
}
111111
},
112112
{

Sources/ComposableArchitecture/RootStore.swift

+10-4
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,14 @@ public final class RootStore {
5252
defer { index += 1 }
5353
let action = self.bufferedActions[index] as! Action
5454
let effect = reducer.reduce(into: &currentState, action: action)
55+
let uuid = UUID()
5556

5657
switch effect.operation {
5758
case .none:
5859
break
5960
case let .publisher(publisher):
6061
var didComplete = false
6162
let boxedTask = Box<Task<Void, Never>?>(wrappedValue: nil)
62-
let uuid = UUID()
6363
let effectCancellable = withEscapedDependencies { continuation in
6464
publisher
6565
.receive(on: UIScheduler.shared)
@@ -88,11 +88,13 @@ public final class RootStore {
8888
}
8989
boxedTask.wrappedValue = task
9090
tasks.withValue { $0.append(task) }
91-
self.effectCancellables[uuid] = effectCancellable
91+
self.effectCancellables[uuid] = AnyCancellable {
92+
task.cancel()
93+
}
9294
}
9395
case let .run(priority, operation):
9496
withEscapedDependencies { continuation in
95-
let task = Task(priority: priority) { @MainActor in
97+
let task = Task(priority: priority) { @MainActor [weak self] in
9698
let isCompleted = LockIsolated(false)
9799
defer { isCompleted.setValue(true) }
98100
await operation(
@@ -118,14 +120,18 @@ public final class RootStore {
118120
)
119121
}
120122
if let task = continuation.yield({
121-
self.send(effectAction, originatingFrom: action)
123+
self?.send(effectAction, originatingFrom: action)
122124
}) {
123125
tasks.withValue { $0.append(task) }
124126
}
125127
}
126128
)
129+
self?.effectCancellables[uuid] = nil
127130
}
128131
tasks.withValue { $0.append(task) }
132+
self.effectCancellables[uuid] = AnyCancellable {
133+
task.cancel()
134+
}
129135
}
130136
}
131137
}

Tests/ComposableArchitectureTests/StoreLifetimeTests.swift

+35-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Combine
22
@_spi(Logging) import ComposableArchitecture
33
import XCTest
44

5+
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
56
final class StoreLifetimeTests: BaseTCATestCase {
67
@available(*, deprecated)
78
@MainActor
@@ -69,9 +70,6 @@ final class StoreLifetimeTests: BaseTCATestCase {
6970

7071
@MainActor
7172
func testStoreDeinit_RunningEffect() async {
72-
XCTTODO(
73-
"We would like for this to pass, but it requires full deprecation of uncached child stores"
74-
)
7573
Logger.shared.isEnabled = true
7674
let effectFinished = self.expectation(description: "Effect finished")
7775
do {
@@ -99,9 +97,6 @@ final class StoreLifetimeTests: BaseTCATestCase {
9997

10098
@MainActor
10199
func testStoreDeinit_RunningCombineEffect() async {
102-
XCTTODO(
103-
"We would like for this to pass, but it requires full deprecation of uncached child stores"
104-
)
105100
Logger.shared.isEnabled = true
106101
let effectFinished = self.expectation(description: "Effect finished")
107102
do {
@@ -129,28 +124,61 @@ final class StoreLifetimeTests: BaseTCATestCase {
129124
await self.fulfillment(of: [effectFinished], timeout: 0.5)
130125
}
131126
#endif
127+
128+
@MainActor
129+
@available(*, deprecated)
130+
func testUnCachedStores() async {
131+
Logger.shared.isEnabled = true
132+
let clock = TestClock()
133+
let store = Store(initialState: Parent.State()) {
134+
Parent()
135+
} withDependencies: {
136+
$0.continuousClock = clock
137+
}
138+
do {
139+
let child = store.scope(state: { $0.child }, action: { .child($0) })
140+
child.send(.start)
141+
XCTAssertEqual(store.withState(\.child.count), 1)
142+
}
143+
await clock.run()
144+
XCTAssertEqual(store.withState(\.child.count), 2)
145+
}
132146
}
133147

134148
@Reducer
149+
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
135150
private struct Child {
136151
struct State: Equatable {
137152
var count = 0
138153
}
139154
enum Action {
140155
case tap
156+
case start
157+
case response
141158
}
159+
@Dependency(\.continuousClock) var clock
142160
var body: some ReducerOf<Self> {
143161
Reduce { state, action in
144162
switch action {
145163
case .tap:
146164
state.count += 1
147165
return .none
166+
case .start:
167+
state.count += 1
168+
return .run { send in
169+
try await clock.sleep(for: .seconds(0))
170+
await send(.response)
171+
}
172+
case .response:
173+
state.count += 1
174+
return .none
148175
}
149176
}
150177
}
151178
}
152179

153180
@Reducer
181+
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
154182
private struct Parent {
155183
struct State: Equatable {
156184
var child = Child.State()
@@ -166,6 +194,7 @@ private struct Parent {
166194
}
167195

168196
@Reducer
197+
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
169198
private struct Grandparent {
170199
struct State: Equatable {
171200
var child = Parent.State()

0 commit comments

Comments
 (0)