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 }