Files
khadhroony-solana-project/crates/ksp-app-backfill-desk/src/dto_backfill.rs
2026-09-02 19:50:01 +02:00

234 lines
12 KiB
Rust

// 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<String>,
/// Optional exclusive anchor signature used only by Before/After scopes.
pub(crate) anchor_signature: std::option::Option<String>,
/// 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<String>,
/// 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<String>,
/// 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<String> {
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<String> {
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<BackfillRequestLimitsDto> {
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<u32> {
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;