From 5cd90c330dc7d9dd70e57ddaec24f7c6f08785ec Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 17 Jul 2025 13:49:06 -0700 Subject: [PATCH 1/2] Propagate current navigation path to cancellable publisher effects (#3728) * Propagate navigation ID to escaping closure in cancellation. * Add test * wip * wip --------- Co-authored-by: Brandon Williams --- .../Effects/Cancellation.swift | 2 +- .../EffectCancellationTests.swift | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Sources/ComposableArchitecture/Effects/Cancellation.swift b/Sources/ComposableArchitecture/Effects/Cancellation.swift index f5a9047f7b9f..603e74b7b211 100644 --- a/Sources/ComposableArchitecture/Effects/Cancellation.swift +++ b/Sources/ComposableArchitecture/Effects/Cancellation.swift @@ -42,7 +42,7 @@ extension Effect { case let .publisher(publisher): return Self( operation: .publisher( - Deferred { + Deferred { [navigationIDPath] () -> Publishers.HandleEvents< Publishers.PrefixUntilOutput< diff --git a/Tests/ComposableArchitectureTests/EffectCancellationTests.swift b/Tests/ComposableArchitectureTests/EffectCancellationTests.swift index 3f2d8c31682f..4f33cb139899 100644 --- a/Tests/ComposableArchitectureTests/EffectCancellationTests.swift +++ b/Tests/ComposableArchitectureTests/EffectCancellationTests.swift @@ -432,5 +432,37 @@ final class EffectCancellationTests: BaseTCATestCase { // structs in Swift have the same hash value. XCTAssertNotEqual(id1.hashValue, id2.hashValue) } + + func testCancellablePath() async throws { + let navigationIDPath = NavigationIDPath(path: [NavigationID()]) + let effect = withDependencies { + $0.navigationIDPath = navigationIDPath + } operation: { + Effect + .publisher { + Just(()).delay(for: .seconds(1), scheduler: DispatchQueue(label: #function)) + } + .cancellable(id: 1) + } + await withThrowingTaskGroup(of: Void.self) { taskGroup in + taskGroup.addTask { + await withDependencies { + $0.navigationIDPath = NavigationIDPath(path: [NavigationID()]) + } operation: { + for await _ in effect.actions { + XCTFail() + } + } + } + taskGroup.addTask { + try await withDependencies { + $0.navigationIDPath = navigationIDPath + } operation: { + try await Task.sleep(nanoseconds: NSEC_PER_SEC / 2) + Task.cancel(id: 1) + } + } + } + } } #endif From b914abbd730eedd5d1148680f448ebbdc9ead993 Mon Sep 17 00:00:00 2001 From: stephencelis <658+stephencelis@users.noreply.github.com> Date: Thu, 17 Jul 2025 20:57:21 +0000 Subject: [PATCH 2/2] Run swift-format --- Sources/ComposableArchitecture/Effects/Cancellation.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ComposableArchitecture/Effects/Cancellation.swift b/Sources/ComposableArchitecture/Effects/Cancellation.swift index 603e74b7b211..7f075948ff52 100644 --- a/Sources/ComposableArchitecture/Effects/Cancellation.swift +++ b/Sources/ComposableArchitecture/Effects/Cancellation.swift @@ -42,7 +42,8 @@ extension Effect { case let .publisher(publisher): return Self( operation: .publisher( - Deferred { [navigationIDPath] + Deferred { + [navigationIDPath] () -> Publishers.HandleEvents< Publishers.PrefixUntilOutput<