63 lines
2.9 KiB
Swift
63 lines
2.9 KiB
Swift
import Foundation
|
|
|
|
// At most two authenticated downloads per room. Cache only verified bytes, bounded to 16 MiB,
|
|
// and replace the entire actor when API configuration changes. No credential-bearing disk cache.
|
|
actor AgentAttachmentDownloads {
|
|
private var active = 0
|
|
private var invalidated = false
|
|
private var transfers: [UUID: Task<Data, any Error>] = [:]
|
|
private var waiters: [(UUID, CheckedContinuation<Void, any Error>)] = []
|
|
private var cache: [AgentAttachment: Data] = [:]
|
|
private var order: [AgentAttachment] = []
|
|
private var byteCount = 0
|
|
|
|
func data(for attachment: AgentAttachment, client: any AgentAPIClienting) async throws -> Data {
|
|
try Task.checkCancellation()
|
|
guard !invalidated else { throw CancellationError() }
|
|
if let value = cache[attachment] { return value }
|
|
try await acquire()
|
|
defer { release() }
|
|
try Task.checkCancellation()
|
|
if let value = cache[attachment] { return value }
|
|
guard !invalidated else { throw CancellationError() }
|
|
let id = UUID()
|
|
let transfer = Task { try await client.agentDownload(attachment) }
|
|
transfers[id] = transfer
|
|
defer { transfers[id] = nil }
|
|
let value = try await withTaskCancellationHandler { try await transfer.value } onCancel: { transfer.cancel() }
|
|
try Task.checkCancellation()
|
|
guard !invalidated else { throw CancellationError() }
|
|
while byteCount + value.count > 16 * 1024 * 1024, !order.isEmpty {
|
|
let oldest = order.removeFirst()
|
|
byteCount -= cache.removeValue(forKey: oldest)?.count ?? 0
|
|
}
|
|
if cache[attachment] == nil { cache[attachment] = value; order.append(attachment); byteCount += value.count }
|
|
return value
|
|
}
|
|
func invalidate() {
|
|
invalidated = true
|
|
transfers.values.forEach { $0.cancel() }
|
|
for waiter in waiters { waiter.1.resume(throwing: CancellationError()) }
|
|
waiters = []; cache = [:]; order = []; byteCount = 0
|
|
}
|
|
private func acquire() async throws {
|
|
guard !invalidated else { throw CancellationError() }
|
|
if active < 2 { active += 1; return }
|
|
let id = UUID()
|
|
try await withTaskCancellationHandler {
|
|
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
|
|
if Task.isCancelled { continuation.resume(throwing: CancellationError()) }
|
|
else { waiters.append((id, continuation)) }
|
|
}
|
|
} onCancel: { Task { await self.cancel(id) } }
|
|
}
|
|
private func cancel(_ id: UUID) {
|
|
guard let index = waiters.firstIndex(where: { $0.0 == id }) else { return }
|
|
waiters.remove(at: index).1.resume(throwing: CancellationError())
|
|
}
|
|
private func release() {
|
|
if waiters.isEmpty { active -= 1 }
|
|
else { waiters.removeFirst().1.resume() }
|
|
}
|
|
}
|