// // AsyncOneShot.swift // Tun // // Created by Codex on 2026/4/28. // import Foundation enum AsyncOneShotError: Error { case timeout } actor AsyncOneShot { private struct Waiter { let continuation: CheckedContinuation let timeoutTask: Task? } private var result: Result? private var waiters: [UUID: Waiter] = [:] func wait() async throws -> Value { try await self.waitInternal(timeout: nil, timeoutError: AsyncOneShotError.timeout) } func wait(timeout: Duration, timeoutError: Error = AsyncOneShotError.timeout) async throws -> Value { try await self.waitInternal(timeout: timeout, timeoutError: timeoutError) } func succeed(_ value: Value) { self.complete(.success(value)) } func fail(_ error: Error) { self.complete(.failure(error)) } private func waitInternal(timeout: Duration?, timeoutError: Error) async throws -> Value { let id = UUID() return try await withTaskCancellationHandler { try await withCheckedThrowingContinuation { continuation in self.addWaiter( id: id, continuation: continuation, timeout: timeout, timeoutError: timeoutError ) } } onCancel: { Task { await self.cancelWaiter(id: id, throwing: CancellationError()) } } } private func addWaiter( id: UUID, continuation: CheckedContinuation, timeout: Duration?, timeoutError: Error ) { if let result { continuation.resume(with: result) return } let timeoutTask = timeout.map { timeout in Task { do { try await Task.sleep(for: timeout) if !Task.isCancelled { self.cancelWaiter(id: id, throwing: timeoutError) } } catch { return } } } self.waiters[id] = Waiter(continuation: continuation, timeoutTask: timeoutTask) } private func complete(_ result: Result) { guard self.result == nil else { return } self.result = result let waiters = self.waiters self.waiters.removeAll() for waiter in waiters.values { waiter.timeoutTask?.cancel() waiter.continuation.resume(with: result) } } private func cancelWaiter(id: UUID, throwing error: Error) { guard let waiter = self.waiters.removeValue(forKey: id) else { return } waiter.timeoutTask?.cancel() waiter.continuation.resume(throwing: error) } }