apple_kit

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

RadrootsBackgroundTasks.swift (7674B)


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