field_ios

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

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 }