Files
steipete--codexbar/Sources/CodexBarCore/OpenAIWeb/OpenAIDashboardBrowserCookieImporter+Deadline.swift
2026-07-13 12:22:33 +08:00

191 lines
7.2 KiB
Swift

#if os(macOS)
import Foundation
extension OpenAIDashboardBrowserCookieImporter {
private struct PendingCookieStoreMutation {
let token: UUID
let task: Task<Void, Never>
}
@MainActor private static var pendingCookieStoreMutations: [ObjectIdentifier: PendingCookieStoreMutation] = [:]
private nonisolated static let cookieCacheQueue = DispatchQueue(
label: "com.steipete.codexbar.openai-cookie-cache")
private nonisolated static let deadlineQueue = DispatchQueue(
label: "com.steipete.codexbar.openai-cookie-deadline",
qos: .userInitiated)
private final class CookieLoadCompletion: @unchecked Sendable {
private let lock = NSLock()
private var didFinish = false
func finish(_ action: () -> Void) {
let shouldFinish = self.lock.withLock {
guard !self.didFinish else { return false }
self.didFinish = true
return true
}
if shouldFinish { action() }
}
}
nonisolated static func remainingTimeout(
until deadline: Date?,
cappedAt localLimit: TimeInterval? = nil,
now: Date = Date()) throws -> TimeInterval
{
guard let deadline else {
return localLimit.map(OpenAIDashboardFetcher.sanitizedTimeout) ?? .greatestFiniteMagnitude
}
let remaining = deadline.timeIntervalSince(now)
guard remaining > 0 else { throw URLError(.timedOut) }
guard let localLimit else { return remaining }
return min(OpenAIDashboardFetcher.sanitizedTimeout(localLimit), remaining)
}
nonisolated static func runBoundedCookieLoad<T: Sendable>(
deadline: Date?,
timeoutObserver: (@Sendable () -> Void)? = nil,
operation: @escaping @Sendable () throws -> T) async throws -> T
{
// The detached/GCD loader does not inherit TaskLocal prompt policy or retry selection.
let contextualOperation = BrowserCookieAccessGate.operationPreservingAccessContext(operation)
guard let deadline else {
return try await Task.detached(priority: .userInitiated, operation: contextualOperation).value
}
let timeout = try self.remainingTimeout(until: deadline)
let completion = CookieLoadCompletion()
return try await withCheckedThrowingContinuation { continuation in
DispatchQueue.global(qos: .userInitiated).async {
let result = Result(catching: contextualOperation)
completion.finish { continuation.resume(with: result) }
}
self.deadlineQueue.asyncAfter(deadline: .now() + timeout) {
completion.finish {
timeoutObserver?()
continuation.resume(throwing: URLError(.timedOut))
}
}
}
}
nonisolated static func runBoundedCookieCacheOperation<T: Sendable>(
deadline: Date?,
operation: @escaping @Sendable () throws -> T) async throws -> T
{
guard let deadline else {
return try await withCheckedThrowingContinuation { continuation in
self.cookieCacheQueue.async {
continuation.resume(with: Result(catching: operation))
}
}
}
let timeout = try self.remainingTimeout(until: deadline)
let completion = CookieLoadCompletion()
return try await withCheckedThrowingContinuation { continuation in
self.cookieCacheQueue.async {
let result = Result(catching: operation)
completion.finish { continuation.resume(with: result) }
}
self.deadlineQueue.asyncAfter(deadline: .now() + timeout) {
completion.finish { continuation.resume(throwing: URLError(.timedOut)) }
}
}
}
static func runBoundedCallback(
deadline: Date?,
timeoutObserver: (@Sendable () -> Void)? = nil,
start: (@escaping @Sendable () -> Void) -> Void) async throws
{
let completion = CookieLoadCompletion()
guard let deadline else {
await withCheckedContinuation { continuation in
start {
completion.finish { continuation.resume() }
}
}
return
}
let timeout = try self.remainingTimeout(until: deadline)
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
start {
completion.finish { continuation.resume() }
}
self.deadlineQueue.asyncAfter(deadline: .now() + timeout) {
completion.finish {
timeoutObserver?()
continuation.resume(throwing: URLError(.timedOut))
}
}
}
}
static func runBoundedValueCallback<T: Sendable>(
deadline: Date?,
timeoutObserver: (@Sendable () -> Void)? = nil,
start: (@escaping @Sendable (T) -> Void) -> Void) async throws -> T
{
let completion = CookieLoadCompletion()
guard let deadline else {
return await withCheckedContinuation { continuation in
start { value in
completion.finish { continuation.resume(returning: value) }
}
}
}
let timeout = try self.remainingTimeout(until: deadline)
return try await withCheckedThrowingContinuation { continuation in
start { value in
completion.finish { continuation.resume(returning: value) }
}
self.deadlineQueue.asyncAfter(deadline: .now() + timeout) {
completion.finish {
timeoutObserver?()
continuation.resume(throwing: URLError(.timedOut))
}
}
}
}
static func runSerializedCallback(
key: ObjectIdentifier,
deadline: Date?,
start: @escaping (@escaping @Sendable () -> Void) -> Void) async throws
{
while let pending = self.pendingCookieStoreMutations[key] {
try await self.waitForMutation(pending.task, deadline: deadline)
if self.pendingCookieStoreMutations[key]?.token == pending.token {
self.pendingCookieStoreMutations[key] = nil
}
}
let token = UUID()
let task = Task { @MainActor in
await withCheckedContinuation { continuation in
start { continuation.resume() }
}
}
self.pendingCookieStoreMutations[key] = PendingCookieStoreMutation(token: token, task: task)
Task { @MainActor in
await task.value
if self.pendingCookieStoreMutations[key]?.token == token {
self.pendingCookieStoreMutations[key] = nil
}
}
try await self.waitForMutation(task, deadline: deadline)
}
private static func waitForMutation(_ task: Task<Void, Never>, deadline: Date?) async throws {
try await self.runBoundedCallback(deadline: deadline) { completion in
Task { @MainActor in
await task.value
completion()
}
}
}
}
#endif