Files
openclaw/apps/shared/OpenClawKit/Sources/OpenClawKit/AsyncTimeout.swift
Peter Steinberger 28a3540f32 feat: add native inline widget support (#109212)
* feat: add native inline widget support

* refactor: simplify plugin surface refresh

* fix: preserve plugin surface refresh API

* fix: reload widgets after WebKit termination

* fix: harden native widget refresh

* fix: bound native widget recovery

* docs: defer native widget release note

* chore: refresh native i18n inventory

* fix: harden native widget delivery

* refactor: simplify native widget API surface

* fix: serialize native widget capability refresh

* fix: harden native widget recovery

* fix: recover native widget capabilities

* style: format widget refresh role selection

* fix: correct widget refresh formatting

* fix: harden cross-platform widget recovery

* fix: bind widget trust and recovery to routes

* chore: refresh native i18n inventory

* chore: regenerate Android gateway protocol

* test: remove unused release workflow read
2026-07-16 19:24:41 -07:00

118 lines
3.9 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)
// Unstructured racers let the caller return without awaiting a cancellation-ignoring loser.
// The actor resumes once and cancels every racer; 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)
}
}
var tasks = [operationTask]
if clamped > 0 {
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)
}
}
tasks.append(timeoutTask)
}
if Task.isCancelled {
tasks.forEach { $0.cancel() }
throw CancellationError()
}
return try await race.wait(for: tasks)
} 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)
}
}