field_ios

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

TeraUploadRenewal.swift (5958B)


      1 import Foundation
      2 import RadrootsKit
      3 
      4 /// Explicit foreground user action only. Rust owns budget, expiry and signing.
      5 struct TeraUploadRenewal: Sendable {
      6   let transfer: any RadrootsBackgroundTransfer
      7   let preparer: RadrootsAppleMediaPreparer
      8   struct Prior: Sendable {
      9     let identifier: RadrootsBackgroundTransferIdentifier
     10     let attempt: String
     11     let revision: UInt64
     12   }
     13 
     14   func run(_ submission: TeraSubmissionStatus, media: TeraPreparedMedia, handle: TeraPreparedMediaHandle,
     15            backend: any TeraRuntimeBackend) async throws -> TeraSubmissionStatus
     16   {
     17     try Task.checkCancellation()
     18     guard !submission.delivery.isStopped,
     19           let item = submission.media.first(where: { $0.opaqueReference == media.opaqueReference }),
     20           !item.authorizations.isEmpty, item.authorizations.count <= 5,
     21           let uploadURL = item.progress.uploadURL else { throw TeraNativeUploadExecution.unknown }
     22     let inventory = try await transfer.snapshots()
     23     let lineage = try Self.identities(item.authorizations, parent: submission.intentID, inventory: inventory)
     24     let known = Set(lineage.map(\.identifier))
     25     for snapshot in inventory where snapshot.identifier.rawValue.hasPrefix("radroots.add.\(submission.intentID).")
     26       && snapshot.request.expectedSourceSHA256 == media.sha256
     27     {
     28       guard known.contains(snapshot.identifier),
     29             try TeraBackgroundUploadRequest.persistedRequestMatchesMedia(snapshot.request, media: media, uploadURL: uploadURL)
     30       else { throw TeraNativeUploadExecution.unknown }
     31     }
     32     guard let latest = lineage.last else { throw TeraNativeUploadExecution.unknown }
     33     return try await Self.holding(lineage, transfer: transfer) { snapshots in
     34       try Task.checkCancellation()
     35       for snapshot in snapshots {
     36         guard try TeraBackgroundUploadRequest.persistedRequestMatchesMedia(snapshot.request, media: media, uploadURL: uploadURL)
     37         else { throw TeraNativeUploadExecution.unknown }
     38       }
     39       let retry = TeraSubmissionUploadRenewal(priorRevision: latest.revision, priorAttempt: latest.attempt,
     40                                               nativeFailed: snapshots.contains { $0.identifier == latest.identifier && [.failed, .expired].contains($0.state) })
     41       let input = TeraSubmissionMediaRequest(request: submission.request, expectedRevision: submission.revision, media: handle)
     42       let policy: RadrootsBackgroundTransferNetworkPolicy = uploadURL.hasPrefix("https:") ? .publicHTTPS : .simulatorLoopbackHTTP
     43       if !RadrootsAppleBackgroundTransferAdapters.supportsNewEnqueue(for: policy) {
     44         return try await backend.renewSubmissionMedia(input: input, renewal: retry)
     45       }
     46       let job = try await backend.renewSubmissionUpload(input: input, renewal: retry)
     47       try Task.checkCancellation()
     48       let current = try await backend.submissionStatus(request: submission.request)
     49       guard !current.delivery.isStopped, current.revision == job.submission.revision else { throw TeraNativeUploadExecution.unknown }
     50       let request = try await TeraBackgroundUploadRequest.prepare(job: job.transfer, media: media, preparer: preparer)
     51       guard !known.contains(request.identifier) else { throw TeraNativeUploadExecution.unknown }
     52       let active = try await TeraNativeUploadExecution.start(transfer: transfer, request: request, retrying: false)
     53       let receipt = try await TeraBackgroundUploadWaiter.receipt(transfer: transfer, for: active.identifier,
     54                                                                  draftID: submission.intentID, expectedRevision: job.submission.revision, request: request, baseline: active)
     55       let status = try await backend.completeSubmissionUpload(input: .init(request: submission.request,
     56                                                                            expectedRevision: receipt.expectedRevision, media: handle), response: receipt)
     57       try await transfer.settle(active.identifier, verification: .accepted)
     58       return status
     59     }
     60   }
     61 
     62   static func identities(_ attempts: [TeraUploadAttemptIdentity], parent: String,
     63                          inventory: [RadrootsBackgroundTransferSnapshot]) throws -> [Prior]
     64   {
     65     guard !attempts.isEmpty, attempts.count <= 5 else { throw TeraNativeUploadExecution.unknown }
     66     return try attempts.map { attempt in
     67       let matches = inventory.compactMap { snapshot -> Prior? in
     68         guard let identity = TeraBackgroundUploadRequest.transferIdentity(snapshot.identifier),
     69               identity.draftID == parent, identity.attempt == attempt.operationID else { return nil }
     70         return Prior(identifier: snapshot.identifier, attempt: identity.attempt, revision: identity.revision)
     71       }
     72       guard matches.count <= 1 else { throw TeraNativeUploadExecution.unknown }
     73       guard let revision = attempt.revision ?? matches.first?.revision, revision > 0,
     74             matches.first.map({ $0.revision == revision }) ?? true else { throw TeraNativeUploadExecution.unknown }
     75       return try Prior(identifier: RadrootsBackgroundTransferIdentifier("radroots.add.\(parent).\(revision).\(attempt.operationID)"),
     76                        attempt: attempt.operationID, revision: revision)
     77     }
     78   }
     79 
     80   static func holding<Result: Sendable>(_ lineage: [Prior], transfer: any RadrootsBackgroundTransfer,
     81                                         snapshots: [RadrootsBackgroundTransferSnapshot] = [],
     82                                         operation: @escaping @Sendable ([RadrootsBackgroundTransferSnapshot]) async throws -> Result) async throws -> Result
     83   {
     84     guard let first = lineage.first else { return try await operation(snapshots) }
     85     return try await transfer.withInactiveExecution(for: first.identifier) { snapshot in
     86       try Task.checkCancellation()
     87       return try await holding(Array(lineage.dropFirst()), transfer: transfer,
     88                                snapshots: snapshots + (snapshot.map { [$0] } ?? []), operation: operation)
     89     }
     90   }
     91 }