Skip to content

Commit 48f0cc6

Browse files
authored
Add @_unsafeInheritExecutor to withTaskCancellation(id:) (#1779)
* Add @_unsafeInheritExecutor to withTaskCancellation(id:) * wip
1 parent 43291b2 commit 48f0cc6

File tree

2 files changed

+139
-91
lines changed

2 files changed

+139
-91
lines changed

Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/ComposableArchitecture/Effects/Cancellation.swift

Lines changed: 137 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -152,101 +152,149 @@ extension EffectPublisher {
152152
}
153153
}
154154

155-
/// Execute an operation with a cancellation identifier.
156-
///
157-
/// If the operation is in-flight when `Task.cancel(id:)` is called with the same identifier, the
158-
/// operation will be cancelled.
159-
///
160-
/// ```
161-
/// enum CancelID.self {}
162-
///
163-
/// await withTaskCancellation(id: CancelID.self) {
164-
/// // ...
165-
/// }
166-
/// ```
167-
///
168-
/// ### Debouncing tasks
169-
///
170-
/// When paired with a clock, this function can be used to debounce a unit of async work by
171-
/// specifying the `cancelInFlight`, which will automatically cancel any in-flight work with the
172-
/// same identifier:
173-
///
174-
/// ```swift
175-
/// @Dependency(\.continuousClock) var clock
176-
/// enum CancelID {}
177-
///
178-
/// // ...
179-
///
180-
/// return .task {
181-
/// await withTaskCancellation(id: CancelID.self, cancelInFlight: true) {
182-
/// try await self.clock.sleep(for: .seconds(0.3))
183-
/// return await .debouncedResponse(
184-
/// TaskResult { try await environment.request() }
185-
/// )
186-
/// }
187-
/// }
188-
/// ```
189-
///
190-
/// - Parameters:
191-
/// - id: A unique identifier for the operation.
192-
/// - cancelInFlight: Determines if any in-flight operation with the same identifier should be
193-
/// canceled before starting this new one.
194-
/// - operation: An async operation.
195-
/// - Throws: An error thrown by the operation.
196-
/// - Returns: A value produced by operation.
197-
public func withTaskCancellation<T: Sendable>(
198-
id: AnyHashable,
199-
cancelInFlight: Bool = false,
200-
operation: @Sendable @escaping () async throws -> T
201-
) async rethrows -> T {
202-
let id = _CancelToken(id: id)
203-
let (cancellable, task) = _cancellablesLock.sync { () -> (AnyCancellable, Task<T, Error>) in
204-
if cancelInFlight {
205-
_cancellationCancellables[id]?.forEach { $0.cancel() }
155+
#if swift(>=5.7)
156+
/// Execute an operation with a cancellation identifier.
157+
///
158+
/// If the operation is in-flight when `Task.cancel(id:)` is called with the same identifier, the
159+
/// operation will be cancelled.
160+
///
161+
/// ```
162+
/// enum CancelID.self {}
163+
///
164+
/// await withTaskCancellation(id: CancelID.self) {
165+
/// // ...
166+
/// }
167+
/// ```
168+
///
169+
/// ### Debouncing tasks
170+
///
171+
/// When paired with a clock, this function can be used to debounce a unit of async work by
172+
/// specifying the `cancelInFlight`, which will automatically cancel any in-flight work with the
173+
/// same identifier:
174+
///
175+
/// ```swift
176+
/// @Dependency(\.continuousClock) var clock
177+
/// enum CancelID {}
178+
///
179+
/// // ...
180+
///
181+
/// return .task {
182+
/// await withTaskCancellation(id: CancelID.self, cancelInFlight: true) {
183+
/// try await self.clock.sleep(for: .seconds(0.3))
184+
/// return await .debouncedResponse(
185+
/// TaskResult { try await environment.request() }
186+
/// )
187+
/// }
188+
/// }
189+
/// ```
190+
///
191+
/// - Parameters:
192+
/// - id: A unique identifier for the operation.
193+
/// - cancelInFlight: Determines if any in-flight operation with the same identifier should be
194+
/// canceled before starting this new one.
195+
/// - operation: An async operation.
196+
/// - Throws: An error thrown by the operation.
197+
/// - Returns: A value produced by operation.
198+
@_unsafeInheritExecutor
199+
public func withTaskCancellation<T: Sendable>(
200+
id: AnyHashable,
201+
cancelInFlight: Bool = false,
202+
operation: @Sendable @escaping () async throws -> T
203+
) async rethrows -> T {
204+
let id = _CancelToken(id: id)
205+
let (cancellable, task) = _cancellablesLock.sync { () -> (AnyCancellable, Task<T, Error>) in
206+
if cancelInFlight {
207+
_cancellationCancellables[id]?.forEach { $0.cancel() }
208+
}
209+
let task = Task { try await operation() }
210+
let cancellable = AnyCancellable { task.cancel() }
211+
_cancellationCancellables[id, default: []].insert(cancellable)
212+
return (cancellable, task)
206213
}
207-
let task = Task { try await operation() }
208-
let cancellable = AnyCancellable { task.cancel() }
209-
_cancellationCancellables[id, default: []].insert(cancellable)
210-
return (cancellable, task)
211-
}
212-
defer {
213-
_cancellablesLock.sync {
214-
_cancellationCancellables[id]?.remove(cancellable)
215-
if _cancellationCancellables[id]?.isEmpty == .some(true) {
216-
_cancellationCancellables[id] = nil
214+
defer {
215+
_cancellablesLock.sync {
216+
_cancellationCancellables[id]?.remove(cancellable)
217+
if _cancellationCancellables[id]?.isEmpty == .some(true) {
218+
_cancellationCancellables[id] = nil
219+
}
217220
}
218221
}
222+
do {
223+
return try await task.cancellableValue
224+
} catch {
225+
return try Result<T, Error>.failure(error)._rethrowGet()
226+
}
219227
}
220-
do {
221-
return try await task.cancellableValue
222-
} catch {
223-
return try Result<T, Error>.failure(error)._rethrowGet()
228+
#else
229+
public func withTaskCancellation<T: Sendable>(
230+
id: AnyHashable,
231+
cancelInFlight: Bool = false,
232+
operation: @Sendable @escaping () async throws -> T
233+
) async rethrows -> T {
234+
let id = _CancelToken(id: id)
235+
let (cancellable, task) = _cancellablesLock.sync { () -> (AnyCancellable, Task<T, Error>) in
236+
if cancelInFlight {
237+
_cancellationCancellables[id]?.forEach { $0.cancel() }
238+
}
239+
let task = Task { try await operation() }
240+
let cancellable = AnyCancellable { task.cancel() }
241+
_cancellationCancellables[id, default: []].insert(cancellable)
242+
return (cancellable, task)
243+
}
244+
defer {
245+
_cancellablesLock.sync {
246+
_cancellationCancellables[id]?.remove(cancellable)
247+
if _cancellationCancellables[id]?.isEmpty == .some(true) {
248+
_cancellationCancellables[id] = nil
249+
}
250+
}
251+
}
252+
do {
253+
return try await task.cancellableValue
254+
} catch {
255+
return try Result<T, Error>.failure(error)._rethrowGet()
256+
}
224257
}
225-
}
258+
#endif
226259

227-
/// Execute an operation with a cancellation identifier.
228-
///
229-
/// A convenience for calling ``withTaskCancellation(id:cancelInFlight:operation:)-4dtr6`` with a
230-
/// static type as the operation's unique identifier.
231-
///
232-
/// - Parameters:
233-
/// - id: A unique type identifying the operation.
234-
/// - cancelInFlight: Determines if any in-flight operation with the same identifier should be
235-
/// canceled before starting this new one.
236-
/// - operation: An async operation.
237-
/// - Throws: An error thrown by the operation.
238-
/// - Returns: A value produced by operation.
239-
public func withTaskCancellation<T: Sendable>(
240-
id: Any.Type,
241-
cancelInFlight: Bool = false,
242-
operation: @Sendable @escaping () async throws -> T
243-
) async rethrows -> T {
244-
try await withTaskCancellation(
245-
id: ObjectIdentifier(id),
246-
cancelInFlight: cancelInFlight,
247-
operation: operation
248-
)
249-
}
260+
#if swift(>=5.7)
261+
/// Execute an operation with a cancellation identifier.
262+
///
263+
/// A convenience for calling ``withTaskCancellation(id:cancelInFlight:operation:)-4dtr6`` with a
264+
/// static type as the operation's unique identifier.
265+
///
266+
/// - Parameters:
267+
/// - id: A unique type identifying the operation.
268+
/// - cancelInFlight: Determines if any in-flight operation with the same identifier should be
269+
/// canceled before starting this new one.
270+
/// - operation: An async operation.
271+
/// - Throws: An error thrown by the operation.
272+
/// - Returns: A value produced by operation.
273+
@_unsafeInheritExecutor
274+
public func withTaskCancellation<T: Sendable>(
275+
id: Any.Type,
276+
cancelInFlight: Bool = false,
277+
operation: @Sendable @escaping () async throws -> T
278+
) async rethrows -> T {
279+
try await withTaskCancellation(
280+
id: ObjectIdentifier(id),
281+
cancelInFlight: cancelInFlight,
282+
operation: operation
283+
)
284+
}
285+
#else
286+
public func withTaskCancellation<T: Sendable>(
287+
id: Any.Type,
288+
cancelInFlight: Bool = false,
289+
operation: @Sendable @escaping () async throws -> T
290+
) async rethrows -> T {
291+
try await withTaskCancellation(
292+
id: ObjectIdentifier(id),
293+
cancelInFlight: cancelInFlight,
294+
operation: operation
295+
)
296+
}
297+
#endif
250298

251299
extension Task where Success == Never, Failure == Never {
252300
/// Cancel any currently in-flight operation with the given identifier.

0 commit comments

Comments
 (0)