// file: crates/ksp-app-backfill-desk/src/dto_backfill.rs // version: 4 //! Application-owned Backfill campaign DTOs and backend-derived request limits. use ts_rs::TS; // rust-rules: trait-import /// Backend-derived limits and application defaults rendered by the Backfill campaign form. #[derive(Clone, Debug, serde::Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillRequestLimitsDto.ts")] pub(crate) struct BackfillRequestLimitsDto { /// Initial hydration concurrency proposed by this Desk while remaining below the Job-owned maximum. pub(crate) default_hydration_concurrency: u32, /// Initial candidate cap proposed by this Desk while remaining below the Job-owned maximum. pub(crate) default_max_candidates: u32, /// Initial discovery-page cap proposed by this Desk while remaining below the Job-owned maximum. pub(crate) default_max_pages: u32, /// Initial signature page size proposed by this Desk while remaining below the Job-owned maximum. pub(crate) default_page_size: u32, /// Maximum hydration concurrency owned by `ksp-job-backfill-lib`. pub(crate) max_hydration_concurrency: u32, /// Maximum candidate count owned by `ksp-job-backfill-lib`. pub(crate) max_candidates: u32, /// Maximum discovery-page count owned by `ksp-job-backfill-lib`. pub(crate) max_pages: u32, /// Maximum signature page size owned by `ksp-job-backfill-lib`. pub(crate) max_page_size: u32, /// Maximum encoded transaction-signature text length owned by `ksp-job-backfill-lib`. pub(crate) max_signature_text_bytes: u32, /// Minimum encoded transaction-signature text length owned by `ksp-job-backfill-lib`. pub(crate) min_signature_text_bytes: u32, } /// App-owned request received from the Backfill campaign form before a Job is started. /// /// Network and physical endpoint information are intentionally absent. The backend derives the /// network from the configured Store and maps `http_role` only after checking the current safe /// Transport inventory. #[derive(serde::Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartRequestDto.ts")] pub(crate) struct BackfillStartRequestDto { /// Optional address text used only by address-based scopes. pub(crate) address: std::option::Option, /// Optional exclusive anchor signature used only by Before/After scopes. pub(crate) anchor_signature: std::option::Option, /// Stable Backfill commitment code. pub(crate) commitment: String, /// Explicit signature texts used only by the explicit-signatures scope. pub(crate) explicit_signatures: std::vec::Vec, /// Maximum number of concurrent candidate hydrations. pub(crate) hydration_concurrency: u32, /// Logical HTTP role selected from `BackfillDeskOptionsDto::http_routes`. pub(crate) http_role: String, /// Maximum number of candidates admitted by the campaign. pub(crate) max_candidates: u32, /// Maximum number of address-discovery pages admitted by the campaign. pub(crate) max_pages: u32, /// Optional minimum context slot encoded as decimal text to avoid JavaScript integer precision loss. pub(crate) min_context_slot: std::option::Option, /// Signature page size used by address discovery. pub(crate) page_size: u32, /// Stable Backfill scope kind code. pub(crate) scope_kind: String, } /// Safe immediate acknowledgement returned after one Backfill run is admitted and spawned. #[derive(Clone, Debug, serde::Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartResponseDto.ts")] pub(crate) struct BackfillStartResponseDto { /// Backend-generated bounded Job identifier for this in-session run. pub(crate) job_id: String, /// Initial lifecycle state at the time Start returns to the frontend. pub(crate) state: String, } /// Safe acknowledgement of one targeted cooperative cancellation request. #[derive(Clone, Debug, serde::Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillCancelResponseDto.ts")] pub(crate) struct BackfillCancelResponseDto { /// Whether this call won the first pre-terminal cancellation request for the targeted run. pub(crate) accepted: bool, /// Backend-generated Job identifier that the cancellation request targeted. pub(crate) job_id: String, /// Safe current or cancellation-intent lifecycle code at command completion. pub(crate) state: String, } /// Safe acknowledgement returned when one retained checkpoint is resumed into a new backend Job lifecycle. #[derive(Clone, Debug, serde::Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillResumeResponseDto.ts")] pub(crate) struct BackfillResumeResponseDto { /// Whether the retained in-session checkpoint was accepted for a new run. pub(crate) accepted: bool, /// Newly allocated backend Job identifier owning the reissued checkpoint. pub(crate) job_id: String, /// Initial lifecycle state at the time Resume returns to the frontend. pub(crate) state: String, } /// Safe projection proving that one app request mapped to the KSP Backfill contract. /// /// Address and signature values are intentionally reduced to presence/count metadata. This DTO is /// suitable for request validation feedback before the runtime Start slice exists. #[derive(Clone, Debug, serde::Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillRequestPreviewDto.ts")] pub(crate) struct BackfillRequestPreviewDto { /// Whether an address participates in the validated scope. pub(crate) address_present: bool, /// Whether an exclusive anchor participates in the validated scope. pub(crate) anchor_present: bool, /// Stable validated commitment code. pub(crate) commitment: String, /// Number of explicit signatures after Backfill-owned stable deduplication. pub(crate) explicit_signature_count: u32, /// Validated hydration concurrency. pub(crate) hydration_concurrency: u32, /// Validated logical HTTP role. pub(crate) http_role: String, /// Validated maximum candidate count. pub(crate) max_candidates: u32, /// Validated maximum discovery-page count. pub(crate) max_pages: u32, /// Whether a minimum context slot participates in the validated request. pub(crate) min_context_slot_present: bool, /// Backend-derived logical Store network. pub(crate) network: String, /// Validated signature page size. pub(crate) page_size: u32, /// Stable validated scope kind code. pub(crate) scope_kind: String, } /// Returns the exact commitment codes currently admitted by the Backfill runtime. #[must_use] pub(crate) fn backfill_commitment_codes() -> std::vec::Vec { return vec![ksp_job_backfill_lib::BackfillCommitment::Finalized.code().to_owned(), ksp_job_backfill_lib::BackfillCommitment::Confirmed.code().to_owned()]; } /// Returns the exact HTTP scope codes currently admitted by the Backfill runtime. #[must_use] pub(crate) fn backfill_scope_kind_codes() -> std::vec::Vec { return vec![ ksp_job_backfill_lib::BackfillScopeKind::LatestAddress.code().to_owned(), ksp_job_backfill_lib::BackfillScopeKind::BeforeAddress.code().to_owned(), ksp_job_backfill_lib::BackfillScopeKind::AfterAddress.code().to_owned(), ksp_job_backfill_lib::BackfillScopeKind::ExplicitSignatures.code().to_owned(), ]; } /// Builds frontend-safe request limits directly from the public Backfill constants. pub(crate) fn backfill_request_limits() -> ksp_core_lib::Result { let max_hydration_concurrency = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_HYDRATION_CONCURRENCY, "max_hydration_concurrency"); let max_hydration_concurrency = match max_hydration_concurrency { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let max_candidates = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_CANDIDATES, "max_candidates"); let max_candidates = match max_candidates { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let max_pages = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_PAGES, "max_pages"); let max_pages = match max_pages { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let max_page_size = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_PAGE_SIZE, "max_page_size"); let max_page_size = match max_page_size { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let max_signature_text_bytes = usize_to_u32(ksp_job_backfill_lib::MAX_BACKFILL_SIGNATURE_TEXT_BYTES, "max_signature_text_bytes"); let max_signature_text_bytes = match max_signature_text_bytes { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let min_signature_text_bytes = usize_to_u32(ksp_job_backfill_lib::MIN_BACKFILL_SIGNATURE_TEXT_BYTES, "min_signature_text_bytes"); let min_signature_text_bytes = match min_signature_text_bytes { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if crate::DEFAULT_BACKFILL_HYDRATION_CONCURRENCY == 0 || crate::DEFAULT_BACKFILL_HYDRATION_CONCURRENCY > max_hydration_concurrency { return std::result::Result::Err(limit_contract_error("default_hydration_concurrency")); } if crate::DEFAULT_BACKFILL_MAX_CANDIDATES == 0 || crate::DEFAULT_BACKFILL_MAX_CANDIDATES > max_candidates { return std::result::Result::Err(limit_contract_error("default_max_candidates")); } if crate::DEFAULT_BACKFILL_MAX_PAGES == 0 || crate::DEFAULT_BACKFILL_MAX_PAGES > max_pages { return std::result::Result::Err(limit_contract_error("default_max_pages")); } if crate::DEFAULT_BACKFILL_PAGE_SIZE == 0 || crate::DEFAULT_BACKFILL_PAGE_SIZE > max_page_size { return std::result::Result::Err(limit_contract_error("default_page_size")); } return std::result::Result::Ok(BackfillRequestLimitsDto { default_hydration_concurrency: crate::DEFAULT_BACKFILL_HYDRATION_CONCURRENCY, default_max_candidates: crate::DEFAULT_BACKFILL_MAX_CANDIDATES, default_max_pages: crate::DEFAULT_BACKFILL_MAX_PAGES, default_page_size: crate::DEFAULT_BACKFILL_PAGE_SIZE, max_hydration_concurrency, max_candidates, max_pages, max_page_size, max_signature_text_bytes, min_signature_text_bytes, }); } fn limit_contract_error(field: &'static str) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Backfill Desk request-limit projection is internally inconsistent") .with_context("field", field); } fn usize_to_u32(value: usize, field: &'static str) -> ksp_core_lib::Result { let converted = u32::try_from(value); return match converted { std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Err(error) => std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Backfill Desk cannot project a Job-owned bound to the frontend") .with_context("field", field) .with_source(error), ), }; } #[cfg(test)] #[path = "../unit_tests/dto_backfill.rs"] mod tests;