mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-18 14:01:41 +00:00
* fix(ios): return promptly from noncooperative timeouts * fix(apple): harden async timeout cancellation Co-authored-by: NianJiuZst <180004567+NianJiuZst@users.noreply.github.com> * fix(apple): preserve legacy location requests * style(apple): format location continuation code --------- Co-authored-by: NianJiuZst <180004567+NianJiuZst@users.noreply.github.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
119 lines
3.8 KiB
Swift
119 lines
3.8 KiB
Swift
import Foundation
|
|
|
|
private actor AsyncTimeoutRace<T: Sendable> {
|
|
private enum Outcome {
|
|
case success(T)
|
|
case failure(any Error)
|
|
}
|
|
|
|
private var outcome: Outcome?
|
|
private var continuation: CheckedContinuation<T, any Error>?
|
|
private var tasks: [Task<Void, Never>] = []
|
|
|
|
func wait(for tasks: [Task<Void, Never>]) async throws -> T {
|
|
if let outcome {
|
|
tasks.forEach { $0.cancel() }
|
|
return try self.value(from: outcome)
|
|
}
|
|
self.tasks = tasks
|
|
return try await withCheckedThrowingContinuation { continuation in
|
|
self.continuation = continuation
|
|
}
|
|
}
|
|
|
|
private func resolve(_ outcome: Outcome) {
|
|
guard self.outcome == nil else { return }
|
|
self.outcome = outcome
|
|
self.tasks.forEach { $0.cancel() }
|
|
self.tasks.removeAll()
|
|
if let continuation {
|
|
self.continuation = nil
|
|
self.resume(continuation, with: outcome)
|
|
}
|
|
}
|
|
|
|
func resolveSuccess(_ value: T) {
|
|
self.resolve(.success(value))
|
|
}
|
|
|
|
func resolveFailure(_ error: any Error) {
|
|
self.resolve(.failure(error))
|
|
}
|
|
|
|
private func resume(_ continuation: CheckedContinuation<T, any Error>, with outcome: Outcome) {
|
|
switch outcome {
|
|
case let .success(value):
|
|
continuation.resume(returning: value)
|
|
case let .failure(error):
|
|
continuation.resume(throwing: error)
|
|
}
|
|
}
|
|
|
|
private func value(from outcome: Outcome) throws -> T {
|
|
switch outcome {
|
|
case let .success(value):
|
|
return value
|
|
case let .failure(error):
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum AsyncTimeout {
|
|
public static func withTimeout<T: Sendable>(
|
|
seconds: Double,
|
|
onTimeout: @escaping @Sendable () -> Error,
|
|
operation: @escaping @Sendable () async throws -> T) async throws -> T
|
|
{
|
|
let clamped = max(0, seconds)
|
|
if clamped == 0 {
|
|
return try await operation()
|
|
}
|
|
|
|
// Unstructured racers let the caller return without awaiting a cancellation-ignoring loser.
|
|
// The actor resumes once and cancels both tasks; noncooperative work may still finish later,
|
|
// so callers must own resource cleanup and stale-result safety.
|
|
let race = AsyncTimeoutRace<T>()
|
|
return try await withTaskCancellationHandler {
|
|
try Task.checkCancellation()
|
|
|
|
let operationTask = Task {
|
|
do {
|
|
let value = try await operation()
|
|
await race.resolveSuccess(value)
|
|
} catch {
|
|
await race.resolveFailure(error)
|
|
}
|
|
}
|
|
let timeoutTask = Task {
|
|
do {
|
|
try await Task.sleep(nanoseconds: UInt64(clamped * 1_000_000_000))
|
|
await race.resolveFailure(onTimeout())
|
|
} catch is CancellationError {
|
|
// The operation or caller resolved the race first.
|
|
} catch {
|
|
await race.resolveFailure(error)
|
|
}
|
|
}
|
|
if Task.isCancelled {
|
|
operationTask.cancel()
|
|
timeoutTask.cancel()
|
|
throw CancellationError()
|
|
}
|
|
return try await race.wait(for: [operationTask, timeoutTask])
|
|
} onCancel: {
|
|
Task { await race.resolveFailure(CancellationError()) }
|
|
}
|
|
}
|
|
|
|
public static func withTimeoutMs<T: Sendable>(
|
|
timeoutMs: Int,
|
|
onTimeout: @escaping @Sendable () -> Error,
|
|
operation: @escaping @Sendable () async throws -> T) async throws -> T
|
|
{
|
|
let clamped = max(0, timeoutMs)
|
|
let seconds = Double(clamped) / 1000.0
|
|
return try await self.withTimeout(seconds: seconds, onTimeout: onTimeout, operation: operation)
|
|
}
|
|
}
|