From f1b426d569860d09df25ae86646a8ceb97a35607 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Wed, 26 Feb 2025 16:02:29 -0800 Subject: [PATCH 1/6] Cancel effects when root store deinits. --- Package.resolved | 6 +++--- Sources/ComposableArchitecture/RootStore.swift | 14 ++++++++++---- .../StoreLifetimeTests.swift | 12 ++++++------ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/Package.resolved b/Package.resolved index 9b3471c17b71..5e9e94ed2eca 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "6727aa1791df9992e75965cc70f604fddc462c64604478ad596f7f96230963a6", + "originHash" : "4cc63b23e996f494117d890d6db8517dfc65a86023208ff4ad39d9c2b09ee033", "pins" : [ { "identity" : "combine-schedulers", @@ -105,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-navigation", "state" : { - "revision" : "e28911721538fa0c2439e92320bad13e3200866f", - "version" : "2.2.3" + "revision" : "db6bc9dbfed001f21e6728fd36413d9342c235b4", + "version" : "2.3.0" } }, { diff --git a/Sources/ComposableArchitecture/RootStore.swift b/Sources/ComposableArchitecture/RootStore.swift index 650ed7e812e7..378deabfa9a9 100644 --- a/Sources/ComposableArchitecture/RootStore.swift +++ b/Sources/ComposableArchitecture/RootStore.swift @@ -52,6 +52,7 @@ public final class RootStore { defer { index += 1 } let action = self.bufferedActions[index] as! Action let effect = reducer.reduce(into: ¤tState, action: action) + let uuid = UUID() switch effect.operation { case .none: @@ -59,7 +60,6 @@ public final class RootStore { case let .publisher(publisher): var didComplete = false let boxedTask = Box?>(wrappedValue: nil) - let uuid = UUID() let effectCancellable = withEscapedDependencies { continuation in publisher .receive(on: UIScheduler.shared) @@ -88,11 +88,13 @@ public final class RootStore { } boxedTask.wrappedValue = task tasks.withValue { $0.append(task) } - self.effectCancellables[uuid] = effectCancellable + self.effectCancellables[uuid] = AnyCancellable { + task.cancel() + } } case let .run(priority, operation): withEscapedDependencies { continuation in - let task = Task(priority: priority) { @MainActor in + let task = Task(priority: priority) { @MainActor [weak self] in let isCompleted = LockIsolated(false) defer { isCompleted.setValue(true) } await operation( @@ -118,14 +120,18 @@ public final class RootStore { ) } if let task = continuation.yield({ - self.send(effectAction, originatingFrom: action) + self?.send(effectAction, originatingFrom: action) }) { tasks.withValue { $0.append(task) } } } ) + self?.effectCancellables[uuid] = nil } tasks.withValue { $0.append(task) } + self.effectCancellables[uuid] = AnyCancellable { + task.cancel() + } } } } diff --git a/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift b/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift index 065f94a74dcb..474f646e0753 100644 --- a/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift +++ b/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift @@ -69,9 +69,9 @@ final class StoreLifetimeTests: BaseTCATestCase { @MainActor func testStoreDeinit_RunningEffect() async { - XCTTODO( - "We would like for this to pass, but it requires full deprecation of uncached child stores" - ) +// XCTTODO( +// "We would like for this to pass, but it requires full deprecation of uncached child stores" +// ) Logger.shared.isEnabled = true let effectFinished = self.expectation(description: "Effect finished") do { @@ -99,9 +99,9 @@ final class StoreLifetimeTests: BaseTCATestCase { @MainActor func testStoreDeinit_RunningCombineEffect() async { - XCTTODO( - "We would like for this to pass, but it requires full deprecation of uncached child stores" - ) +// XCTTODO( +// "We would like for this to pass, but it requires full deprecation of uncached child stores" +// ) Logger.shared.isEnabled = true let effectFinished = self.expectation(description: "Effect finished") do { From 7557b87aa7a94ecaf2e9e149eb18d76a1694fdb9 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 27 Feb 2025 08:56:43 -0800 Subject: [PATCH 2/6] Add test for uncached stores. --- .../StoreLifetimeTests.swift | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift b/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift index 474f646e0753..318082667630 100644 --- a/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift +++ b/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift @@ -129,6 +129,25 @@ final class StoreLifetimeTests: BaseTCATestCase { await self.fulfillment(of: [effectFinished], timeout: 0.5) } #endif + + @MainActor + @available(*, deprecated) + func testUnCachedStores() async { + Logger.shared.isEnabled = true + let clock = TestClock() + let store = Store(initialState: Parent.State()) { + Parent() + } withDependencies: { + $0.continuousClock = clock + } + do { + let child = store.scope(state: { $0.child }, action: { .child($0) }) + child.send(.start) + XCTAssertEqual(store.withState(\.child.count), 1) + } + await clock.run() + XCTAssertEqual(store.withState(\.child.count), 2) + } } @Reducer @@ -138,13 +157,25 @@ private struct Child { } enum Action { case tap + case start + case response } + @Dependency(\.continuousClock) var clock var body: some ReducerOf { Reduce { state, action in switch action { case .tap: state.count += 1 return .none + case .start: + state.count += 1 + return .run { send in + try await clock.sleep(for: .seconds(0)) + await send(.response) + } + case .response: + state.count += 1 + return .none } } } From f75f311c9d13ccc3e2c7ede856fa93866257741f Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 27 Feb 2025 09:53:18 -0800 Subject: [PATCH 3/6] wip --- Tests/ComposableArchitectureTests/StoreLifetimeTests.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift b/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift index 318082667630..c630a795b6e0 100644 --- a/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift +++ b/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift @@ -69,9 +69,6 @@ final class StoreLifetimeTests: BaseTCATestCase { @MainActor func testStoreDeinit_RunningEffect() async { -// XCTTODO( -// "We would like for this to pass, but it requires full deprecation of uncached child stores" -// ) Logger.shared.isEnabled = true let effectFinished = self.expectation(description: "Effect finished") do { @@ -99,9 +96,6 @@ final class StoreLifetimeTests: BaseTCATestCase { @MainActor func testStoreDeinit_RunningCombineEffect() async { -// XCTTODO( -// "We would like for this to pass, but it requires full deprecation of uncached child stores" -// ) Logger.shared.isEnabled = true let effectFinished = self.expectation(description: "Effect finished") do { From f37ef4eff10f07c049a90f922809317818c8eb58 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 27 Feb 2025 09:58:19 -0800 Subject: [PATCH 4/6] wip --- Tests/ComposableArchitectureTests/StoreLifetimeTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift b/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift index c630a795b6e0..4bc1ebdfef0c 100644 --- a/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift +++ b/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift @@ -126,6 +126,7 @@ final class StoreLifetimeTests: BaseTCATestCase { @MainActor @available(*, deprecated) + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) func testUnCachedStores() async { Logger.shared.isEnabled = true let clock = TestClock() From a6da7f5711317e377322e6d058d49255a1e95f21 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 27 Feb 2025 12:22:58 -0800 Subject: [PATCH 5/6] wip --- .../xcshareddata/swiftpm/Package.resolved | 34 +++++++++---------- .../StoreLifetimeTests.swift | 3 +- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved index 89ba4b1bca4b..776ea708829d 100644 --- a/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "bc92c4b27f9a84bfb498cdbfdf35d5a357e9161f", - "version" : "1.5.6" + "revision" : "19b7263bacb9751f151ec0c93ec816fe1ef67c7b", + "version" : "1.6.1" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "85f89f5d0ce5a18945f65371d40ca997da85a41a", - "version" : "1.6.3" + "revision" : "121a428c505c01c4ce02d5ada1c8fc3da93afce9", + "version" : "1.8.0" } }, { @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-identified-collections", "state" : { - "revision" : "2f5ab6e091dd032b63dacbda052405756010dc3b", - "version" : "1.1.0" + "revision" : "322d9ffeeba85c9f7c4984b39422ec7cc3c56597", + "version" : "1.1.1" } }, { @@ -96,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-macro-testing", "state" : { - "revision" : "20c1a8f3b624fb5d1503eadcaa84743050c350f4", - "version" : "0.5.2" + "revision" : "0b80a098d4805a21c412b65f01ffde7b01aab2fa", + "version" : "0.6.0" } }, { @@ -114,8 +114,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-perception", "state" : { - "revision" : "8d52279b9809ef27eabe7d5420f03734528f19da", - "version" : "1.4.1" + "revision" : "21811d6230a625fa0f2e6ffa85be857075cc02c4", + "version" : "1.5.0" } }, { @@ -123,17 +123,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-sharing", "state" : { - "revision" : "c5ea46f0712cd3b639e2c7d6bf3f193116e0ff8d", - "version" : "2.0.2" + "revision" : "2c840cf2ae0526ad6090e7796c4e13d9a2339f4a", + "version" : "2.3.3" } }, { "identity" : "swift-snapshot-testing", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-snapshot-testing", + "location" : "https://github.com/pointfreeco/swift-snapshot-testing.git", "state" : { - "revision" : "42a086182681cf661f5c47c9b7dc3931de18c6d7", - "version" : "1.17.6" + "revision" : "b2d4cb30735f4fbc3a01963a9c658336dd21e9ba", + "version" : "1.18.1" } }, { @@ -159,8 +159,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "a3f634d1a409c7979cabc0a71b3f26ffa9fc8af1", - "version" : "1.4.3" + "revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4", + "version" : "1.5.2" } } ], diff --git a/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift b/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift index 4bc1ebdfef0c..e53ceec82a7d 100644 --- a/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift +++ b/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift @@ -2,6 +2,7 @@ import Combine @_spi(Logging) import ComposableArchitecture import XCTest +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) final class StoreLifetimeTests: BaseTCATestCase { @available(*, deprecated) @MainActor @@ -126,7 +127,6 @@ final class StoreLifetimeTests: BaseTCATestCase { @MainActor @available(*, deprecated) - @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) func testUnCachedStores() async { Logger.shared.isEnabled = true let clock = TestClock() @@ -146,6 +146,7 @@ final class StoreLifetimeTests: BaseTCATestCase { } @Reducer +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) private struct Child { struct State: Equatable { var count = 0 From 3996a0860d15da7e7a93e38e2392cd80729f8220 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 27 Feb 2025 13:04:34 -0800 Subject: [PATCH 6/6] wip --- Tests/ComposableArchitectureTests/StoreLifetimeTests.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift b/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift index e53ceec82a7d..be246622091c 100644 --- a/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift +++ b/Tests/ComposableArchitectureTests/StoreLifetimeTests.swift @@ -178,6 +178,7 @@ private struct Child { } @Reducer +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) private struct Parent { struct State: Equatable { var child = Child.State() @@ -193,6 +194,7 @@ private struct Parent { } @Reducer +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) private struct Grandparent { struct State: Equatable { var child = Parent.State()