// file: crates/ksp-app-backfill-desk/src/backfill_status.rs // version: 1 //! Safe latest-value monitoring projection for one Backfill Desk run. use ts_rs::TS; // rust-rules: trait-import /// Stable Tauri event carrying the latest complete safe Backfill run status. pub(crate) const BACKFILL_STATUS_EVENT_NAME: &str = "ksp-backfill-status"; /// Frontend-safe latest-value projection of one concrete Backfill runtime snapshot. #[derive(Clone, Debug, serde::Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/backfill_status/BackfillRunStatusDto.ts")] pub(crate) struct BackfillRunStatusDto { /// Whether the represented Job is currently non-terminal. pub(crate) active: bool, /// Number of candidates admitted into hydration. pub(crate) candidates_admitted: u32, /// Number of admitted candidates that reached a known coordinator result. pub(crate) candidates_finished: u32, /// Number of candidates selected by discovery. pub(crate) candidates_selected: u32, /// Number of candidates cancelled before Store submission. pub(crate) cancelled_candidates: u32, /// Whether a safe caller-owned checkpoint currently exists. pub(crate) checkpoint_present: bool, /// Normal terminal completion classification, when applicable. pub(crate) completion: std::option::Option, /// Safe contiguous completion prefix retained by the current checkpoint frontier. pub(crate) contiguous_completed: u32, /// Number of Store content conflicts observed by the run. pub(crate) conflicts: u32, /// Safe discovery boundary code after discovery completes. pub(crate) discovery_boundary: std::option::Option, /// Number of identical canonical RAW transactions already present. pub(crate) entities_existing: u32, /// Number of newly inserted canonical RAW transactions. pub(crate) entities_inserted: u32, /// Number of durable purge tombstones respected by normal Backfill persistence. pub(crate) entities_purged: u32, /// Stable fatal error code retained by a failed Job, when applicable. pub(crate) failure_code: std::option::Option, /// Stable fatal error domain retained by a failed Job, when applicable. pub(crate) failure_domain: std::option::Option, /// Number of known candidate outcomes blocking the contiguous frontier. pub(crate) holes: u32, /// Backend-generated logical Job identity. pub(crate) job_id: String, /// Stable concrete Job family code. pub(crate) job_kind: String, /// Greatest observed candidate concurrency. pub(crate) maximum_in_flight: u32, /// Number of `getTransaction = null` candidates. pub(crate) missing: u32, /// Number of acquisition observations already durable. pub(crate) observations_existing: u32, /// Number of newly inserted acquisition observations. pub(crate) observations_inserted: u32, /// Stable concrete Backfill runtime phase. pub(crate) phase: String, /// Monotone latest-value notification sequence encoded as decimal text. pub(crate) sequence: String, /// Stable Backfill scope category without address/signature payloads. pub(crate) scope_kind: String, /// Stable generic Job lifecycle state. pub(crate) state: String, /// Whether this latest value is terminal. pub(crate) terminal: bool, } /// Projects one complete Backfill latest-value notification to the safe desktop monitoring contract. pub(crate) fn project_backfill_status( notification: &ksp_job_api::JobNotification, ) -> ksp_core_lib::Result { let snapshot = notification.snapshot(); let state = notification.state(); let failure = snapshot.failure_code(); let candidates_admitted = usize_to_u32(snapshot.candidates_admitted(), "candidates_admitted"); let candidates_admitted = match candidates_admitted { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let candidates_finished = usize_to_u32(snapshot.candidates_finished(), "candidates_finished"); let candidates_finished = match candidates_finished { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let candidates_selected = usize_to_u32(snapshot.candidates_selected(), "candidates_selected"); let candidates_selected = match candidates_selected { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let cancelled_candidates = usize_to_u32(snapshot.cancelled_candidates(), "cancelled_candidates"); let cancelled_candidates = match cancelled_candidates { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let contiguous_completed = usize_to_u32(snapshot.contiguous_completed(), "contiguous_completed"); let contiguous_completed = match contiguous_completed { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let conflicts = usize_to_u32(snapshot.conflicts(), "conflicts"); let conflicts = match conflicts { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let entities_existing = usize_to_u32(snapshot.entities_existing(), "entities_existing"); let entities_existing = match entities_existing { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let entities_inserted = usize_to_u32(snapshot.entities_inserted(), "entities_inserted"); let entities_inserted = match entities_inserted { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let entities_purged = usize_to_u32(snapshot.entities_purged(), "entities_purged"); let entities_purged = match entities_purged { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let holes = usize_to_u32(snapshot.holes(), "holes"); let holes = match holes { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let maximum_in_flight = usize_to_u32(snapshot.maximum_in_flight(), "maximum_in_flight"); let maximum_in_flight = match maximum_in_flight { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let missing = usize_to_u32(snapshot.missing(), "missing"); let missing = match missing { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let observations_existing = usize_to_u32(snapshot.observations_existing(), "observations_existing"); let observations_existing = match observations_existing { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let observations_inserted = usize_to_u32(snapshot.observations_inserted(), "observations_inserted"); let observations_inserted = match observations_inserted { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(BackfillRunStatusDto { active: !state.is_terminal(), candidates_admitted, candidates_finished, candidates_selected, cancelled_candidates, checkpoint_present: snapshot.checkpoint().is_some(), completion: state.completion().map(|completion| return completion.code().to_owned()), contiguous_completed, conflicts, discovery_boundary: snapshot.discovery_boundary().map(|boundary| return boundary.code().to_owned()), entities_existing, entities_inserted, entities_purged, failure_code: failure.map(|code| return code.code().to_owned()), failure_domain: failure.map(|code| return code.domain().to_owned()), holes, job_id: notification.id().as_str().to_owned(), job_kind: notification.kind().as_str().to_owned(), maximum_in_flight, missing, observations_existing, observations_inserted, phase: snapshot.phase().code().to_owned(), sequence: notification.sequence().value().to_string(), scope_kind: snapshot.scope_kind().code().to_owned(), state: state.code().to_owned(), terminal: state.is_terminal(), }); } 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 bounded Job counter to the frontend") .with_context("field", field) .with_source(error), ), }; } #[cfg(test)] #[path = "../unit_tests/backfill_status.rs"] mod tests;