field_ios

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

TeraSubmissionEffects.swift (5304B)


      1 import Foundation
      2 
      3 /// Bounded native scheduling over one immutable Rust-owned operation.
      4 @MainActor
      5 struct TeraSubmissionEffects {
      6   let client: TeraRuntimeClient
      7   let media: (any TeraAddMediaHandling)?
      8   let ensure: () throws -> Void
      9   let accept: (TeraSubmissionStatus) throws -> Void
     10   var mayStart: () -> Bool = { true }
     11   var stopControl: TeraSubmissionStopControl?
     12 
     13   func advance(_ initial: TeraSubmissionStatus) async throws {
     14     try ensure()
     15     if initial.delivery.isStopped {
     16       try await reconcileStopped(initial)
     17       return
     18     }
     19     guard mayStart() else { return }
     20     if !initial.retry.mayStart || [.complete, .terminal, .cancelled].contains(initial.state) {
     21       try await media?.reconcileBackgroundSubmissions([initial], client: client)
     22       try ensure()
     23       return
     24     }
     25     var current = initial
     26     if current.media.contains(where: { $0.progress.stage != .verified }) {
     27       current = try await upload(current)
     28     }
     29     try ensure()
     30     if mayStart(), current.retry.mayStart, !current.delivery.isStopped, ![.complete, .terminal, .cancelled].contains(current.state) {
     31       current = try await client.advanceSubmission(request: current.request, expectedRevision: current.revision)
     32       try accept(current)
     33     }
     34     try await media?.reconcileBackgroundSubmissions([current], client: client)
     35     try ensure()
     36   }
     37 
     38   private func upload(_ initial: TeraSubmissionStatus) async throws -> TeraSubmissionStatus {
     39     guard let media else {
     40       throw TeraRuntimeFailure.local(operation: "submission.media", code: "ios.add.media_unavailable",
     41                                      safeMessage: "Prepared photos are unavailable on this device.")
     42     }
     43     var current = initial
     44     let prepared = initial.preparedMedia
     45     let opened = try await TeraOpenedMedia.open(prepared, using: media)
     46     defer { opened.close() }
     47     try ensure()
     48     for item in initial.media where item.progress.stage != .verified {
     49       guard mayStart(), !current.delivery.isStopped else { return current }
     50       guard let source = prepared.first(where: { $0.opaqueReference == item.opaqueReference }),
     51             let handle = opened.handles.first(where: { $0.media.opaqueReference == item.opaqueReference })
     52       else {
     53         throw TeraComposerAcknowledgment.unconfirmed
     54       }
     55       if !item.authorizations.isEmpty {
     56         current = try await renew(current, source: source, using: media)
     57         continue
     58       }
     59       if try await media.prefersSharedForegroundUpload(ownerID: current.intentID) {
     60         let input = TeraSubmissionMediaRequest(request: current.request, expectedRevision: current.revision, media: handle)
     61         // View cancellation does not cancel the effect. The client retains
     62         // admission and Rust file ownership through any late deadline callback.
     63         current = try await Task { try await client.uploadSubmissionMedia(input: input) }.value
     64         try accept(current)
     65         try ensure()
     66         continue
     67       }
     68       let job = try await client.prepareSubmissionUpload(input: TeraSubmissionMediaRequest(
     69         request: current.request, expectedRevision: current.revision, media: handle
     70       ))
     71       try accept(job.submission)
     72       try ensure()
     73       guard mayStart(), !job.submission.delivery.isStopped else { return job.submission }
     74       let receipt = try await upload(job.transfer, source: source, using: media)
     75       try ensure()
     76       // Keep an uncertain OS receipt until Rust has durably verified it. A
     77       // storage/read/cancellation failure is never proof the upload was rejected.
     78       current = try await client.completeSubmissionUpload(input: TeraSubmissionMediaRequest(
     79         request: current.request, expectedRevision: job.submission.revision, media: handle
     80       ), response: receipt)
     81       try accept(current)
     82       try await media.settleBackgroundUpload(identifier: receipt.identifier, accepted: true)
     83       _ = await TeraNativeRecoveryClassification.report(receipt.identifier, reason: .resolved, client: client)
     84       try ensure()
     85     }
     86     return current
     87   }
     88 
     89   private func upload(_ transfer: TeraNativeTransferJob, source: TeraPreparedMedia, using media: any TeraAddMediaHandling) async throws -> TeraAddBackgroundUploadReceipt {
     90     if let stopControl {
     91       return try await stopControl.upload(using: media, transfer: transfer, source: source)
     92     }
     93     return try await media.uploadInBackground(transfer: transfer, media: source)
     94   }
     95 
     96   private func renew(_ current: TeraSubmissionStatus, source: TeraPreparedMedia, using media: any TeraAddMediaHandling) async throws -> TeraSubmissionStatus {
     97     let renewed = if let stopControl {
     98       try await stopControl.renew(using: media, submission: current, source: source, client: client)
     99     } else {
    100       try await media.renewSubmissionUpload(current, media: source, client: client)
    101     }
    102     try accept(renewed)
    103     try ensure()
    104     return renewed
    105   }
    106 
    107   func reconcileStopped(_ initial: TeraSubmissionStatus) async throws {
    108     guard let media else { return }
    109     // The authoritative inventory carries each original revision and attempt
    110     // through idempotent completion. Never relabel it as the current head.
    111     _ = try await media.recoverNativeUploads(client: client)
    112     try ensure()
    113     try await accept(client.submissionStatus(request: initial.request))
    114   }
    115 }