field_ios

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

TeraLegacySubmission.swift (5153B)


      1 import Foundation
      2 
      3 /// Existing legacy and revision behavior; scoped IDs never enter this adapter.
      4 @MainActor
      5 struct TeraLegacySubmission {
      6   let runtimeClient: TeraRuntimeClient
      7   let media: (any TeraAddMediaHandling)?
      8   let revisionID: () -> String?
      9   let initial: () async throws -> TeraDraftStatus
     10   let ensure: () throws -> Void
     11   let acceptDraft: (TeraDraftStatus) throws -> Void
     12   let acceptRevision: (TeraRevisionStatus) throws -> Void
     13   let message: (String) -> Void
     14   let refreshMedia: () async -> Void
     15 
     16   func submit() async throws {
     17       var status = try await initial()
     18 
     19       try ensure()
     20       guard status.coordinateWritable else {
     21         try acceptDraft(status)
     22         message(status.honestSummary)
     23         return
     24       }
     25       if !status.media.isEmpty,
     26         status.media.contains(where: { $0.stage != .verified })
     27       {
     28         status = try await uploadPendingMedia(status)
     29       }
     30 
     31       try ensure()
     32       if status.isRevision {
     33         let revision = try await runtimeClient.advanceRevision(
     34           operationID: revisionID() ?? status.id
     35         )
     36         try acceptRevision(revision)
     37         message(revision.honestSummary)
     38         return
     39       }
     40 
     41       if status.state.isEditable || status.state == .readyToSign {
     42         do {
     43           status = try await runtimeClient.queueAddIntent(
     44             id: status.id,
     45             expectedRevision: status.revision
     46           )
     47           try acceptDraft(status)
     48         } catch {
     49           try ensure()
     50           if TeraAddPresentation.failure(for: error)?.code == "writable_relay_unavailable" {
     51             try acceptDraft(status)
     52             message(status.media.isEmpty
     53               ? "Draft saved. Configure a writable relay to publish."
     54               : "Photo verified and draft saved. Configure a writable relay to publish.")
     55             return
     56           }
     57           throw error
     58         }
     59       }
     60 
     61       try await advanceSubmittedDraft(status)
     62   }
     63 
     64   private func advanceSubmittedDraft(
     65     _ initial: TeraDraftStatus
     66   ) async throws {
     67     var status = initial
     68     do {
     69       if status.canAdvance {
     70         status = try await runtimeClient.advanceDraft(
     71           id: status.id,
     72           expectedRevision: status.revision
     73         )
     74         try acceptDraft(status)
     75       }
     76       message(status.honestSummary)
     77     } catch {
     78       try ensure()
     79       // Queueing is the commit point. A later retry must reuse this immutable snapshot.
     80       message("Saved for retry. \(TeraAddPresentation.message(for: error))")
     81     }
     82   }
     83 
     84   private func uploadPendingMedia(_ initial: TeraDraftStatus) async throws -> TeraDraftStatus {
     85     guard let media else {
     86       throw TeraRuntimeFailure.local(
     87         operation: "add.media.upload",
     88         code: "ios.add.media_unavailable",
     89         safeMessage: "Prepared photos are unavailable on this device."
     90       )
     91     }
     92     var status = initial
     93     guard let form = status.form else { return status }
     94     let opened = try await TeraOpenedMedia.open(form.media, using: media)
     95     defer { opened.close() }
     96     try ensure()
     97     for mediaStatus in status.media where mediaStatus.stage != .verified {
     98       guard let persisted = form.media.first(where: { $0.remoteURL == mediaStatus.url }),
     99         let handle = opened.handles.first(where: {
    100           $0.media.opaqueReference == persisted.opaqueReference
    101         })
    102       else {
    103         throw TeraRuntimeFailure.local(
    104           operation: "add.media.upload",
    105           code: "ios.add.media_missing",
    106           safeMessage: "A prepared photo is unavailable."
    107         )
    108       }
    109       let intent = TeraBlossomUploadIntent(
    110         draftID: status.id,
    111         expectedRevision: status.revision,
    112         media: handle
    113       )
    114       if try await media.prefersSharedForegroundUpload(ownerID: status.id) {
    115         status = try await uploadForeground(intent, progress: mediaStatus)
    116         try acceptDraft(status)
    117         await refreshMedia()
    118         try ensure()
    119         continue
    120       }
    121       let job = try await runtimeClient.prepareAddMediaBackground(input: intent)
    122       try acceptDraft(job.draft)
    123       let receipt = try await media.uploadInBackground(job: job, media: persisted)
    124       try ensure()
    125       status = try await TeraAddUploadCompletion.complete(receipt, handle: handle, media: media, runtimeClient: runtimeClient)
    126       try await media.settleBackgroundUpload(identifier: receipt.identifier, accepted: true)
    127       _ = await TeraNativeRecoveryClassification.report(receipt.identifier, reason: .resolved, client: runtimeClient)
    128       try acceptDraft(status)
    129       await refreshMedia()
    130       try ensure()
    131     }
    132     return status
    133   }
    134 
    135   private func uploadForeground(_ intent: TeraBlossomUploadIntent, progress: TeraDraftMediaStatus) async throws -> TeraDraftStatus {
    136     try ensure()
    137     guard progress.stage != .uploading, !progress.possibleOrphan else {
    138       throw TeraRuntimeFailure.local(operation: "add.media.upload", code: "ios.add.media_recovery_required",
    139                                      safeMessage: "The previous photo upload must be checked before trying again.")
    140     }
    141     return try await Task { try await runtimeClient.uploadAddMediaIntent(input: intent) }.value
    142   }
    143 }