diff --git a/apps/ios/Sources/Bridge/BridgeClient.swift b/apps/ios/Sources/Bridge/BridgeClient.swift index 4bb88152a..d4204fb4f 100644 --- a/apps/ios/Sources/Bridge/BridgeClient.swift +++ b/apps/ios/Sources/Bridge/BridgeClient.swift @@ -161,18 +161,10 @@ actor BridgeClient { purpose: String, _ op: @escaping @Sendable () async throws -> T) async throws -> T { - try await withThrowingTaskGroup(of: T.self) { group in - group.addTask { - try await op() - } - group.addTask { - try await Task.sleep(nanoseconds: UInt64(seconds) * 1_000_000_000) - throw TimeoutError(purpose: purpose, seconds: seconds) - } - let result = try await group.next()! - group.cancelAll() - return result - } + try await AsyncTimeout.withTimeout( + seconds: Double(seconds), + onTimeout: { TimeoutError(purpose: purpose, seconds: seconds) }, + operation: op) } private func startAndWaitForReady(_ connection: NWConnection, queue: DispatchQueue) async throws { diff --git a/apps/ios/Sources/Bridge/BridgeSession.swift b/apps/ios/Sources/Bridge/BridgeSession.swift index c8a834237..9e0f20604 100644 --- a/apps/ios/Sources/Bridge/BridgeSession.swift +++ b/apps/ios/Sources/Bridge/BridgeSession.swift @@ -321,20 +321,10 @@ actor BridgeSession { seconds: Double, operation: @escaping @Sendable () async throws -> T) async throws -> T { - try await withThrowingTaskGroup(of: T.self) { group in - group.addTask { try await operation() } - group.addTask { - try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) - throw TimeoutError(message: "UNAVAILABLE: connection timeout") - } - - guard let first = try await group.next() else { - throw TimeoutError(message: "UNAVAILABLE: connection timeout") - } - - group.cancelAll() - return first - } + try await AsyncTimeout.withTimeout( + seconds: seconds, + onTimeout: { TimeoutError(message: "UNAVAILABLE: connection timeout") }, + operation: operation) } private static func makeStateStream(for connection: NWConnection) -> AsyncStream { diff --git a/apps/ios/Sources/Location/LocationService.swift b/apps/ios/Sources/Location/LocationService.swift index 3c1a80516..0b241c811 100644 --- a/apps/ios/Sources/Location/LocationService.swift +++ b/apps/ios/Sources/Location/LocationService.swift @@ -90,20 +90,7 @@ final class LocationService: NSObject, CLLocationManagerDelegate { timeoutMs: Int, operation: @escaping @Sendable () async throws -> T) async throws -> T { - if timeoutMs == 0 { - return try await operation() - } - - return try await withThrowingTaskGroup(of: T.self) { group in - group.addTask { try await operation() } - group.addTask { - try await Task.sleep(nanoseconds: UInt64(timeoutMs) * 1_000_000) - throw Error.timeout - } - let result = try await group.next()! - group.cancelAll() - return result - } + try await AsyncTimeout.withTimeoutMs(timeoutMs: timeoutMs, onTimeout: { Error.timeout }, operation: operation) } private static func accuracyValue(_ accuracy: ClawdbotLocationAccuracy) -> CLLocationAccuracy { diff --git a/apps/macos/Sources/Clawdbot/AsyncTimeout.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/AsyncTimeout.swift similarity index 54% rename from apps/macos/Sources/Clawdbot/AsyncTimeout.swift rename to apps/shared/ClawdbotKit/Sources/ClawdbotKit/AsyncTimeout.swift index 7ab4627c4..eed2d758a 100644 --- a/apps/macos/Sources/Clawdbot/AsyncTimeout.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/AsyncTimeout.swift @@ -1,12 +1,16 @@ import Foundation -enum AsyncTimeout { - static func withTimeout( +public enum AsyncTimeout { + public static func withTimeout( 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() + } + return try await withThrowingTaskGroup(of: T.self) { group in group.addTask { try await operation() } group.addTask { @@ -19,4 +23,14 @@ enum AsyncTimeout { throw onTimeout() } } + + public static func withTimeoutMs( + 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) + } }