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/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..be246622091c 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 @@ -69,9 +70,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 +97,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 { @@ -129,28 +124,61 @@ 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 +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) private struct Child { struct State: Equatable { var count = 0 } 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 } } } } @Reducer +@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) private struct Parent { struct State: Equatable { var child = Child.State() @@ -166,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()