RadrootsBackgroundTransfer.swift (9904B)
1 import Foundation 2 3 public enum RadrootsBackgroundTransferError: Error, Equatable, Sendable { 4 case invalidRequest 5 case unavailable 6 case transferFailure 7 case persistenceFailure 8 /// Filesystem capacity is exhausted; existing effects still require reconciliation. 9 case spaceInsufficient 10 /// The bounded receipt envelope is full. This is distinct from free disk space. 11 case receiptCapacityExceeded 12 } 13 14 extension RadrootsBackgroundTransferError: LocalizedError { 15 public var errorDescription: String? { 16 switch self { 17 case .invalidRequest: "The background transfer request is invalid." 18 case .unavailable: "Background transfer is unavailable." 19 case .transferFailure: "The background transfer could not be completed." 20 case .persistenceFailure: "The background transfer state could not be saved." 21 case .spaceInsufficient: "There is not enough storage space to save the transfer state." 22 case .receiptCapacityExceeded: "The transfer receipt store has reached its capacity." 23 } 24 } 25 } 26 27 extension RadrootsBackgroundTransferError { 28 static func persistence(_ error: any Error) -> Self { 29 if let typed = error as? Self, typed == .spaceInsufficient || typed == .receiptCapacityExceeded { return typed } 30 return RadrootsAppleFileError.classified(error) == .spaceInsufficient ? .spaceInsufficient : .persistenceFailure 31 } 32 } 33 34 public struct RadrootsBackgroundTransferIdentifier: Sendable, Equatable, Hashable, Comparable, 35 Codable 36 { 37 public let rawValue: String 38 39 public init(_ value: String) throws { 40 rawValue = try RadrootsBackgroundTransferValidation.normalizedIdentifier(value) 41 } 42 43 public static func generated() -> Self { 44 Self(validatedRawValue: UUID().uuidString.lowercased()) 45 } 46 47 public static func < (lhs: Self, rhs: Self) -> Bool { 48 lhs.rawValue < rhs.rawValue 49 } 50 51 private init(validatedRawValue: String) { 52 rawValue = validatedRawValue 53 } 54 55 private enum CodingKeys: String, CodingKey { case rawValue } 56 57 public init(from decoder: any Decoder) throws { 58 let values = try decoder.container(keyedBy: CodingKeys.self) 59 try self.init(values.decode(String.self, forKey: .rawValue)) 60 } 61 62 public func encode(to encoder: any Encoder) throws { 63 var values = encoder.container(keyedBy: CodingKeys.self) 64 try values.encode(rawValue, forKey: .rawValue) 65 } 66 } 67 68 public enum RadrootsBackgroundTransferMethod: String, Sendable, Equatable, Hashable, Codable, 69 CaseIterable 70 { 71 case get = "GET" 72 case post = "POST" 73 case put = "PUT" 74 } 75 76 public enum RadrootsBackgroundTransferLocalFile: Sendable, Equatable, Hashable, Codable { 77 case file(RadrootsFileReference) 78 case stagedBlob(RadrootsStagedBlobReference) 79 } 80 81 public enum RadrootsBackgroundTransferOperation: Sendable, Equatable, Hashable, Codable { 82 case download(destination: RadrootsBackgroundTransferLocalFile) 83 case upload(source: RadrootsBackgroundTransferLocalFile) 84 } 85 86 public enum RadrootsBackgroundTransferState: String, Sendable, Equatable, Hashable, Codable, 87 CaseIterable 88 { 89 case queued 90 case running 91 case awaitingVerification 92 case completed 93 case failed 94 case cancelled 95 case expired 96 case interrupted 97 } 98 99 public enum RadrootsBackgroundTransferFailure: String, Sendable, Equatable, Hashable, Codable, 100 CaseIterable 101 { 102 case enqueueFailed = "background_transfer_enqueue_failed" 103 case expired = "background_transfer_expired" 104 case interrupted = "background_transfer_interrupted" 105 case verificationRejected = "background_transfer_verification_rejected" 106 case transferTooLarge = "background_transfer_transfer_too_large" 107 case responseTooLarge = "background_transfer_response_too_large" 108 case responseMediaType = "background_transfer_response_media_type" 109 case responseContentEncoding = "background_transfer_response_content_encoding" 110 case platformFailure = "background_transfer_platform_failure" 111 case responseMissing = "background_transfer_response_missing" 112 case httpStatus = "background_transfer_http_status" 113 case responseInvalid = "background_transfer_response_invalid" 114 case downloadStagingFailure = "background_transfer_download_staging_failure" 115 case destinationFailure = "background_transfer_destination_failure" 116 } 117 118 public enum RadrootsBackgroundTransferNetworkPolicy: String, Sendable, Equatable, Hashable, Codable { 119 case publicHTTPS 120 case simulatorLoopbackHTTP 121 } 122 123 public protocol RadrootsBackgroundTransferStore: Sendable { 124 /// Serializes admission across suspension and independent owners. This is a 125 /// nonblocking reservation, separate from short persistence transactions. 126 func withAdmission<Result: Sendable>( 127 for identifier: RadrootsBackgroundTransferIdentifier, 128 operation: @escaping @Sendable () async throws -> Result 129 ) async throws -> Result 130 func admissionIsActive(for identifier: RadrootsBackgroundTransferIdentifier) async throws -> Bool 131 /// Atomically installs a redacted snapshot only if the exact expected value 132 /// still owns the identifier. Nil reserves a previously absent identifier. 133 func compareExchangeSnapshot( 134 expected: RadrootsBackgroundTransferSnapshot?, desired: RadrootsBackgroundTransferSnapshot 135 ) async throws -> Bool 136 func loadSnapshots() async throws -> [RadrootsBackgroundTransferSnapshot] 137 func saveSnapshot(_ snapshot: RadrootsBackgroundTransferSnapshot) async throws 138 func removeSnapshot(for identifier: RadrootsBackgroundTransferIdentifier) async throws 139 func removeAllSnapshots() async throws 140 } 141 142 public protocol RadrootsBackgroundTransfer: Sendable { 143 /// Explicitly reconcile the OS and retain exclusive admission for this prior 144 /// identifier during bounded caller work. Only absent or inactive failed 145 /// execution without a retained response is admitted. Unknown state throws. 146 /// This does not establish absence of remote effects or grant retry/signing 147 /// authority. Preserve prior lineage and use a new identifier for renewal; 148 /// enqueue/retry of the reserved identifier is rejected until the body exits. 149 func withInactiveExecution<Result: Sendable>( 150 for identifier: RadrootsBackgroundTransferIdentifier, 151 operation: @escaping @Sendable (RadrootsBackgroundTransferSnapshot?) async throws -> Result 152 ) async throws -> Result 153 func enqueue(_ request: RadrootsBackgroundTransferRequest) async throws 154 -> RadrootsBackgroundTransferHandle 155 func retry(_ request: RadrootsBackgroundTransferRequest) async throws 156 -> RadrootsBackgroundTransferHandle 157 func cancel(_ identifier: RadrootsBackgroundTransferIdentifier) async throws 158 func expire(_ identifier: RadrootsBackgroundTransferIdentifier) async throws 159 func settle( 160 _ identifier: RadrootsBackgroundTransferIdentifier, 161 verification: RadrootsBackgroundTransferVerification 162 ) async throws 163 func snapshot(for identifier: RadrootsBackgroundTransferIdentifier) async throws 164 -> RadrootsBackgroundTransferSnapshot? 165 func snapshots() async throws -> [RadrootsBackgroundTransferSnapshot] 166 func handleEventsForBackgroundURLSession( 167 identifier: String, completionHandler: @escaping @Sendable () -> Void 168 ) async 169 } 170 171 public extension RadrootsBackgroundTransfer { 172 func withInactiveExecution<Result: Sendable>( 173 for _: RadrootsBackgroundTransferIdentifier, 174 operation _: @escaping @Sendable (RadrootsBackgroundTransferSnapshot?) async throws -> Result 175 ) async throws -> Result { 176 throw RadrootsBackgroundTransferError.unavailable 177 } 178 } 179 180 public protocol RadrootsBackgroundTransferFileResolver: Sendable { 181 func resolve(_ file: RadrootsBackgroundTransferLocalFile) throws -> URL 182 func read(_ file: RadrootsBackgroundTransferLocalFile, maximumBytes: Int) throws -> Data 183 func prepareUploadLease( 184 for request: RadrootsBackgroundTransferRequest, executionID: UUID, 185 existing: RadrootsStagedBlobReference? 186 ) throws -> RadrootsStagedBlobLease 187 func releaseUploadLease(executionID: UUID) throws 188 } 189 190 public struct RadrootsUnavailableBackgroundTransfer: RadrootsBackgroundTransfer, Sendable { 191 public init() {} 192 193 public func enqueue(_: RadrootsBackgroundTransferRequest) async throws 194 -> RadrootsBackgroundTransferHandle 195 { 196 throw RadrootsBackgroundTransferError.unavailable 197 } 198 199 public func retry(_: RadrootsBackgroundTransferRequest) async throws 200 -> RadrootsBackgroundTransferHandle 201 { 202 throw RadrootsBackgroundTransferError.unavailable 203 } 204 205 public func cancel(_: RadrootsBackgroundTransferIdentifier) async throws { 206 throw RadrootsBackgroundTransferError.unavailable 207 } 208 209 public func expire(_: RadrootsBackgroundTransferIdentifier) async throws { 210 throw RadrootsBackgroundTransferError.unavailable 211 } 212 213 public func settle( 214 _: RadrootsBackgroundTransferIdentifier, 215 verification _: RadrootsBackgroundTransferVerification 216 ) async throws { 217 throw RadrootsBackgroundTransferError.unavailable 218 } 219 220 public func snapshot(for _: RadrootsBackgroundTransferIdentifier) async throws 221 -> RadrootsBackgroundTransferSnapshot? 222 { 223 throw RadrootsBackgroundTransferError.unavailable 224 } 225 226 public func snapshots() async throws -> [RadrootsBackgroundTransferSnapshot] { 227 throw RadrootsBackgroundTransferError.unavailable 228 } 229 230 public func handleEventsForBackgroundURLSession( 231 identifier _: String, completionHandler: @escaping @Sendable () -> Void 232 ) async { 233 completionHandler() 234 } 235 } 236 237 extension RadrootsBackgroundTransferOperation { 238 var redactedLabel: String { 239 switch self { 240 case .download: "download" 241 case .upload: "upload" 242 } 243 } 244 }