// file: kb-app-demo-desktop/src/demo_core_extraction.rs // version: 14 //! Tauri commands and UI payloads for canonical transaction to core extraction. use tauri::Emitter; // rust-rules: trait-import use ts_rs::TS; // rust-rules: derive-import /// One registered program offered as a non-blocking core extraction suggestion. #[derive(Clone, Debug, serde::Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts( export, export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_core_extraction/DemoCoreExtractionProgramOption.ts" )] pub(crate) struct DemoCoreExtractionProgramOption { /// Stable registry code. pub(crate) code: std::string::String, /// Canonical Solana program id. pub(crate) program_id: std::string::String, } /// Initial options shown by the core extraction demo. #[derive(Clone, Debug, serde::Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts( export, export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_core_extraction/DemoCoreExtractionOptionsPayload.ts" )] pub(crate) struct DemoCoreExtractionOptionsPayload { /// Current extractor implementation version. pub(crate) processor_version: std::string::String, /// Default maximum selected canonical transactions. pub(crate) default_limit: u32, /// Default maximum concurrent extractions. pub(crate) default_max_concurrent_extractions: u32, /// Whether one extraction campaign is currently running. pub(crate) running: bool, /// Registered program ids offered without restricting free text entry. pub(crate) programs: std::vec::Vec, } /// UI request for one bounded core extraction campaign. #[derive(Clone, Debug, serde::Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts( export, export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_core_extraction/DemoCoreExtractionRequest.ts" )] pub(crate) struct DemoCoreExtractionRequest { /// Source mode: signatures, pending or slot_range. pub(crate) mode: std::string::String, /// Newline-separated signatures for exact selection. pub(crate) signatures_text: std::option::Option, /// Optional program id already present in core instructions. pub(crate) program_id: std::option::Option, /// Optional inclusive minimum slot. #[ts(type = "number | null")] pub(crate) min_slot: std::option::Option, /// Optional inclusive maximum slot. #[ts(type = "number | null")] pub(crate) max_slot: std::option::Option, /// Maximum selected canonical transactions. pub(crate) limit: u32, /// Maximum concurrent extraction operations. pub(crate) max_concurrent_extractions: u32, /// Forces replacement of already current core rows. pub(crate) force_replay: bool, } /// One progress event emitted to the core extraction window. #[derive(Clone, Debug, serde::Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts( export, export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_core_extraction/DemoCoreExtractionProgressPayload.ts" )] pub(crate) struct DemoCoreExtractionProgressPayload { /// RFC 3339 timestamp. pub(crate) timestamp: std::string::String, /// Stable severity code. pub(crate) level: std::string::String, /// Human-readable message. pub(crate) message: std::string::String, /// Number of terminal candidates. #[ts(type = "number")] pub(crate) completed: u64, /// Total selected candidates. #[ts(type = "number")] pub(crate) total: u64, } /// Final UI-safe summary for one extraction campaign. #[derive(Clone, Debug, serde::Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts( export, export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_core_extraction/DemoCoreExtractionSummaryPayload.ts" )] pub(crate) struct DemoCoreExtractionSummaryPayload { /// Current extractor implementation version. pub(crate) processor_version: std::string::String, /// Number of selected canonical transactions. #[ts(type = "number")] pub(crate) selected: u64, /// Number admitted to the bounded execution queue. #[ts(type = "number")] pub(crate) started: u64, /// Number reaching a terminal result. #[ts(type = "number")] pub(crate) completed: u64, /// Number skipped by version/hash idempotence. #[ts(type = "number")] pub(crate) skipped: u64, /// Number extracted and committed. #[ts(type = "number")] pub(crate) extracted: u64, /// Number failed. #[ts(type = "number")] pub(crate) failed: u64, /// Number admitted but cancelled before a terminal result. #[ts(type = "number")] pub(crate) cancelled_candidates: u64, /// Number selected but not started after cancellation. #[ts(type = "number")] pub(crate) not_started: u64, /// Whether the campaign was cancelled. pub(crate) cancelled: bool, /// Campaign start timestamp. pub(crate) started_at: std::string::String, /// Campaign finish timestamp. pub(crate) finished_at: std::string::String, } pub(crate) struct DemoCoreExtractionObserver<'a> { pub(crate) app_handle: tauri::AppHandle, pub(crate) cancel_requested: &'a std::sync::atomic::AtomicBool, } impl ks_pipeline::CoreExtractionObserver for crate::DemoCoreExtractionObserver<'_> { fn on_progress(&self, event: &ks_pipeline::CoreExtractionProgressEvent) { let payload = crate::DemoCoreExtractionProgressPayload { timestamp: event.timestamp.clone(), level: event.level.code().to_string(), message: event.message.clone(), completed: event.completed, total: event.total, }; let emit_result = self.app_handle.emit_to( "demo_core_extraction", "demo-core-extraction-progress", payload, ); if let std::result::Result::Err(error) = emit_result { tracing::warn!( target: crate::TRACING_TARGET, "cannot emit core extraction progress: {error}" ); } } fn is_cancelled(&self) -> bool { return self.cancel_requested.load(std::sync::atomic::Ordering::Relaxed); } } pub(crate) struct DemoCoreExtractionRunGuard<'a> { pub(crate) running: &'a std::sync::atomic::AtomicBool, } impl std::ops::Drop for crate::DemoCoreExtractionRunGuard<'_> { fn drop(&mut self) { self.running.store(false, std::sync::atomic::Ordering::Release); } } pub(crate) fn demo_core_extraction_options_payload( state: &crate::AppState, ) -> crate::DemoCoreExtractionOptionsPayload { return crate::DemoCoreExtractionOptionsPayload { processor_version: ks_pipeline::CORE_EXTRACTION_PROCESSOR_VERSION.to_string(), default_limit: 100, default_max_concurrent_extractions: 4, running: state.demo_core_extraction_running().load(std::sync::atomic::Ordering::Acquire), programs: ks_program_ids::registered_program_ids() .iter() .map(|entry| { return crate::DemoCoreExtractionProgramOption { code: entry.code().to_string(), program_id: entry.program_id().to_string(), }; }) .collect(), }; } pub(crate) async fn demo_core_extraction_execute( app_handle: tauri::AppHandle, state: &crate::AppState, request: crate::DemoCoreExtractionRequest, ) -> std::result::Result { let acquire_result = state.demo_core_extraction_running().compare_exchange( false, true, std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire, ); if acquire_result.is_err() { return std::result::Result::Err( "a core extraction campaign is already running".to_string(), ); } let _run_guard = crate::DemoCoreExtractionRunGuard { running: state.demo_core_extraction_running(), }; state .demo_core_extraction_cancel_requested() .store(false, std::sync::atomic::Ordering::Release); let pipeline_request_result = build_pipeline_request(request); let pipeline_request = match pipeline_request_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let store_result = crate::connect_postgres_store(state.active_profile()).await; let store = match store_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let observer = crate::DemoCoreExtractionObserver { app_handle, cancel_requested: state.demo_core_extraction_cancel_requested(), }; let summary_result = ks_pipeline::execute_core_extraction(&store, &pipeline_request, &observer).await; let summary = match summary_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; return std::result::Result::Ok(summary_payload(summary)); } pub(crate) fn demo_core_extraction_cancel(state: &crate::AppState) -> bool { let running = state.demo_core_extraction_running().load(std::sync::atomic::Ordering::Acquire); state .demo_core_extraction_cancel_requested() .store(true, std::sync::atomic::Ordering::Release); return running; } fn build_pipeline_request( request: crate::DemoCoreExtractionRequest, ) -> std::result::Result { let source_result = match request.mode.trim() { "signatures" => { let signatures = split_signatures(request.signatures_text.as_deref()); std::result::Result::Ok(ks_pipeline::CoreExtractionSource::Signatures(signatures)) }, "pending" => std::result::Result::Ok(ks_pipeline::CoreExtractionSource::Pending), "program_id" => { let program_id = match request.program_id { std::option::Option::Some(value) if !value.trim().is_empty() => value, _ => { return std::result::Result::Err( "program id is required for program extraction".to_string(), ); }, }; std::result::Result::Ok(ks_pipeline::CoreExtractionSource::ProgramId { program_id }) }, "slot_range" => { let min_slot = match request.min_slot { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err( "minimum slot is required for slot range extraction".to_string(), ); }, }; let max_slot = match request.max_slot { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err( "maximum slot is required for slot range extraction".to_string(), ); }, }; std::result::Result::Ok(ks_pipeline::CoreExtractionSource::SlotRange { min_slot, max_slot, }) }, _ => std::result::Result::Err("unsupported core extraction mode".to_string()), }; let source = match source_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let pipeline_request = ks_pipeline::CoreExtractionRequest { source, limit: request.limit, max_concurrent_extractions: request.max_concurrent_extractions, force_replay: request.force_replay, }; let validation_result = pipeline_request.validate(); if let std::result::Result::Err(error) = validation_result { return std::result::Result::Err(error.to_string()); } return std::result::Result::Ok(pipeline_request); } fn summary_payload( summary: ks_pipeline::CoreExtractionSummary, ) -> crate::DemoCoreExtractionSummaryPayload { return crate::DemoCoreExtractionSummaryPayload { processor_version: summary.processor_version, selected: summary.selected, started: summary.started, completed: summary.completed, skipped: summary.skipped, extracted: summary.extracted, failed: summary.failed, cancelled_candidates: summary.cancelled_candidates, not_started: summary.not_started, cancelled: summary.cancelled, started_at: summary.started_at, finished_at: summary.finished_at, }; } fn split_signatures(text: std::option::Option<&str>) -> std::vec::Vec { let source = match text { std::option::Option::Some(value) => value, std::option::Option::None => "", }; let mut unique = std::collections::BTreeSet::::new(); let mut output = std::vec::Vec::new(); for line in source.lines() { let signature = line.trim(); if signature.is_empty() { continue; } if unique.insert(signature.to_string()) { output.push(signature.to_string()); } } return output; } #[cfg(test)] mod tests { #[test] fn signature_source_splits_deduplicates_and_ignores_empty_rows() { let values = super::split_signatures(std::option::Option::Some("alpha\n\n beta \nalpha\n")); assert_eq!(values, std::vec!["alpha".to_string(), "beta".to_string()]); } }