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 }