TeraNativeRecoveryInventory.swift (4626B)
1 import Foundation 2 import RadrootsKit 3 4 struct TeraNativeRecoveryInspection: Sendable { 5 var selectedKey: String? 6 var completedNeedsRepair: @Sendable (String) async throws -> Bool = { _ in false } 7 } 8 9 /// One explicit bounded pass over authoritative native receipts. The owner 10 /// persists a redacted ordering key after each visited position. Interrupted 11 /// work is replayed idempotently; a new sweep revisits earlier quarantined IDs. 12 enum TeraNativeRecoveryInventory { 13 static let passLimit = 64 14 15 static func run( 16 transfer: any RadrootsBackgroundTransfer, cursor: String?, 17 inspection: TeraNativeRecoveryInspection = .init(), 18 checkpoint: @Sendable (String?) async throws -> Void = { _ in }, 19 complete: @Sendable (RadrootsBackgroundTransferSnapshot, TeraNativeUploadRecoveryOwner) async throws -> Void = { _, _ in throw TeraComposerAcknowledgment.unconfirmed }, 20 report: @Sendable (RadrootsBackgroundTransferSnapshot, TeraNativeRecoveryReason) async -> TeraNativeRecoveryIssue? = { snapshot, reason in 21 reason == .resolved ? nil : .init(key: TeraNativeRecoveryIssue.key(snapshot.identifier.rawValue), reason: reason, status: nil) 22 }, 23 lookup: @Sendable (String) async throws -> TeraNativeUploadRecoveryOwner? 24 ) async throws -> (progress: TeraNativeRecoveryProgress, cursor: String?) { 25 let snapshots = try await transfer.snapshots() 26 .filter { $0.state == .awaitingVerification || $0.state == .completed } 27 .map { (key: TeraNativeRecoveryIssue.key($0.identifier.rawValue), snapshot: $0) } 28 .sorted { $0.key < $1.key } 29 let pending = snapshots.filter { 30 if let selected = inspection.selectedKey { 31 return $0.key == selected 32 } 33 return cursor == nil || $0.key > (cursor ?? "") 34 } 35 guard inspection.selectedKey == nil || pending.count == 1 else { throw TeraComposerAcknowledgment.unconfirmed } 36 var attention = false 37 var issues: [TeraNativeRecoveryIssue] = [] 38 var pause: TeraNativeRecoveryPause? 39 var visited = 0 40 var last = cursor 41 recovery: for position in pending.prefix(passLimit) { 42 let snapshot = position.snapshot 43 try Task.checkCancellation() 44 switch try await recover(snapshot, inspection: inspection, complete: complete, lookup: lookup) { 45 case .complete: 46 if let resolved = await report(snapshot, .resolved) { 47 issues.append(resolved) 48 attention = attention || resolved.reason != .resolved 49 } 50 case .ignored: break 51 case let .issue(reason): 52 attention = true 53 if let issue = await report(snapshot, reason) { 54 issues.append(issue) 55 } 56 case let .pause(value): 57 pause = value 58 break recovery // Revisit this same item after unlock/storage recovery. 59 } 60 try await checkpoint(position.key) 61 try Task.checkCancellation() 62 visited += 1 63 last = position.key 64 } 65 let remaining = pending.count - visited 66 if remaining == 0 { 67 try await checkpoint(nil) 68 } 69 return (.init(visited: visited, remaining: remaining, needsAttention: attention, issues: issues, pause: pause), remaining > 0 ? last : nil) 70 } 71 72 private enum Outcome { case complete, ignored, issue(TeraNativeRecoveryReason), pause(TeraNativeRecoveryPause) } 73 74 private static func recover(_ snapshot: RadrootsBackgroundTransferSnapshot, 75 inspection: TeraNativeRecoveryInspection, 76 complete: @Sendable (RadrootsBackgroundTransferSnapshot, TeraNativeUploadRecoveryOwner) async throws -> Void, 77 lookup: @Sendable (String) async throws -> TeraNativeUploadRecoveryOwner?) async throws -> Outcome 78 { 79 do { 80 if snapshot.state == .completed, inspection.selectedKey == nil, 81 try await !inspection.completedNeedsRepair(TeraNativeRecoveryIssue.key(snapshot.identifier.rawValue)) { 82 return .ignored 83 } 84 guard let identity = TeraBackgroundUploadRequest.transferIdentity(snapshot.identifier) else { throw TeraNativeRecoveryFault.associationMismatch } 85 guard let owner = try await lookup(identity.draftID) else { throw TeraNativeRecoveryFault.missingParent } 86 guard owner.id == identity.draftID else { throw TeraNativeRecoveryFault.associationMismatch } 87 try Task.checkCancellation() 88 try await complete(snapshot, owner) 89 return .complete 90 } catch { 91 try Task.checkCancellation() 92 if let pause = TeraNativeRecoveryClassification.pause(error) { 93 return .pause(pause) 94 } 95 return .issue(TeraNativeRecoveryClassification.reason(error)) 96 } 97 } 98 }