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

RadrootsBackgroundTasks.swift (6932B)


      1 import Foundation
      2 
      3 public enum RadrootsBackgroundTaskKind: String, Sendable, Equatable, Hashable, CaseIterable {
      4     case appRefresh
      5     case processing
      6 }
      7 
      8 public enum RadrootsBackgroundTaskError: Error, Equatable, Sendable {
      9     case invalidRequest
     10     case unavailable
     11     case schedulerFailure
     12 }
     13 
     14 extension RadrootsBackgroundTaskError: LocalizedError {
     15     public var errorDescription: String? {
     16         switch self {
     17         case .invalidRequest: "The background task request is invalid."
     18         case .unavailable: "Background tasks are unavailable."
     19         case .schedulerFailure: "The background task could not be scheduled."
     20         }
     21     }
     22 }
     23 
     24 public struct RadrootsBackgroundTaskIdentifier: Sendable, Equatable, Hashable, Comparable {
     25     public let rawValue: String
     26 
     27     public init(_ value: String) throws {
     28         rawValue = try RadrootsBackgroundTaskValidation.normalizedIdentifier(value)
     29     }
     30 
     31     public static func < (lhs: Self, rhs: Self) -> Bool {
     32         lhs.rawValue < rhs.rawValue
     33     }
     34 }
     35 
     36 public struct RadrootsBackgroundTaskRequest: Sendable, Equatable, Hashable {
     37     public let identifier: RadrootsBackgroundTaskIdentifier
     38     public let kind: RadrootsBackgroundTaskKind
     39     public let earliestBeginDate: Date?
     40     public let requiresNetworkConnectivity: Bool
     41     public let requiresExternalPower: Bool
     42 
     43     public init(
     44         identifier: RadrootsBackgroundTaskIdentifier,
     45         kind: RadrootsBackgroundTaskKind,
     46         earliestBeginDate: Date? = nil,
     47         requiresNetworkConnectivity: Bool = false,
     48         requiresExternalPower: Bool = false
     49     ) throws {
     50         try RadrootsBackgroundTaskValidation.validate(
     51             kind: kind,
     52             earliestBeginDate: earliestBeginDate,
     53             requiresNetworkConnectivity: requiresNetworkConnectivity,
     54             requiresExternalPower: requiresExternalPower
     55         )
     56         self.identifier = identifier
     57         self.kind = kind
     58         self.earliestBeginDate = earliestBeginDate
     59         self.requiresNetworkConnectivity = requiresNetworkConnectivity
     60         self.requiresExternalPower = requiresExternalPower
     61     }
     62 
     63     public init(
     64         identifier: String,
     65         kind: RadrootsBackgroundTaskKind,
     66         earliestBeginDate: Date? = nil,
     67         requiresNetworkConnectivity: Bool = false,
     68         requiresExternalPower: Bool = false
     69     ) throws {
     70         try self.init(
     71             identifier: RadrootsBackgroundTaskIdentifier(identifier),
     72             kind: kind,
     73             earliestBeginDate: earliestBeginDate,
     74             requiresNetworkConnectivity: requiresNetworkConnectivity,
     75             requiresExternalPower: requiresExternalPower
     76         )
     77     }
     78 }
     79 
     80 public struct RadrootsBackgroundTaskSnapshot: Sendable, Equatable, Hashable {
     81     public let identifier: RadrootsBackgroundTaskIdentifier
     82     public let kind: RadrootsBackgroundTaskKind
     83     public let earliestBeginDate: Date?
     84     public let submittedAt: Date
     85     public let requiresNetworkConnectivity: Bool
     86     public let requiresExternalPower: Bool
     87 
     88     public init(
     89         identifier: RadrootsBackgroundTaskIdentifier,
     90         kind: RadrootsBackgroundTaskKind,
     91         earliestBeginDate: Date? = nil,
     92         submittedAt: Date = Date(),
     93         requiresNetworkConnectivity: Bool = false,
     94         requiresExternalPower: Bool = false
     95     ) throws {
     96         try RadrootsBackgroundTaskValidation.validate(
     97             kind: kind,
     98             earliestBeginDate: earliestBeginDate,
     99             requiresNetworkConnectivity: requiresNetworkConnectivity,
    100             requiresExternalPower: requiresExternalPower
    101         )
    102         guard submittedAt.timeIntervalSinceReferenceDate.isFinite else {
    103             throw RadrootsBackgroundTaskError.invalidRequest
    104         }
    105         self.identifier = identifier
    106         self.kind = kind
    107         self.earliestBeginDate = earliestBeginDate
    108         self.submittedAt = submittedAt
    109         self.requiresNetworkConnectivity = requiresNetworkConnectivity
    110         self.requiresExternalPower = requiresExternalPower
    111     }
    112 
    113     public init(request: RadrootsBackgroundTaskRequest, submittedAt: Date = Date()) throws {
    114         try self.init(
    115             identifier: request.identifier,
    116             kind: request.kind,
    117             earliestBeginDate: request.earliestBeginDate,
    118             submittedAt: submittedAt,
    119             requiresNetworkConnectivity: request.requiresNetworkConnectivity,
    120             requiresExternalPower: request.requiresExternalPower
    121         )
    122     }
    123 }
    124 
    125 public protocol RadrootsBackgroundTaskScheduler: Sendable {
    126     func submit(_ request: RadrootsBackgroundTaskRequest) async throws
    127         -> RadrootsBackgroundTaskSnapshot
    128     func cancel(_ identifier: RadrootsBackgroundTaskIdentifier) async throws
    129     func cancelAll() async throws
    130     func pendingTasks() async throws -> [RadrootsBackgroundTaskSnapshot]
    131 }
    132 
    133 public struct RadrootsUnavailableBackgroundTaskScheduler: RadrootsBackgroundTaskScheduler, Sendable {
    134     public init() {}
    135 
    136     public func submit(_: RadrootsBackgroundTaskRequest) async throws
    137         -> RadrootsBackgroundTaskSnapshot
    138     {
    139         throw RadrootsBackgroundTaskError.unavailable
    140     }
    141 
    142     public func cancel(_: RadrootsBackgroundTaskIdentifier) async throws {
    143         throw RadrootsBackgroundTaskError.unavailable
    144     }
    145 
    146     public func cancelAll() async throws {
    147         throw RadrootsBackgroundTaskError.unavailable
    148     }
    149 
    150     public func pendingTasks() async throws -> [RadrootsBackgroundTaskSnapshot] {
    151         throw RadrootsBackgroundTaskError.unavailable
    152     }
    153 }
    154 
    155 public enum RadrootsBackgroundTaskValidation {
    156     public static func normalizedIdentifier(_ value: String) throws -> String {
    157         let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
    158         guard !trimmed.isEmpty else {
    159             throw RadrootsBackgroundTaskError.invalidRequest
    160         }
    161         guard trimmed.count <= 255 else {
    162             throw RadrootsBackgroundTaskError.invalidRequest
    163         }
    164         guard
    165             trimmed.range(
    166             of: "^[a-z0-9][a-z0-9._-]*[a-z0-9]$|^[a-z0-9]$",
    167             options: .regularExpression
    168             ) != nil
    169         else {
    170             throw RadrootsBackgroundTaskError.invalidRequest
    171         }
    172         guard !trimmed.contains("..") else {
    173             throw RadrootsBackgroundTaskError.invalidRequest
    174         }
    175         return trimmed
    176     }
    177 
    178     public static func validate(
    179         kind: RadrootsBackgroundTaskKind,
    180         earliestBeginDate: Date?,
    181         requiresNetworkConnectivity: Bool,
    182         requiresExternalPower: Bool
    183     ) throws {
    184         if let earliestBeginDate {
    185             guard earliestBeginDate.timeIntervalSinceReferenceDate.isFinite else {
    186                 throw RadrootsBackgroundTaskError.invalidRequest
    187             }
    188         }
    189         guard kind == .processing || (!requiresNetworkConnectivity && !requiresExternalPower) else {
    190             throw RadrootsBackgroundTaskError.invalidRequest
    191         }
    192     }
    193 }