submission.rs (5453B)
1 //! Stable action identity and exact saved source; no publication acknowledgement. 2 3 use crate::{ 4 FfiComposerDraftRecord, FfiComposerScopeRecord, MOBILE_FFI_SCHEMA_VERSION, TeraAppError, 5 }; 6 use tera_core::runtime::product_surface::{ 7 ComposerRevision, SubmissionCommandId, SubmissionReservationError, 8 SubmissionReservationReceipt, SubmissionReservationRequest, 9 }; 10 11 mod error; 12 mod operation; 13 mod page; 14 mod renewal; 15 mod retry; 16 mod targets; 17 pub use operation::*; 18 pub use page::*; 19 pub use renewal::FfiSubmissionUploadRenewal; 20 pub use retry::*; 21 pub use targets::*; 22 23 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] 24 pub struct FfiSubmissionCommandIdRecord { 25 pub schema_version: u16, 26 pub id: String, 27 } 28 29 /// Generate once for each intentional Submit and retain it for every retry. 30 #[cfg_attr(not(coverage_nightly), uniffi::export)] 31 pub fn submission_reserve_id() -> Result<FfiSubmissionCommandIdRecord, TeraAppError> { 32 Ok(FfiSubmissionCommandIdRecord { 33 schema_version: MOBILE_FFI_SCHEMA_VERSION, 34 id: hex::encode(SubmissionCommandId::generate()?.as_bytes()), 35 }) 36 } 37 38 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] 39 pub struct FfiSubmissionReservationRequest { 40 pub schema_version: u16, 41 pub command_id: String, 42 pub scope: FfiComposerScopeRecord, 43 pub composer_id: String, 44 pub expected_revision: u64, 45 } 46 47 impl From<&SubmissionReservationRequest> for FfiSubmissionReservationRequest { 48 fn from(value: &SubmissionReservationRequest) -> Self { 49 Self { 50 schema_version: MOBILE_FFI_SCHEMA_VERSION, 51 command_id: hex::encode(value.command_id().as_bytes()), 52 scope: value.scope().into(), 53 composer_id: hex::encode(value.composer_id().as_bytes()), 54 expected_revision: value.expected_revision().get(), 55 } 56 } 57 } 58 59 impl TryFrom<FfiSubmissionReservationRequest> for SubmissionReservationRequest { 60 type Error = TeraAppError; 61 fn try_from(value: FfiSubmissionReservationRequest) -> Result<Self, Self::Error> { 62 if value.schema_version != MOBILE_FFI_SCHEMA_VERSION { 63 return Err(SubmissionReservationError::UnsupportedSchema.into()); 64 } 65 if value.command_id.len() != 32 66 || !value 67 .command_id 68 .bytes() 69 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) 70 { 71 return Err(SubmissionReservationError::InvalidCommandId.into()); 72 } 73 let command_id = SubmissionCommandId::new(crate::decode_id( 74 &value.command_id, 75 "submission_command_id_invalid", 76 )?)?; 77 Ok(Self::new( 78 command_id, 79 value.scope.try_into()?, 80 crate::composer::id(&value.composer_id)?, 81 ComposerRevision::new(value.expected_revision)?, 82 )) 83 } 84 } 85 86 /// Confirms only the committed reservation and its immutable historical composer. 87 #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] 88 pub struct FfiSubmissionReservationReceipt { 89 pub schema_version: u16, 90 pub command_id: String, 91 pub reservation_id: String, 92 pub captured: FfiComposerDraftRecord, 93 pub reserved_at_unix_ms: u64, 94 pub replayed: bool, 95 } 96 97 impl From<&SubmissionReservationReceipt> for FfiSubmissionReservationReceipt { 98 fn from(value: &SubmissionReservationReceipt) -> Self { 99 Self { 100 schema_version: MOBILE_FFI_SCHEMA_VERSION, 101 command_id: hex::encode(value.request().command_id().as_bytes()), 102 reservation_id: hex::encode(value.reservation_id().as_bytes()), 103 captured: value.captured().into(), 104 reserved_at_unix_ms: value.reserved_at_unix_ms(), 105 replayed: value.is_replay(), 106 } 107 } 108 } 109 110 impl From<SubmissionReservationError> for TeraAppError { 111 fn from(error: SubmissionReservationError) -> Self { 112 use SubmissionReservationError as E; 113 let (code, retryable, actions): (_, _, &[&str]) = match error { 114 E::Source(error) => return error.into(), 115 E::Lifecycle(error) => return tera_core::TeraAppError::from(error).into(), 116 E::InvalidCommandId => return Self::invalid_argument("submission_command_id_invalid"), 117 E::IdempotencyConflict => { 118 ("idempotency_conflict", false, &["restore_original_request"]) 119 } 120 E::CorruptRecord => ( 121 "submission_record_corrupt", 122 false, 123 &["preserve_local_work", "inspect_local_stores"], 124 ), 125 E::UnsupportedSchema => ( 126 "submission_schema_unsupported", 127 false, 128 &["preserve_local_work", "update_app"], 129 ), 130 E::InvalidReceipt => ("submission_receipt_mismatch", true, &["retry_same_command"]), 131 E::ClockUnavailable => ("operation_clock_unavailable", true, &["check_device_clock"]), 132 E::Storage(radroots_storage::Error::SpaceInsufficient) => { 133 return Self::needs_space("submission"); 134 } 135 E::Storage(_) => ( 136 "submission_storage_failed", 137 true, 138 &["inspect_local_stores", "retry_same_command"], 139 ), 140 }; 141 Self::failure( 142 code, 143 "submission", 144 retryable, 145 actions, 146 "The submission reservation could not be confirmed.", 147 ) 148 } 149 }