332 lines
13 KiB
Rust
332 lines
13 KiB
Rust
// file: kb-app-demo-desktop/src/demo_core_extraction.rs
|
|
// version: 10
|
|
|
|
//! 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
|
|
|
|
/// 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,
|
|
}
|
|
|
|
/// 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<std::string::String>,
|
|
/// Optional program id already present in core instructions.
|
|
pub(crate) program_id: std::option::Option<std::string::String>,
|
|
/// Optional inclusive minimum slot.
|
|
#[ts(type = "number | null")]
|
|
pub(crate) min_slot: std::option::Option<u64>,
|
|
/// Optional inclusive maximum slot.
|
|
#[ts(type = "number | null")]
|
|
pub(crate) max_slot: std::option::Option<u64>,
|
|
/// 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 kb_pipeline::CoreExtractionObserver for crate::DemoCoreExtractionObserver<'_> {
|
|
fn on_progress(&self, event: &kb_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: kb_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),
|
|
};
|
|
}
|
|
|
|
pub(crate) async fn execute_demo_core_extraction(
|
|
app_handle: tauri::AppHandle,
|
|
state: &crate::AppState,
|
|
request: crate::DemoCoreExtractionRequest,
|
|
) -> std::result::Result<crate::DemoCoreExtractionSummaryPayload, std::string::String> {
|
|
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 = crate::build_demo_core_extraction_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 =
|
|
kb_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(crate::demo_core_extraction_summary_payload(summary));
|
|
}
|
|
|
|
pub(crate) fn cancel_demo_core_extraction(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;
|
|
}
|
|
|
|
pub(crate) fn build_demo_core_extraction_pipeline_request(
|
|
request: crate::DemoCoreExtractionRequest,
|
|
) -> std::result::Result<kb_pipeline::CoreExtractionRequest, std::string::String> {
|
|
let source_result = match request.mode.trim() {
|
|
"signatures" => {
|
|
let signatures = split_signatures(request.signatures_text.as_deref());
|
|
std::result::Result::Ok(kb_pipeline::CoreExtractionSource::Signatures(signatures))
|
|
},
|
|
"pending" => std::result::Result::Ok(kb_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(kb_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(kb_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 = kb_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 split_signatures(text: std::option::Option<&str>) -> std::vec::Vec<std::string::String> {
|
|
let source = match text {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => "",
|
|
};
|
|
let mut unique = std::collections::BTreeSet::<std::string::String>::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;
|
|
}
|
|
|
|
pub(crate) fn demo_core_extraction_summary_payload(
|
|
summary: kb_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,
|
|
};
|
|
}
|
|
|
|
#[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()]);
|
|
}
|
|
}
|