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 }