diff --git a/Sources/ComposableArchitecture/Core.swift b/Sources/ComposableArchitecture/Core.swift index b50119322fb0..5a2c876db524 100644 --- a/Sources/ComposableArchitecture/Core.swift +++ b/Sources/ComposableArchitecture/Core.swift @@ -136,9 +136,9 @@ final class RootCore: Core { task.cancel() } } - case let .run(priority, operation): + case let .run(name, priority, operation): withEscapedDependencies { continuation in - let task = Task(priority: priority) { @MainActor [weak self] in + let task = Task(name: name, priority: priority) { @MainActor [weak self] in let isCompleted = LockIsolated(false) defer { isCompleted.setValue(true) } await operation( diff --git a/Sources/ComposableArchitecture/Effect.swift b/Sources/ComposableArchitecture/Effect.swift index 965565d50a14..505bd797e3f4 100644 --- a/Sources/ComposableArchitecture/Effect.swift +++ b/Sources/ComposableArchitecture/Effect.swift @@ -7,7 +7,11 @@ public struct Effect: Sendable { enum Operation: Sendable { case none case publisher(AnyPublisher) - case run(TaskPriority? = nil, @Sendable (_ send: Send) async -> Void) + case run( + name: String? = nil, + priority: TaskPriority? = nil, + operation: @Sendable (_ send: Send) async -> Void + ) } @usableFromInline @@ -76,6 +80,7 @@ extension Effect { /// - Parameters: /// - priority: Priority of the underlying task. If `nil`, the priority will come from /// `Task.currentPriority`. + /// - name: An optional name to associate with the task that runs this effect. /// - operation: The operation to execute. /// - handler: An error handler, invoked if the operation throws an error other than /// `CancellationError`. @@ -86,6 +91,7 @@ extension Effect { /// - Returns: An effect wrapping the given asynchronous work. public static func run( priority: TaskPriority? = nil, + name: String? = nil, operation: @escaping @Sendable (_ send: Send) async throws -> Void, catch handler: (@Sendable (_ error: any Error, _ send: Send) async -> Void)? = nil, fileID: StaticString = #fileID, @@ -95,7 +101,7 @@ extension Effect { ) -> Self { withEscapedDependencies { escaped in Self( - operation: .run(priority) { send in + operation: .run(name: name, priority: priority) { send in await escaped.yield { do { try await operation(send) @@ -268,14 +274,17 @@ extension Effect { .eraseToAnyPublisher() ) ) - case let (.run(lhsPriority, lhsOperation), .run(rhsPriority, rhsOperation)): + case ( + .run(let lhsName, let lhsPriority, let lhsOperation), + .run(let rhsName, let rhsPriority, let rhsOperation) + ): return Self( operation: .run { send in await withTaskGroup(of: Void.self) { group in - group.addTask(priority: lhsPriority) { + group.addTask(name: lhsName, priority: lhsPriority) { await lhsOperation(send) } - group.addTask(priority: rhsPriority) { + group.addTask(name: rhsName, priority: rhsPriority) { await rhsOperation(send) } } @@ -328,16 +337,21 @@ extension Effect { .eraseToAnyPublisher() ) ) - case let (.run(lhsPriority, lhsOperation), .run(rhsPriority, rhsOperation)): + case ( + .run(let lhsName, let lhsPriority, let lhsOperation), + .run(let rhsName, let rhsPriority, let rhsOperation) + ): return Self( operation: .run { send in if let lhsPriority { - await Task(priority: lhsPriority) { await lhsOperation(send) }.cancellableValue + await Task(name: lhsName, priority: lhsPriority) { await lhsOperation(send) } + .cancellableValue } else { await lhsOperation(send) } if let rhsPriority { - await Task(priority: rhsPriority) { await rhsOperation(send) }.cancellableValue + await Task(name: rhsName, priority: rhsPriority) { await rhsOperation(send) } + .cancellableValue } else { await rhsOperation(send) } @@ -356,7 +370,7 @@ extension Effect { switch self.operation { case .none: return .none - case let .publisher(publisher): + case .publisher(let publisher): return .init( operation: .publisher( publisher @@ -372,10 +386,10 @@ extension Effect { .eraseToAnyPublisher() ) ) - case let .run(priority, operation): + case .run(let name, let priority, let operation): return withEscapedDependencies { escaped in .init( - operation: .run(priority) { send in + operation: .run(name: name, priority: priority) { send in await escaped.yield { await operation( Send { action in @@ -389,3 +403,39 @@ extension Effect { } } } + +#if swift(<6.2) + // NB: Backwards-compatible shims. + extension Task { + @discardableResult + @usableFromInline + init( + name: String?, + priority: TaskPriority? = nil, + operation: @escaping @Sendable () async -> Success + ) where Failure == Never { + self.init(priority: priority, operation: operation) + } + + @discardableResult + @usableFromInline + init( + name: String?, + priority: TaskPriority? = nil, + operation: @escaping @Sendable () async throws -> Success + ) where Failure == Error { + self.init(priority: priority, operation: operation) + } + } + + extension TaskGroup { + @usableFromInline + mutating func addTask( + name: String?, + priority: TaskPriority? = nil, + operation: @escaping @Sendable () async -> ChildTaskResult + ) { + addTask(priority: priority, operation: operation) + } + } +#endif diff --git a/Sources/ComposableArchitecture/Effects/Animation.swift b/Sources/ComposableArchitecture/Effects/Animation.swift index 7b46866dead4..6bb4c21b6ebf 100644 --- a/Sources/ComposableArchitecture/Effects/Animation.swift +++ b/Sources/ComposableArchitecture/Effects/Animation.swift @@ -42,10 +42,10 @@ extension Effect { TransactionPublisher(upstream: publisher, transaction: transaction).eraseToAnyPublisher() ) ) - case let .run(priority, operation): + case let .run(name, priority, operation): let uncheckedTransaction = UncheckedSendable(transaction) return Self( - operation: .run(priority) { send in + operation: .run(name: name, priority: priority) { send in await operation( Send { value in withTransaction(uncheckedTransaction.value) { diff --git a/Sources/ComposableArchitecture/Effects/Cancellation.swift b/Sources/ComposableArchitecture/Effects/Cancellation.swift index 7f075948ff52..a6ef9552a1fa 100644 --- a/Sources/ComposableArchitecture/Effects/Cancellation.swift +++ b/Sources/ComposableArchitecture/Effects/Cancellation.swift @@ -83,10 +83,10 @@ extension Effect { .eraseToAnyPublisher() ) ) - case let .run(priority, operation): + case let .run(name, priority, operation): return withEscapedDependencies { continuation in return Self( - operation: .run(priority) { send in + operation: .run(name: name, priority: priority) { send in await continuation.yield { await withTaskCancellation(id: id, cancelInFlight: cancelInFlight) { await operation(send) diff --git a/Sources/ComposableArchitecture/Effects/Publisher.swift b/Sources/ComposableArchitecture/Effects/Publisher.swift index 4093d31c9d11..8fb9b7a661e7 100644 --- a/Sources/ComposableArchitecture/Effects/Publisher.swift +++ b/Sources/ComposableArchitecture/Effects/Publisher.swift @@ -30,9 +30,9 @@ public struct _EffectPublisher: Publisher { return Empty().eraseToAnyPublisher() case let .publisher(publisher): return publisher - case let .run(priority, operation): + case let .run(name, priority, operation): return .create { subscriber in - let task = Task(priority: priority) { @MainActor in + let task = Task(name: name, priority: priority) { @MainActor in defer { subscriber.send(completion: .finished) } await operation(Send { subscriber.send($0) }) } diff --git a/Sources/ComposableArchitecture/Internal/EffectActions.swift b/Sources/ComposableArchitecture/Internal/EffectActions.swift index 0dc15cad4f2c..338be97c11e6 100644 --- a/Sources/ComposableArchitecture/Internal/EffectActions.swift +++ b/Sources/ComposableArchitecture/Internal/EffectActions.swift @@ -15,9 +15,9 @@ extension Effect where Action: Sendable { cancellable.cancel() } } - case let .run(priority, operation): + case let .run(name, priority, operation): return AsyncStream { continuation in - let task = Task(priority: priority) { + let task = Task(name: name, priority: priority) { await operation(Send { action in continuation.yield(action) }) continuation.finish() } diff --git a/Sources/ComposableArchitecture/Reducer/Reducers/SignpostReducer.swift b/Sources/ComposableArchitecture/Reducer/Reducers/SignpostReducer.swift index aa32beaed479..01769f025e30 100644 --- a/Sources/ComposableArchitecture/Reducer/Reducers/SignpostReducer.swift +++ b/Sources/ComposableArchitecture/Reducer/Reducers/SignpostReducer.swift @@ -114,9 +114,9 @@ extension Effect { .eraseToAnyPublisher() ) ) - case let .run(priority, operation): + case let .run(name, priority, operation): return .init( - operation: .run(priority) { send in + operation: .run(name: name, priority: priority) { send in os_signpost( .begin, log: log, name: "Effect", signpostID: sid, "%sStarted from %s", prefix, actionOutput diff --git a/Tests/ComposableArchitectureTests/EffectOperationTests.swift b/Tests/ComposableArchitectureTests/EffectOperationTests.swift index 7936a1d32b5c..0863474cb422 100644 --- a/Tests/ComposableArchitectureTests/EffectOperationTests.swift +++ b/Tests/ComposableArchitectureTests/EffectOperationTests.swift @@ -17,7 +17,7 @@ effect = Effect.run { send in await send(42) } .merge(with: .none) switch effect.operation { - case let .run(_, send): + case let .run(_, _, send): await send(.init(send: { XCTAssertEqual($0, 42) })) default: XCTFail() @@ -26,7 +26,7 @@ effect = Effect.none .merge(with: .run { send in await send(42) }) switch effect.operation { - case let .run(_, send): + case let .run(_, _, send): await send(.init(send: { XCTAssertEqual($0, 42) })) default: XCTFail() @@ -35,7 +35,7 @@ effect = Effect.run { await $0(42) } .merge(with: .none) switch effect.operation { - case let .run(_, send): + case let .run(_, _, send): await send(.init(send: { XCTAssertEqual($0, 42) })) default: XCTFail() @@ -44,7 +44,7 @@ effect = Effect.none .merge(with: .run { await $0(42) }) switch effect.operation { - case let .run(_, send): + case let .run(_, _, send): await send(.init(send: { XCTAssertEqual($0, 42) })) default: XCTFail() @@ -64,7 +64,7 @@ effect = Effect.run { send in await send(42) } .concatenate(with: .none) switch effect.operation { - case let .run(_, send): + case let .run(_, _, send): await send(.init(send: { XCTAssertEqual($0, 42) })) default: XCTFail() @@ -73,7 +73,7 @@ effect = Effect.none .concatenate(with: .run { send in await send(42) }) switch effect.operation { - case let .run(_, send): + case let .run(_, _, send): await send(.init(send: { XCTAssertEqual($0, 42) })) default: XCTFail() @@ -82,7 +82,7 @@ effect = Effect.run { send in await send(42) } .concatenate(with: .none) switch effect.operation { - case let .run(_, send): + case let .run(_, _, send): await send(.init(send: { XCTAssertEqual($0, 42) })) default: XCTFail() @@ -91,7 +91,7 @@ effect = Effect.none .concatenate(with: .run { send in await send(42) }) switch effect.operation { - case let .run(_, send): + case let .run(_, _, send): await send(.init(send: { XCTAssertEqual($0, 42) })) default: XCTFail() @@ -113,7 +113,7 @@ } ) switch effect.operation { - case let .run(_, send): + case let .run(_, _, send): await send(.init { values.append($0) }) default: XCTFail() @@ -129,7 +129,7 @@ let effect = Effect.run { send in await send(42) } .concatenate(with: .run { send in await send(1729) }) switch effect.operation { - case let .run(_, send): + case let .run(_, _, send): await send(.init(send: { values.append($0) })) default: XCTFail() @@ -143,7 +143,7 @@ .map { "\($0)" } switch effect.operation { - case let .run(_, send): + case let .run(_, _, send): await send(.init(send: { XCTAssertEqual($0, "42") })) default: XCTFail()