field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

TeraNativeRecoveryStatus.swift (5137B)


      1 import CryptoKit
      2 import Foundation
      3 import RadrootsKit
      4 
      5 enum TeraNativeRecoveryReason: Sendable, Equatable {
      6   case missingParent, invalidParent, associationMismatch, outcomeUnconfirmed, resolved
      7 }
      8 
      9 enum TeraNativeRecoveryPause: Sendable, Equatable {
     10   case protectedData, storageUnavailable, quota, receiptQuota, credentials, runtimeUnavailable
     11 
     12   var message: String {
     13     let reason = switch self {
     14     case .protectedData: "Photo recovery is paused until this device is unlocked."
     15     case .storageUnavailable: "Photo recovery is paused until local storage is available."
     16     case .quota: "Photo recovery is paused because local storage is full. Free space, then check again."
     17     case .receiptQuota: "Photo recovery is paused because transfer receipt storage reached its limit. Preserve the original transfer; clearing photo cache does not resolve this limit."
     18     case .credentials: "Photo recovery is paused until this account’s credentials are available."
     19     case .runtimeUnavailable: "Photo recovery is paused until this account’s runtime is available."
     20     }
     21     return "\(reason) Keep your existing work; new saves may be unavailable."
     22   }
     23 }
     24 
     25 enum TeraNativeRecoveryFault: Error {
     26   case missingParent, invalidParent, associationMismatch
     27 }
     28 
     29 struct TeraNativeRecoveryStatus: Sendable, Equatable {
     30   let key: String
     31   let reason: TeraNativeRecoveryReason
     32   let revision: UInt64
     33   let firstObservedUnixMS: UInt64
     34   let updatedAtUnixMS: UInt64
     35 }
     36 
     37 struct TeraNativeRecoveryIssue: Sendable, Equatable, Identifiable {
     38   let key: String
     39   let reason: TeraNativeRecoveryReason
     40   let status: TeraNativeRecoveryStatus?
     41   var id: String {
     42     key
     43   }
     44 
     45   static func key(_ identifier: String) -> String {
     46     SHA256.hash(data: Data(identifier.utf8)).map { String(format: "%02x", $0) }.joined()
     47   }
     48 }
     49 
     50 enum TeraNativeRecoveryClassification {
     51   static func pause(_ error: Error) -> TeraNativeRecoveryPause? {
     52     if let client = error as? TeraRuntimeClientError, case .notRunning = client {
     53       return .runtimeUnavailable
     54     }
     55     if let failure = TeraRuntimeFailure.from(error) {
     56       switch failure.recovery.disposition {
     57       case .protectedDataUnavailable: return .protectedData
     58       case .storageFailure: return .storageUnavailable
     59       case .quotaExhausted: return .quota
     60       case .identityUnavailable: return .credentials
     61       case .runtimeUnavailable: return .runtimeUnavailable
     62       default: break
     63       }
     64     }
     65     return nativePause(error)
     66   }
     67 
     68   private static func nativePause(_ error: Error) -> TeraNativeRecoveryPause? {
     69     if let native = error as? RadrootsBackgroundTransferError {
     70       switch native {
     71       case .spaceInsufficient: return .quota
     72       case .receiptCapacityExceeded: return .receiptQuota
     73       case .persistenceFailure, .unavailable: return .storageUnavailable
     74       default: break
     75       }
     76     }
     77     if let file = error as? RadrootsAppleFileError, file == .spaceInsufficient {
     78       return .quota
     79     }
     80     if let capture = error as? RadrootsCaptureIntakeError, capture == .spaceInsufficient {
     81       return .quota
     82     }
     83     return nil
     84   }
     85 
     86   static func reason(_ error: Error) -> TeraNativeRecoveryReason {
     87     switch error {
     88     case TeraNativeRecoveryFault.missingParent: .missingParent
     89     case TeraNativeRecoveryFault.invalidParent: .invalidParent
     90     case TeraNativeRecoveryFault.associationMismatch: .associationMismatch
     91     default: .outcomeUnconfirmed
     92     }
     93   }
     94 
     95   static func report(_ identifier: String, reason: TeraNativeRecoveryReason,
     96                      client: TeraRuntimeClient) async -> TeraNativeRecoveryIssue?
     97   {
     98     let key = TeraNativeRecoveryIssue.key(identifier)
     99     do {
    100       let status = try await client.reportNativeRecoveryStatus(key: key, reason: reason)
    101       return .init(key: key, reason: reason, status: status)
    102     } catch {
    103       guard reason == .resolved else { return .init(key: key, reason: reason, status: nil) }
    104       // A new store may have no transient notice. Read back a retained advisory
    105       // after a failed resolution write; only durable resolved state clears it.
    106       do {
    107         return try await client.nativeRecoveryStatus(key: key).map { .init(key: key, reason: $0.reason, status: $0) }
    108       } catch {
    109         return .init(key: key, reason: .outcomeUnconfirmed, status: nil)
    110       }
    111     }
    112   }
    113 }
    114 
    115 extension TeraRuntimeBackend {
    116   func nativeRecoveryStatus(key _: String) async throws -> TeraNativeRecoveryStatus? {
    117     throw TeraComposerAcknowledgment.unconfirmed
    118   }
    119 
    120   func reportNativeRecoveryStatus(key _: String, reason _: TeraNativeRecoveryReason) async throws -> TeraNativeRecoveryStatus? {
    121     throw TeraComposerAcknowledgment.unconfirmed
    122   }
    123 }
    124 
    125 extension TeraRuntimeClient {
    126   func nativeRecoveryStatus(key: String) async throws -> TeraNativeRecoveryStatus? {
    127     try await addOperation("runtime.recovery.status") { try await $0.nativeRecoveryStatus(key: key) }
    128   }
    129 
    130   func reportNativeRecoveryStatus(key: String, reason: TeraNativeRecoveryReason) async throws -> TeraNativeRecoveryStatus? {
    131     try await addOperation("runtime.recovery.report") { try await $0.reportNativeRecoveryStatus(key: key, reason: reason) }
    132   }
    133 }