|
| 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 task: 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 | + task?.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: @Sendable (_ continuation: CancellableContinuation) async throws -> ()) async throws -> T { |
| 45 | + assert(task == 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 | + try await withoutActuallyEscaping(body) { escapingBody in |
| 49 | + try await withCheckedThrowingContinuation { localContinuation in |
| 50 | + /// Synchronously a) check if we've been cancelled, stopping early, b) save the contnuation, and c) assign the task, which runs immediately. |
| 51 | + /// 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. |
| 52 | + guard !Task.isCancelled else { |
| 53 | + localContinuation.resume(throwing: CancellationError()) |
| 54 | + return |
| 55 | + } |
| 56 | + |
| 57 | + self.continuation = localContinuation |
| 58 | + self.task = Task { [unowned self] in |
| 59 | + /// 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. |
| 60 | + guard let continuation = self.continuation else { return } |
| 61 | + do { |
| 62 | + try await escapingBody(self) |
| 63 | + } catch { |
| 64 | + /// 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. |
| 65 | + continuation.resume(throwing: error) |
| 66 | + self.continuation = nil |
| 67 | + } |
| 68 | + } |
| 69 | + } |
| 70 | + } |
| 71 | + } onCancel: { |
| 72 | + cancel() |
| 73 | + } |
| 74 | + } |
| 75 | +} |
| 76 | + |
| 77 | +/// 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. |
| 78 | +func withCancellableFirstSuccessfulContinuation<T: Sendable>(_ body: (_ continuation: CancellableContinuation<T>) async throws -> ()) async throws -> T { |
| 79 | + try await CancellableContinuation().wrap { try await body($0) } |
| 80 | +} |
0 commit comments