Skip to content

Add race, timeout, and deadline async functions #343

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions Sources/AsyncAlgorithms/AsyncRace.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Async Algorithms open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//

/// Returns the value or throws an error, from the first completed or failed operation.
public func race(_ operations: (@Sendable () async throws -> Void)...) async throws {
try await race(operations)
}

/// Returns the value or throws an error, from the first completed or failed operation.
public func race<T: Sendable>(_ operations: (@Sendable () async throws -> T)...) async throws -> T? {
try await race(operations)
}

/// Returns the value or throws an error, from the first completed or failed operation.
public func race<T: Sendable>(_ operations: [@Sendable () async throws -> T]) async throws -> T? {
try await withThrowingTaskGroup(of: T.self) { group in
operations.forEach { operation in
group.addTask { try await operation() }
}
defer {
group.cancelAll()
}
return try await group.next()
}
}

/// Returns the value or throws an error, from the first completed or failed operation.
public func race<T: Sendable>(_ operations: (@Sendable () async throws -> T?)...) async throws -> T? {
try await race(operations)
}

/// Returns the value or throws an error, from the first completed or failed operation.
public func race<T: Sendable>(_ operations: [@Sendable () async throws -> T?]) async throws -> T? {
try await withThrowingTaskGroup(of: T?.self) { group in
operations.forEach { operation in
group.addTask { try await operation() }
}
defer {
group.cancelAll()
}
let value = try await group.next()
switch value {
case .none:
return nil
case let .some(value):
return value
}
}
}
101 changes: 101 additions & 0 deletions Sources/AsyncAlgorithms/AsyncTimeout.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Async Algorithms open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//

/**
- Parameters:
- customError: The failure returned by this closure is thrown when the operation timeouts.
If `customError` is `nil`, then `CancellationError` is thrown.
*/
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public func withTimeout<Success: Sendable>(
_ duration: ContinuousClock.Duration,
tolerance: ContinuousClock.Duration? = nil,
customError: (@Sendable () -> Error)? = nil,
operation: @Sendable () async throws -> Success
) async throws -> Success {
let clock = ContinuousClock()
return try await withDeadline(after: clock.now.advanced(by: duration), tolerance: tolerance, clock: clock, customError: customError, operation: operation)
}

#if compiler(<6.1)
/**
- Parameters:
- customError: The failure returned by this closure is thrown when the operation timeouts.
If `customError` is `nil`, then `CancellationError` is thrown.
*/
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public func withTimeout<C: Clock, Success: Sendable>(
_ duration: C.Duration,
tolerance: C.Duration? = nil,
clock: C,
customError: (@Sendable () -> Error)? = nil,
operation: @Sendable () async throws -> Success
) async throws -> Success {
try await withDeadline(after: clock.now.advanced(by: duration), tolerance: tolerance, clock: clock, customError: customError, operation: operation)
}
#endif

/**
- Parameters:
- customError: The failure returned by this closure is thrown when the operation timeouts.
If `customError` is `nil`, then `CancellationError` is thrown.
*/
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public func withTimeout<Success: Sendable>(
_ duration: Duration,
tolerance: Duration? = nil,
clock: any Clock<Duration>,
customError: (@Sendable () -> Error)? = nil,
operation: @Sendable () async throws -> Success
) async throws -> Success {
try await withoutActuallyEscaping(operation) { operation in
try await race(operation) {
try await clock.sleep(for: duration, tolerance: tolerance)
throw customError?() ?? CancellationError()
}.unsafelyUnwrapped
}
}

/**
- Parameters:
- customError: The failure returned by this closure is thrown when the operation timeouts.
If `customError` is `nil`, then `CancellationError` is thrown.
*/
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public func withDeadline<Success: Sendable>(
after instant: ContinuousClock.Instant,
tolerance: ContinuousClock.Duration? = nil,
customError: (@Sendable () -> Error)? = nil,
operation: @Sendable () async throws -> Success
) async throws -> Success {
try await withDeadline(after: instant, tolerance: tolerance, clock: .continuous, customError: customError, operation: operation)
}

/**
- Parameters:
- customError: The failure returned by this closure is thrown when the operation timeouts.
If `customError` is `nil`, then `CancellationError` is thrown.
*/
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public func withDeadline<C: Clock, Success: Sendable>(
after instant: C.Instant,
tolerance: C.Duration? = nil,
clock: C,
customError: (@Sendable () -> Error)? = nil,
operation: @Sendable () async throws -> Success
) async throws -> Success {
try await withoutActuallyEscaping(operation) { operation in
try await race(operation) {
try await clock.sleep(until: instant, tolerance: tolerance)
throw customError?() ?? CancellationError()
}.unsafelyUnwrapped
}
}