234 lines
12 KiB
Rust
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;
|