Skip to content

Commit 924d3fd

Browse files
Added helper types for dealing with continuations that can be cancelled
1 parent b1c51af commit 924d3fd

File tree

1 file changed

+92
-0
lines changed

1 file changed

+92
-0
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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

Comments
 (0)