apple_kit

Apple-native services for Radroots iOS and macOS apps
git clone https://radroots.dev/git/apple_kit.git
Log | Files | Refs | README | LICENSE

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 }