|
| 1 | +//===----------------------------------------------------------------------===// |
| 2 | +// |
| 3 | +// This source file is part of the WebAuthn Swift open source project |
| 4 | +// |
| 5 | +// Copyright (c) 2023 the WebAuthn Swift project authors |
| 6 | +// Licensed under Apache License v2.0 |
| 7 | +// |
| 8 | +// See LICENSE.txt for license information |
| 9 | +// See CONTRIBUTORS.txt for the list of WebAuthn Swift project authors |
| 10 | +// |
| 11 | +// SPDX-License-Identifier: Apache-2.0 |
| 12 | +// |
| 13 | +//===----------------------------------------------------------------------===// |
| 14 | + |
| 15 | +/// An internal type to assist kicking off work without primarily awaiting it, instead allowing that work to call into a continuation as needed. |
| 16 | +/// Use ``withCancellableFirstSuccessfulContinuation()`` instead of invoking this directly. |
| 17 | +actor CancellableContinuation<T: Sendable>: Sendable { |
| 18 | + private var bodyTask: Task<Void, Error>? |
| 19 | + private var continuation: CheckedContinuation<T, Error>? |
| 20 | + private var isCancelled = false |
| 21 | + |
| 22 | + private func cancelMainTask() { |
| 23 | + continuation?.resume(throwing: CancellationError()) |
| 24 | + continuation = nil |
| 25 | + bodyTask?.cancel() |
| 26 | + isCancelled = true |
| 27 | + } |
| 28 | + |
| 29 | + private func isolatedResume(returning value: T) { |
| 30 | + continuation?.resume(returning: value) |
| 31 | + continuation = nil |
| 32 | + cancelMainTask() |
| 33 | + } |
| 34 | + |
| 35 | + nonisolated func cancel() { |
| 36 | + Task { await cancelMainTask() } |
| 37 | + } |
| 38 | + |
| 39 | + nonisolated func resume(returning value: T) { |
| 40 | + Task { await isolatedResume(returning: value) } |
| 41 | + } |
| 42 | + |
| 43 | + /// Wrap an asynchronous closure providing a continuation for when results are ready that can be called any number of times, but also allowing the closure to be cancelled at any time, including once the first successful value is provided. |
| 44 | + fileprivate func wrap(_ body: Body) async throws -> T { |
| 45 | + assert(bodyTask == nil, "A CancellableContinuationTask should only be used once.") |
| 46 | + /// Register a cancellation callback that will: a) immediately cancel the continuation if we have one, b) unset it so it doesn't get called a second time, and c) cancel the main task. |
| 47 | + return try await withTaskCancellationHandler { |
| 48 | + let response: T = try await withCheckedThrowingContinuation { localContinuation in |
| 49 | + /// Synchronously a) check if we've been cancelled, stopping early, b) save the contnuation, and c) assign the task, which runs immediately. |
| 50 | + /// This works since we are guaranteed to hear back from the cancellation handler either immediately, since Task.isCancelled is already set, or after task is set, since we are executing on the actor's executor. |
| 51 | + guard !Task.isCancelled else { |
| 52 | + localContinuation.resume(throwing: CancellationError()) |
| 53 | + return |
| 54 | + } |
| 55 | + |
| 56 | + self.continuation = localContinuation |
| 57 | + self.bodyTask = Task { [unowned self] in |
| 58 | + /// If the continuation doesn't exist at this point, it's because we've already been cancelled. This is guaranteed to run after the task has been set and potentially cancelled since it also runs on the task executor. |
| 59 | + guard let continuation = self.continuation else { return } |
| 60 | + do { |
| 61 | + try await body(self) |
| 62 | + } catch { |
| 63 | + /// If the main body fails for any reason, pass along the error. This will be a no-op if the continuation was already resumed or cancelled. |
| 64 | + continuation.resume(throwing: error) |
| 65 | + self.continuation = nil |
| 66 | + } |
| 67 | + } |
| 68 | + } |
| 69 | + /// Wait for the body to finish cancelling before continuing, so it doesn't run into any data races. |
| 70 | + try? await bodyTask?.value |
| 71 | + return response |
| 72 | + } onCancel: { |
| 73 | + cancel() |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + /// A wrapper for the body, which will ever only be called once, in a non-escaping manner before the continuation resumes. |
| 78 | + fileprivate struct Body: @unchecked Sendable { |
| 79 | + var body: (_ continuation: CancellableContinuation<T>) async throws -> () |
| 80 | + |
| 81 | + func callAsFunction(_ continuation: CancellableContinuation<T>) async throws { |
| 82 | + try await body(continuation) |
| 83 | + } |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +/// Execute an operation providing it a continuation for when results are ready that can be called any number of times, but also allowing the operation to be cancelled at any time, including once the first successful value is provided. |
| 88 | +func withCancellableFirstSuccessfulContinuation<T: Sendable>(_ body: (_ continuation: CancellableContinuation<T>) async throws -> ()) async throws -> T { |
| 89 | + try await withoutActuallyEscaping(body) { escapingBody in |
| 90 | + try await CancellableContinuation().wrap(.init { try await escapingBody($0) }) |
| 91 | + } |
| 92 | +} |
0 commit comments