191 lines
7.2 KiB
Swift
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
|