Created
October 17, 2024 19:01
-
-
Save jayrhynas/bf5ad4497e5281a17c0a06171c2ad5aa to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
typealias CancellationHandler = () -> Void | |
/// Invokes the passed in closure with a checked continuation for the current task. | |
/// | |
/// If the current task is cancelled, the cancellation handler returned by `body` will be invoked. | |
/// | |
/// - Parameters: | |
/// - function: A string identifying the declaration that is the notional source for the continuation, used to identify the continuation in runtime diagnostics related to misuse of this continuation. | |
/// - body: A closure that takes a `CancellableContinuation` parameter and returns a `CancellationHandler`. | |
/// | |
/// - SeeAlso: `withCheckedContinuation(isolation:function:_:)` | |
/// - SeeAlso: `withCheckedThrowingContinuation(isolation:function:_:)` | |
func withCheckedCancellableThrowingContinuation<T>(isolation: isolated (any Actor)? = #isolation, function: String = #function, _ body: (CancellableContinuation<T, any Error>) -> CancellationHandler) async throws -> sending T { | |
let canceller = Canceller() | |
return try await withTaskCancellationHandler(operation: { | |
try canceller.checkCancellation() | |
return try await withCheckedThrowingContinuation(isolation: isolation, function: function) { continuation in | |
let cancellable = body(CancellableContinuation { result in | |
guard canceller.deactivate() else { | |
return | |
} | |
continuation.resume(with: result) | |
}) | |
canceller.assign { | |
cancellable() | |
continuation.resume(throwing: CancellationError()) | |
} | |
} | |
}, onCancel: { | |
canceller.cancel() | |
}, isolation: isolation) | |
} | |
private final class Canceller: @unchecked Sendable { | |
private let lock = NSLock() | |
private var cancellable: CancellationHandler? | |
enum State { | |
case active, cancelled, deactivated | |
} | |
private var _state: State = .active | |
var state: State { | |
lock.withLock { _state } | |
} | |
func cancel() { | |
// only transition to `cancelled` state and call cancellable | |
// if we're currently in the active state | |
let cancellable = lock.withLock { | |
guard _state != .active else { | |
return CancellationHandler?.none | |
} | |
_state = .cancelled | |
defer { self.cancellable = nil } | |
return self.cancellable | |
} | |
// call cancellable outside of lock in case it's slow | |
cancellable?() | |
} | |
func checkCancellation() throws(CancellationError) { | |
if state == .cancelled { | |
throw CancellationError() | |
} | |
} | |
/// Save the cancellable for later invoking | |
/// - Returns: `true` if cancellable was saved, `false` otherwise | |
/// | |
/// - Note: If this has already been cancelled, `cancellable` will be invoked immediately. | |
/// If this has been deactivated, `cancellable` will be ignored. | |
@discardableResult | |
func assign(_ cancellable: @escaping CancellationHandler) -> Bool { | |
lock.lock() | |
switch _state { | |
case .active: | |
self.cancellable = cancellable | |
lock.unlock() | |
return true | |
case .cancelled: | |
lock.unlock() | |
cancellable() | |
return false | |
case .deactivated: | |
lock.unlock() | |
return false | |
} | |
} | |
/// Clears out cancellable and prevents further assignments | |
/// - Returns: `true` if this was active, `false` if it was already cancelled or deactivated. | |
@discardableResult | |
func deactivate() -> Bool { | |
lock.withLock { | |
guard _state == .active else { return false } | |
_state = .deactivated | |
cancellable = nil | |
return true | |
} | |
} | |
} | |
struct CancellableContinuation<T, E: Error> { | |
private let resume: (Result<T, E>) -> Void | |
fileprivate init(resume: @escaping (Result<T, E>) -> Void) { | |
self.resume = resume | |
} | |
func resume(returning value: T) { | |
resume(.success(value)) | |
} | |
func resume(throwing error: E) { | |
resume(.failure(error)) | |
} | |
func resume(with result: Result<T, E>) { | |
resume(result) | |
} | |
func callAsFunction(returning value: T) { | |
resume(.success(value)) | |
} | |
func callAsFunction(throwing error: E) { | |
resume(.failure(error)) | |
} | |
func callAsFunction(with result: Result<T, E>) { | |
resume(result) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment