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 }