Skip to content

Commit 6085c6c

Browse files
Added helper types for dealing with continuations that can be cancelled
1 parent 735ec24 commit 6085c6c

File tree

1 file changed

+80
-0
lines changed

1 file changed

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

Comments
 (0)