v0.2.12-pre.006
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-solprices-desk/frontend/main.html -->
|
||||
<!-- version: 6 -->
|
||||
<!-- version: 7 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -44,10 +44,19 @@
|
||||
<div class="card app-shell-card mx-auto shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<section data-view-panel="prices">
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<div class="d-flex align-items-start justify-content-between gap-3 flex-wrap mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">Prices</h1>
|
||||
<p class="text-body-secondary mb-0">Inventaire provider-neutral du registry Off-chain. Chaque ligne peut être rafraîchie explicitement sans polling ni fallback automatique.</p>
|
||||
<p class="text-body-secondary mb-0">Inventaire provider-neutral du registry Off-chain. Rafraîchissement individuel, sélectionné ou global, sans polling ni fallback automatique.</p>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<span id="selectedProviderCount" class="small text-body-secondary">0 selected</span>
|
||||
<button id="refreshSelectedButton" class="btn btn-sm btn-outline-primary" type="button" disabled>
|
||||
<i class="fa-solid fa-list-check me-1" aria-hidden="true"></i>Refresh selected
|
||||
</button>
|
||||
<button id="refreshAllButton" class="btn btn-sm btn-primary" type="button" disabled>
|
||||
<i class="fa-solid fa-arrows-rotate me-1" aria-hidden="true"></i>Refresh all
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pricesLoading" class="app-placeholder">
|
||||
@@ -69,6 +78,7 @@
|
||||
<table id="marketPriceTable" class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><span class="visually-hidden">Select</span></th>
|
||||
<th scope="col">Provider</th>
|
||||
<th scope="col">Pair</th>
|
||||
<th scope="col">Semantics</th>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-solprices-desk/frontend/ts/main.ts
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
import "bootstrap";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
@@ -7,6 +7,8 @@ import "simplebar";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import type { MarketPriceRuntimeStatusDto } from "./bindings/ksp_app_solprices_desk/dto_common/MarketPriceRuntimeStatusDto.ts";
|
||||
import type { MarketPriceProviderRowDto } from "./bindings/ksp_app_solprices_desk/market_price_runtime/MarketPriceProviderRowDto.ts";
|
||||
import type { MarketPriceRefreshManyRequestDto } from "./bindings/ksp_app_solprices_desk/market_price_runtime/MarketPriceRefreshManyRequestDto.ts";
|
||||
import type { MarketPriceRefreshResultDto } from "./bindings/ksp_app_solprices_desk/market_price_runtime/MarketPriceRefreshResultDto.ts";
|
||||
import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
|
||||
import { invokeKsp } from "./invoke";
|
||||
|
||||
@@ -21,6 +23,7 @@ const viewTitles: Record<ViewId, string> = {
|
||||
};
|
||||
|
||||
let currentMarketPriceRows: MarketPriceProviderRowDto[] = [];
|
||||
const selectedProviderIds = new Set<string>();
|
||||
|
||||
function isViewId(value: string | undefined): value is ViewId {
|
||||
return value === "prices" || value === "diagnostics";
|
||||
@@ -78,13 +81,66 @@ function showPricesDiagnostic(message: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function selectedProviderIdsInRowOrder(): string[] {
|
||||
return currentMarketPriceRows.filter(provider => selectedProviderIds.has(provider.providerId)).map(provider => provider.providerId);
|
||||
}
|
||||
|
||||
function synchronizeSelectedProviders(): void {
|
||||
const currentIds = new Set(currentMarketPriceRows.map(provider => provider.providerId));
|
||||
for (const providerId of selectedProviderIds) {
|
||||
if (!currentIds.has(providerId)) {
|
||||
selectedProviderIds.delete(providerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function synchronizeBatchControls(): void {
|
||||
const selectedCount = selectedProviderIdsInRowOrder().length;
|
||||
const anyLoading = currentMarketPriceRows.some(provider => provider.loading);
|
||||
const selectedButton = document.querySelector<HTMLButtonElement>("#refreshSelectedButton");
|
||||
const allButton = document.querySelector<HTMLButtonElement>("#refreshAllButton");
|
||||
const selectedCountLabel = document.querySelector<HTMLElement>("#selectedProviderCount");
|
||||
if (selectedButton) {
|
||||
selectedButton.disabled = selectedCount === 0 || anyLoading;
|
||||
}
|
||||
if (allButton) {
|
||||
allButton.disabled = currentMarketPriceRows.length === 0 || anyLoading;
|
||||
}
|
||||
if (selectedCountLabel) {
|
||||
selectedCountLabel.textContent = `${selectedCount} selected`;
|
||||
}
|
||||
}
|
||||
|
||||
function markProvidersLoading(providerIds: string[]): void {
|
||||
const loadingIds = new Set(providerIds);
|
||||
currentMarketPriceRows = currentMarketPriceRows.map(provider => {
|
||||
return loadingIds.has(provider.providerId) ? { ...provider, loading: true } : provider;
|
||||
});
|
||||
renderMarketPriceRows(currentMarketPriceRows);
|
||||
}
|
||||
|
||||
function applyRefreshResult(result: MarketPriceRefreshResultDto): void {
|
||||
const refreshedRows = new Map<string, MarketPriceProviderRowDto>();
|
||||
for (const provider of result.rows) {
|
||||
refreshedRows.set(provider.providerId, provider);
|
||||
}
|
||||
currentMarketPriceRows = currentMarketPriceRows.map(provider => refreshedRows.get(provider.providerId) ?? provider);
|
||||
renderMarketPriceRows(currentMarketPriceRows);
|
||||
}
|
||||
|
||||
async function recoverMarketPriceRows(message: string): Promise<void> {
|
||||
showPricesDiagnostic(message);
|
||||
try {
|
||||
await loadMarketPrices();
|
||||
} catch {
|
||||
frontendWarn("main", "SOL Prices Desk market-price refresh recovery reload failed");
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshMarketPrice(providerId: string): Promise<void> {
|
||||
frontendDebug("main", "SOL Prices Desk market-price refresh control clicked", { providerId });
|
||||
clearPricesDiagnostic();
|
||||
currentMarketPriceRows = currentMarketPriceRows.map(provider => {
|
||||
return provider.providerId === providerId ? { ...provider, loading: true } : provider;
|
||||
});
|
||||
renderMarketPriceRows(currentMarketPriceRows);
|
||||
markProvidersLoading([providerId]);
|
||||
try {
|
||||
const refreshed = await invokeKsp<MarketPriceProviderRowDto>("main", "refresh_market_price", { providerId });
|
||||
currentMarketPriceRows = currentMarketPriceRows.map(provider => {
|
||||
@@ -94,15 +150,76 @@ async function refreshMarketPrice(providerId: string): Promise<void> {
|
||||
frontendTrace("main", "SOL Prices Desk market-price refresh row applied", { providerId });
|
||||
} catch {
|
||||
frontendWarn("main", "SOL Prices Desk market-price refresh failed", { providerId });
|
||||
showPricesDiagnostic("Le provider sélectionné n'a pas pu être rafraîchi. L'état précédent est conservé lorsqu'il reste disponible.");
|
||||
try {
|
||||
await loadMarketPrices();
|
||||
} catch {
|
||||
frontendWarn("main", "SOL Prices Desk market-price refresh recovery reload failed", { providerId });
|
||||
}
|
||||
await recoverMarketPriceRows("Le provider sélectionné n'a pas pu être rafraîchi. L'état précédent est conservé lorsqu'il reste disponible.");
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshSelectedMarketPrices(): Promise<void> {
|
||||
const providerIds = selectedProviderIdsInRowOrder();
|
||||
frontendDebug("main", "SOL Prices Desk selected market-price refresh control clicked", { selectedCount: providerIds.length });
|
||||
if (providerIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
clearPricesDiagnostic();
|
||||
markProvidersLoading(providerIds);
|
||||
const request: MarketPriceRefreshManyRequestDto = { providerIds };
|
||||
try {
|
||||
const result = await invokeKsp<MarketPriceRefreshResultDto>("main", "refresh_market_prices", { request });
|
||||
applyRefreshResult(result);
|
||||
frontendTrace("main", "SOL Prices Desk selected market-price refresh result applied", {
|
||||
requestedCount: result.requestedCount,
|
||||
refreshedCount: result.refreshedCount,
|
||||
});
|
||||
} catch {
|
||||
frontendWarn("main", "SOL Prices Desk selected market-price refresh failed", { selectedCount: providerIds.length });
|
||||
await recoverMarketPriceRows("La sélection n'a pas pu être rafraîchie. Les observations précédentes restent visibles avec leur état réel.");
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAllMarketPrices(): Promise<void> {
|
||||
frontendDebug("main", "SOL Prices Desk global market-price refresh control clicked", { providerCount: currentMarketPriceRows.length });
|
||||
if (currentMarketPriceRows.length === 0) {
|
||||
return;
|
||||
}
|
||||
clearPricesDiagnostic();
|
||||
markProvidersLoading(currentMarketPriceRows.map(provider => provider.providerId));
|
||||
try {
|
||||
const result = await invokeKsp<MarketPriceRefreshResultDto>("main", "refresh_all_market_prices");
|
||||
applyRefreshResult(result);
|
||||
frontendTrace("main", "SOL Prices Desk global market-price refresh result applied", {
|
||||
requestedCount: result.requestedCount,
|
||||
refreshedCount: result.refreshedCount,
|
||||
});
|
||||
} catch {
|
||||
frontendWarn("main", "SOL Prices Desk global market-price refresh failed", { providerCount: currentMarketPriceRows.length });
|
||||
await recoverMarketPriceRows("Le refresh global n'a pas pu être terminé. Les observations précédentes restent visibles avec leur état réel.");
|
||||
}
|
||||
}
|
||||
|
||||
function appendSelectionCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void {
|
||||
const cell = document.createElement("td");
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.className = "form-check-input";
|
||||
checkbox.checked = selectedProviderIds.has(provider.providerId);
|
||||
checkbox.disabled = provider.loading;
|
||||
checkbox.setAttribute("aria-label", `Select ${provider.displayName}`);
|
||||
checkbox.addEventListener("change", () => {
|
||||
if (checkbox.checked) {
|
||||
selectedProviderIds.add(provider.providerId);
|
||||
} else {
|
||||
selectedProviderIds.delete(provider.providerId);
|
||||
}
|
||||
frontendDebug("main", "SOL Prices Desk market-price selection control changed", {
|
||||
providerId: provider.providerId,
|
||||
selected: checkbox.checked,
|
||||
});
|
||||
synchronizeBatchControls();
|
||||
});
|
||||
cell.append(checkbox);
|
||||
row.append(cell);
|
||||
}
|
||||
|
||||
function appendRefreshCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void {
|
||||
const cell = document.createElement("td");
|
||||
const button = document.createElement("button");
|
||||
@@ -132,12 +249,15 @@ function renderMarketPriceRows(rows: MarketPriceProviderRowDto[]): void {
|
||||
frontendWarn("main", "SOL Prices Desk market-price table elements are missing");
|
||||
return;
|
||||
}
|
||||
currentMarketPriceRows = rows;
|
||||
synchronizeSelectedProviders();
|
||||
tableBody.replaceChildren();
|
||||
empty.hidden = rows.length !== 0;
|
||||
tableContainer.hidden = rows.length === 0;
|
||||
for (const provider of rows) {
|
||||
const row = document.createElement("tr");
|
||||
row.dataset.providerId = provider.providerId;
|
||||
appendSelectionCell(row, provider);
|
||||
appendMarketPriceCell(row, provider.displayName);
|
||||
appendMarketPriceCell(row, provider.pair);
|
||||
appendMarketPriceCell(row, provider.semantics);
|
||||
@@ -150,9 +270,23 @@ function renderMarketPriceRows(rows: MarketPriceProviderRowDto[]): void {
|
||||
appendRefreshCell(row, provider);
|
||||
tableBody.append(row);
|
||||
}
|
||||
synchronizeBatchControls();
|
||||
frontendTrace("main", "SOL Prices Desk market-price registry rows rendered", { rowCount: rows.length });
|
||||
}
|
||||
|
||||
function bindMarketPriceControls(): void {
|
||||
const selectedButton = document.querySelector<HTMLButtonElement>("#refreshSelectedButton");
|
||||
const allButton = document.querySelector<HTMLButtonElement>("#refreshAllButton");
|
||||
selectedButton?.addEventListener("click", () => {
|
||||
void refreshSelectedMarketPrices();
|
||||
});
|
||||
allButton?.addEventListener("click", () => {
|
||||
void refreshAllMarketPrices();
|
||||
});
|
||||
synchronizeBatchControls();
|
||||
frontendTrace("main", "SOL Prices Desk market-price batch controls installed");
|
||||
}
|
||||
|
||||
function renderRuntimeStatus(status: MarketPriceRuntimeStatusDto): void {
|
||||
const values: Record<string, string> = {
|
||||
runtimeCompositeProfile: status.activeCompositeProfile,
|
||||
@@ -203,6 +337,7 @@ async function initializeMain(): Promise<void> {
|
||||
const windowLabel = getCurrentWindow().label;
|
||||
frontendInfo("main", "SOL Prices Desk main frontend loaded", { windowLabel });
|
||||
bindNavigation();
|
||||
bindMarketPriceControls();
|
||||
activateView("prices", "startup");
|
||||
try {
|
||||
await loadRuntimeStatus();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ksp-app-solprices-desk",
|
||||
"private": true,
|
||||
"version": "0.2.12-pre.5.fix.1",
|
||||
"version": "0.2.12-pre.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-solprices-desk/src/app_state.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! Shared backend state owned by the SOL Prices Desk Tauri application.
|
||||
|
||||
@@ -73,6 +73,19 @@ impl crate::AppState {
|
||||
return self.market_price_runtime.refresh_one(provider_id).await;
|
||||
}
|
||||
|
||||
/// Refreshes a selected provider set in deterministic request order and returns only safe updated rows/counts.
|
||||
pub(crate) async fn refresh_market_prices(
|
||||
&self,
|
||||
request: crate::MarketPriceRefreshManyRequestDto,
|
||||
) -> ksp_core_lib::Result<crate::MarketPriceRefreshResultDto> {
|
||||
return self.market_price_runtime.refresh_many(request.into_provider_ids()).await;
|
||||
}
|
||||
|
||||
/// Refreshes every configured provider in the deterministic order owned by Off-chain Transport.
|
||||
pub(crate) async fn refresh_all_market_prices(&self) -> ksp_core_lib::Result<crate::MarketPriceRefreshResultDto> {
|
||||
return self.market_price_runtime.refresh_all().await;
|
||||
}
|
||||
|
||||
/// Builds the safe runtime status exposed by the SOL Prices Desk shell.
|
||||
pub(crate) fn runtime_status(&self) -> ksp_core_lib::Result<crate::MarketPriceRuntimeStatusDto> {
|
||||
let document_count = self.config_management.engine().registry().descriptors().count();
|
||||
@@ -116,7 +129,7 @@ impl crate::AppState {
|
||||
fallback_logging_active: runtime.fallback_active,
|
||||
provider_count,
|
||||
ready_provider_count,
|
||||
shell_phase: "pre.005-single-refresh".to_owned(),
|
||||
shell_phase: "pre.006-multi-refresh".to_owned(),
|
||||
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
||||
unavailable_provider_count,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-solprices-desk/src/errors.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Application-local error codes for SOL Prices Desk composition and desktop runtime surfaces.
|
||||
|
||||
@@ -16,6 +16,12 @@ pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode
|
||||
ksp_core_lib::ErrorCode::new("solprices_desk", "frontend_log_target_invalid");
|
||||
/// SOL Prices Desk could not install the managed Logging runtime or its safe fallback.
|
||||
pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("solprices_desk", "logging_bootstrap_failed");
|
||||
/// One requested market-price provider already has an explicit refresh in flight.
|
||||
pub(crate) const ERROR_CODE_MARKET_PRICE_REFRESH_CONFLICT: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("solprices_desk", "market_price_refresh_conflict");
|
||||
/// A selected market-price refresh contains duplicate or unknown provider identifiers.
|
||||
pub(crate) const ERROR_CODE_MARKET_PRICE_SELECTION_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("solprices_desk", "market_price_selection_invalid");
|
||||
/// Splash readiness was invoked from a window other than the splash window.
|
||||
pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("solprices_desk", "splash_origin_invalid");
|
||||
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-solprices-desk/src/lib.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Tauri desktop application scaffold for provider-neutral SOL/USD price visualization.
|
||||
|
||||
@@ -74,6 +74,10 @@ pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
|
||||
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
|
||||
/// SOL Prices Desk could not install the managed Logging runtime or its safe fallback.
|
||||
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
|
||||
/// One requested market-price provider already has an explicit refresh in flight.
|
||||
pub(crate) use self::errors::ERROR_CODE_MARKET_PRICE_REFRESH_CONFLICT;
|
||||
/// A selected market-price refresh contains duplicate or unknown provider identifiers.
|
||||
pub(crate) use self::errors::ERROR_CODE_MARKET_PRICE_SELECTION_INVALID;
|
||||
/// Splash readiness was invoked from a window other than the splash window.
|
||||
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
|
||||
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
|
||||
@@ -92,6 +96,10 @@ pub(crate) use self::frontend_logging::emit_frontend_log_event;
|
||||
pub(crate) use self::logging_runtime::launch_identity;
|
||||
/// Safe provider-neutral market-price row exposed to the frontend.
|
||||
pub(crate) use self::market_price_runtime::MarketPriceProviderRowDto;
|
||||
/// Bounded provider-neutral request used by the selected-provider refresh command.
|
||||
pub(crate) use self::market_price_runtime::MarketPriceRefreshManyRequestDto;
|
||||
/// Safe result returned by selected/global market-price refresh commands.
|
||||
pub(crate) use self::market_price_runtime::MarketPriceRefreshResultDto;
|
||||
/// Backend application runtime owning the Config-selected service and in-memory market-price presentation state.
|
||||
pub(crate) use self::market_price_runtime::MarketPriceRuntime;
|
||||
/// Resolved composite and Off-chain Transport configuration retained by SOL Prices Desk.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-solprices-desk/src/market_price_runtime.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Provider-neutral market-price presentation runtime owned by SOL Prices Desk.
|
||||
|
||||
@@ -36,6 +36,35 @@ pub(crate) struct MarketPriceProviderRowDto {
|
||||
pub(crate) semantics: String,
|
||||
}
|
||||
|
||||
/// Bounded provider-neutral request used by the selected-provider refresh command.
|
||||
#[derive(Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_solprices_desk/market_price_runtime/MarketPriceRefreshManyRequestDto.ts")]
|
||||
pub(crate) struct MarketPriceRefreshManyRequestDto {
|
||||
/// Opaque provider identifiers requested by the frontend in deterministic row order.
|
||||
pub(crate) provider_ids: std::vec::Vec<String>,
|
||||
}
|
||||
|
||||
impl crate::MarketPriceRefreshManyRequestDto {
|
||||
/// Consumes the IPC DTO and returns the opaque provider identifiers for authoritative backend validation.
|
||||
pub(crate) fn into_provider_ids(self) -> std::vec::Vec<String> {
|
||||
return self.provider_ids;
|
||||
}
|
||||
}
|
||||
|
||||
/// Safe result of one selected/global refresh operation.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_solprices_desk/market_price_runtime/MarketPriceRefreshResultDto.ts")]
|
||||
pub(crate) struct MarketPriceRefreshResultDto {
|
||||
/// Number of provider refreshes that produced a new normalized observation.
|
||||
pub(crate) refreshed_count: u32,
|
||||
/// Number of providers included in the validated operation.
|
||||
pub(crate) requested_count: u32,
|
||||
/// Updated provider-neutral rows in the exact dispatch/outcome order.
|
||||
pub(crate) rows: std::vec::Vec<MarketPriceProviderRowDto>,
|
||||
}
|
||||
|
||||
/// Backend application runtime that owns the Config-selected Off-chain service and current in-memory presentation state.
|
||||
pub(crate) struct MarketPriceRuntime {
|
||||
presentation: std::sync::Mutex<MarketPricePresentationState>,
|
||||
@@ -128,7 +157,8 @@ impl crate::MarketPriceRuntime {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let loading = self.set_loading(&provider_id, true);
|
||||
let provider_ids = [provider_id.clone()];
|
||||
let loading = self.begin_refresh(provider_ids.as_slice());
|
||||
if let std::result::Result::Err(error) = loading {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
@@ -142,7 +172,7 @@ impl crate::MarketPriceRuntime {
|
||||
let outcome = match outcome {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
let cleared = self.set_loading(&provider_id, false);
|
||||
let cleared = self.clear_loading(provider_ids.as_slice());
|
||||
if let std::result::Result::Err(clear_error) = cleared {
|
||||
return std::result::Result::Err(clear_error);
|
||||
}
|
||||
@@ -152,7 +182,13 @@ impl crate::MarketPriceRuntime {
|
||||
let row = self.apply_outcome(&outcome);
|
||||
let row = match row {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
std::result::Result::Err(error) => {
|
||||
let cleared = self.clear_loading(provider_ids.as_slice());
|
||||
if let std::result::Result::Err(clear_error) = cleared {
|
||||
return std::result::Result::Err(clear_error);
|
||||
}
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
@@ -165,6 +201,101 @@ impl crate::MarketPriceRuntime {
|
||||
return std::result::Result::Ok(row);
|
||||
}
|
||||
|
||||
/// Refreshes a frontend-selected provider set in deterministic request order through the generic Off-chain service.
|
||||
pub(crate) async fn refresh_many(&self, provider_ids: std::vec::Vec<String>) -> ksp_core_lib::Result<crate::MarketPriceRefreshResultDto> {
|
||||
let provider_ids = parse_provider_ids(provider_ids);
|
||||
let provider_ids = match provider_ids {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let loading = self.begin_refresh(provider_ids.as_slice());
|
||||
if let std::result::Result::Err(error) = loading {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT,
|
||||
provider_count = provider_ids.len(),
|
||||
"started SOL Prices Desk selected-provider market-price refresh"
|
||||
);
|
||||
let outcomes = self.startup.resolved().service().refresh_many(provider_ids.as_slice()).await;
|
||||
let outcomes = match outcomes {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
let cleared = self.clear_loading(provider_ids.as_slice());
|
||||
if let std::result::Result::Err(clear_error) = cleared {
|
||||
return std::result::Result::Err(clear_error);
|
||||
}
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
let result = self.apply_outcomes(outcomes.as_slice());
|
||||
let result = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
let cleared = self.clear_loading(provider_ids.as_slice());
|
||||
if let std::result::Result::Err(clear_error) = cleared {
|
||||
return std::result::Result::Err(clear_error);
|
||||
}
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT,
|
||||
provider_count = result.requested_count,
|
||||
refreshed_count = result.refreshed_count,
|
||||
"completed SOL Prices Desk selected-provider market-price refresh"
|
||||
);
|
||||
return std::result::Result::Ok(result);
|
||||
}
|
||||
|
||||
/// Refreshes every configured provider through the generic Off-chain service in its stable provider order.
|
||||
pub(crate) async fn refresh_all(&self) -> ksp_core_lib::Result<crate::MarketPriceRefreshResultDto> {
|
||||
let registry = self.startup.resolved().service().registry();
|
||||
let provider_ids = registry.entries().iter().map(|entry| return entry.descriptor().id().clone()).collect::<std::vec::Vec<_>>();
|
||||
let loading = self.begin_refresh(provider_ids.as_slice());
|
||||
if let std::result::Result::Err(error) = loading {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT,
|
||||
provider_count = provider_ids.len(),
|
||||
"started SOL Prices Desk global market-price refresh"
|
||||
);
|
||||
let outcomes = self.startup.resolved().service().refresh_all().await;
|
||||
let outcomes = match outcomes {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
let cleared = self.clear_loading(provider_ids.as_slice());
|
||||
if let std::result::Result::Err(clear_error) = cleared {
|
||||
return std::result::Result::Err(clear_error);
|
||||
}
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
let result = self.apply_outcomes(outcomes.as_slice());
|
||||
let result = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
let cleared = self.clear_loading(provider_ids.as_slice());
|
||||
if let std::result::Result::Err(clear_error) = cleared {
|
||||
return std::result::Result::Err(clear_error);
|
||||
}
|
||||
return std::result::Result::Err(error);
|
||||
},
|
||||
};
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT,
|
||||
provider_count = result.requested_count,
|
||||
refreshed_count = result.refreshed_count,
|
||||
"completed SOL Prices Desk global market-price refresh"
|
||||
);
|
||||
return std::result::Result::Ok(result);
|
||||
}
|
||||
|
||||
fn apply_outcome(&self, outcome: &ksp_offchain_transport_lib::MarketPriceRefreshOutcome) -> ksp_core_lib::Result<crate::MarketPriceProviderRowDto> {
|
||||
let registry = self.startup.resolved().service().registry();
|
||||
let registry_entry = registry.entry(outcome.provider_id());
|
||||
@@ -196,13 +327,86 @@ impl crate::MarketPriceRuntime {
|
||||
};
|
||||
}
|
||||
|
||||
fn set_loading(&self, provider_id: &ksp_offchain_transport_lib::MarketPriceProviderId, loading: bool) -> ksp_core_lib::Result<()> {
|
||||
fn apply_outcomes(&self, outcomes: &[ksp_offchain_transport_lib::MarketPriceRefreshOutcome]) -> ksp_core_lib::Result<crate::MarketPriceRefreshResultDto> {
|
||||
let registry = self.startup.resolved().service().registry();
|
||||
let presentation = lock_presentation(&self.presentation);
|
||||
let mut presentation = match presentation {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return set_loading_in_state(&mut presentation, provider_id.as_str(), loading);
|
||||
let mut rows = std::vec::Vec::with_capacity(outcomes.len());
|
||||
let mut refreshed_count = 0usize;
|
||||
for outcome in outcomes {
|
||||
let registry_entry = registry.entry(outcome.provider_id());
|
||||
let registry_entry = match registry_entry {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_APP_STATE_INVALID,
|
||||
"SOL Prices Desk refresh outcome references an unknown registry provider",
|
||||
)
|
||||
.with_context("provider_id", outcome.provider_id().as_str()),
|
||||
);
|
||||
},
|
||||
};
|
||||
let applied = apply_outcome_to_state(&mut presentation, outcome);
|
||||
if let std::result::Result::Err(error) = applied {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if outcome.refreshed() {
|
||||
refreshed_count += 1;
|
||||
}
|
||||
let presentation_entry = presentation.entries.get(outcome.provider_id().as_str());
|
||||
let presentation_entry = match presentation_entry {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_APP_STATE_INVALID,
|
||||
"SOL Prices Desk market-price runtime is missing refreshed provider state",
|
||||
)
|
||||
.with_context("provider_id", outcome.provider_id().as_str()),
|
||||
);
|
||||
},
|
||||
};
|
||||
rows.push(project_registry_entry(registry_entry, presentation_entry));
|
||||
}
|
||||
let requested_count = count_to_u32(outcomes.len(), "refresh_requested_count");
|
||||
let requested_count = match requested_count {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let refreshed_count = count_to_u32(refreshed_count, "refresh_success_count");
|
||||
let refreshed_count = match refreshed_count {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::MarketPriceRefreshResultDto { refreshed_count, requested_count, rows });
|
||||
}
|
||||
|
||||
fn begin_refresh(&self, provider_ids: &[ksp_offchain_transport_lib::MarketPriceProviderId]) -> ksp_core_lib::Result<()> {
|
||||
let presentation = lock_presentation(&self.presentation);
|
||||
let mut presentation = match presentation {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return begin_refresh_in_state(&mut presentation, provider_ids);
|
||||
}
|
||||
|
||||
fn clear_loading(&self, provider_ids: &[ksp_offchain_transport_lib::MarketPriceProviderId]) -> ksp_core_lib::Result<()> {
|
||||
let presentation = lock_presentation(&self.presentation);
|
||||
let mut presentation = match presentation {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
for provider_id in provider_ids {
|
||||
let cleared = set_loading_in_state(&mut presentation, provider_id.as_str(), false);
|
||||
if let std::result::Result::Err(error) = cleared {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,6 +462,53 @@ fn availability_code(availability: ksp_offchain_transport_lib::MarketPriceProvid
|
||||
};
|
||||
}
|
||||
|
||||
fn begin_refresh_in_state(
|
||||
presentation: &mut MarketPricePresentationState,
|
||||
provider_ids: &[ksp_offchain_transport_lib::MarketPriceProviderId],
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
for provider_id in provider_ids {
|
||||
if !seen.insert(provider_id.as_str().to_owned()) {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_MARKET_PRICE_SELECTION_INVALID,
|
||||
"SOL Prices Desk market-price refresh selection contains a duplicate provider",
|
||||
)
|
||||
.with_context("provider_id", provider_id.as_str()),
|
||||
);
|
||||
}
|
||||
let entry = presentation.entries.get(provider_id.as_str());
|
||||
let entry = match entry {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_MARKET_PRICE_SELECTION_INVALID,
|
||||
"SOL Prices Desk market-price refresh selection contains an unknown provider",
|
||||
)
|
||||
.with_context("provider_id", provider_id.as_str()),
|
||||
);
|
||||
},
|
||||
};
|
||||
if entry.loading {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_MARKET_PRICE_REFRESH_CONFLICT,
|
||||
"SOL Prices Desk market-price provider already has a refresh in flight",
|
||||
)
|
||||
.with_context("provider_id", provider_id.as_str()),
|
||||
);
|
||||
}
|
||||
}
|
||||
for provider_id in provider_ids {
|
||||
let updated = set_loading_in_state(presentation, provider_id.as_str(), true);
|
||||
if let std::result::Result::Err(error) = updated {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn count_to_u32(value: usize, field: &'static str) -> ksp_core_lib::Result<u32> {
|
||||
let converted = u32::try_from(value);
|
||||
return match converted {
|
||||
@@ -282,6 +533,19 @@ fn lock_presentation(
|
||||
};
|
||||
}
|
||||
|
||||
fn parse_provider_ids(provider_ids: std::vec::Vec<String>) -> ksp_core_lib::Result<std::vec::Vec<ksp_offchain_transport_lib::MarketPriceProviderId>> {
|
||||
let mut parsed = std::vec::Vec::with_capacity(provider_ids.len());
|
||||
for provider_id in provider_ids {
|
||||
let provider_id = ksp_offchain_transport_lib::MarketPriceProviderId::new(provider_id);
|
||||
let provider_id = match provider_id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
parsed.push(provider_id);
|
||||
}
|
||||
return std::result::Result::Ok(parsed);
|
||||
}
|
||||
|
||||
fn project_registry_entry(
|
||||
entry: &ksp_offchain_transport_lib::MarketPriceProviderRegistryEntry,
|
||||
presentation: &MarketPricePresentationEntry,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-solprices-desk/src/tauri.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! Tauri runtime assembly for the KSP SOL prices desktop application.
|
||||
|
||||
@@ -76,7 +76,9 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
|
||||
emit_frontend_log,
|
||||
get_runtime_status,
|
||||
list_market_prices,
|
||||
refresh_all_market_prices,
|
||||
refresh_market_price,
|
||||
refresh_market_prices,
|
||||
splash_frontend_ready,
|
||||
]);
|
||||
}
|
||||
@@ -128,6 +130,17 @@ fn list_market_prices(
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn refresh_all_market_prices(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::MarketPriceRefreshResultDto, crate::CommandErrorDto> {
|
||||
let result = state.refresh_all_market_prices().await;
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn refresh_market_price(
|
||||
provider_id: String,
|
||||
@@ -140,6 +153,18 @@ async fn refresh_market_price(
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn refresh_market_prices(
|
||||
request: crate::MarketPriceRefreshManyRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::MarketPriceRefreshResultDto, crate::CommandErrorDto> {
|
||||
let result = state.refresh_market_prices(request).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn splash_frontend_ready(
|
||||
app: tauri::AppHandle,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "KSP SOL Prices Desk",
|
||||
"version": "0.2.12-pre.5.fix.1",
|
||||
"version": "0.2.12-pre.6",
|
||||
"identifier": "com.sasedev.ksp-app-solprices-desk",
|
||||
"build": {
|
||||
"beforeDevCommand": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-solprices-desk/tests/desktop_contract.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Desktop scaffold, shared-template and Config packaging contract audits for SOL Prices Desk `0.2.12`.
|
||||
|
||||
@@ -192,29 +192,21 @@ fn pre_002_fix_001_reuses_the_common_ksp_desktop_template() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_market_price_runtime_adds_single_refresh_without_many_or_all_controls() {
|
||||
fn pre_005_market_price_runtime_preserves_single_refresh_contract() {
|
||||
let root = app_root();
|
||||
let manifest = read_text(root.join("Cargo.toml").as_path());
|
||||
assert!(manifest.contains(r#"ksp-offchain-transport-lib = { path = "../ksp-offchain-transport-lib" }"#));
|
||||
assert!(!manifest.contains("reqwest"));
|
||||
let lib_rs = read_text(root.join("src/lib.rs").as_path());
|
||||
assert!(lib_rs.contains("mod market_price_runtime;"));
|
||||
assert!(lib_rs.contains("MarketPriceProviderRowDto"));
|
||||
assert!(lib_rs.contains("MarketPriceRuntime"));
|
||||
let runtime_rs = read_text(root.join("src/market_price_runtime.rs").as_path());
|
||||
assert!(runtime_rs.contains("pub(crate) async fn refresh_one"));
|
||||
assert!(runtime_rs.contains("service().refresh(&provider_id).await"));
|
||||
assert!(runtime_rs.contains("self.set_loading(&provider_id, true)"));
|
||||
assert!(runtime_rs.contains("self.begin_refresh(provider_ids.as_slice())"));
|
||||
assert!(runtime_rs.contains("apply_outcome_to_state"));
|
||||
let tauri_rs = read_text(root.join("src/tauri.rs").as_path());
|
||||
assert!(tauri_rs.contains("refresh_market_price"));
|
||||
for forbidden in ["refresh_market_prices", "refresh_all_market_prices"] {
|
||||
assert!(!tauri_rs.contains(forbidden));
|
||||
}
|
||||
let main_html = read_text(root.join("frontend/main.html").as_path());
|
||||
assert!(main_html.contains(r#"id="marketPriceTable""#));
|
||||
assert!(main_html.contains(">Action</th>"));
|
||||
assert!(main_html.contains("rafraîchie explicitement"));
|
||||
let main_typescript = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||
assert!(main_typescript.contains(r#""refresh_market_price""#));
|
||||
assert!(main_typescript.contains("SOL Prices Desk market-price refresh control clicked"));
|
||||
@@ -224,3 +216,33 @@ fn pre_005_market_price_runtime_adds_single_refresh_without_many_or_all_controls
|
||||
assert!(!main_typescript.contains("parseFloat("));
|
||||
assert!(!main_typescript.contains("Number(provider.price"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_market_price_runtime_adds_selected_and_global_refresh_without_polling() {
|
||||
let root = app_root();
|
||||
let lib_rs = read_text(root.join("src/lib.rs").as_path());
|
||||
assert!(lib_rs.contains("MarketPriceRefreshManyRequestDto"));
|
||||
assert!(lib_rs.contains("MarketPriceRefreshResultDto"));
|
||||
let runtime_rs = read_text(root.join("src/market_price_runtime.rs").as_path());
|
||||
assert!(runtime_rs.contains("pub(crate) async fn refresh_many"));
|
||||
assert!(runtime_rs.contains("service().refresh_many(provider_ids.as_slice()).await"));
|
||||
assert!(runtime_rs.contains("pub(crate) async fn refresh_all"));
|
||||
assert!(runtime_rs.contains("service().refresh_all().await"));
|
||||
assert!(runtime_rs.contains("begin_refresh_in_state"));
|
||||
let tauri_rs = read_text(root.join("src/tauri.rs").as_path());
|
||||
assert!(tauri_rs.contains("refresh_market_prices"));
|
||||
assert!(tauri_rs.contains("refresh_all_market_prices"));
|
||||
let main_html = read_text(root.join("frontend/main.html").as_path());
|
||||
assert!(main_html.contains(r#"id="refreshSelectedButton""#));
|
||||
assert!(main_html.contains(r#"id="refreshAllButton""#));
|
||||
assert!(main_html.contains(r#"id="selectedProviderCount""#));
|
||||
let main_typescript = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||
assert!(main_typescript.contains(r#""refresh_market_prices""#));
|
||||
assert!(main_typescript.contains(r#""refresh_all_market_prices""#));
|
||||
assert!(main_typescript.contains("selectedProviderIdsInRowOrder"));
|
||||
assert!(main_typescript.contains("SOL Prices Desk selected market-price refresh control clicked"));
|
||||
assert!(main_typescript.contains("SOL Prices Desk global market-price refresh control clicked"));
|
||||
for forbidden in ["setInterval(", "setTimeout(", "requestAnimationFrame("] {
|
||||
assert!(!main_typescript.contains(forbidden), "pre.006 must not add consumer scheduling: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-solprices-desk/tests/desktop_security.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Security, ownership and frontend instrumentation canaries for SOL Prices Desk.
|
||||
|
||||
@@ -69,7 +69,7 @@ fn pre_002_frontend_has_no_network_persistence_or_native_dialog_surface() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_tauri_commands_remain_centralized_and_only_single_refresh_is_present() {
|
||||
fn pre_005_tauri_commands_remain_centralized_and_single_refresh_is_preserved() {
|
||||
let root = app_root();
|
||||
let mut rust_files = std::vec::Vec::new();
|
||||
collect_files(root.join("src").as_path(), "rs", &mut rust_files);
|
||||
@@ -85,10 +85,8 @@ fn pre_005_tauri_commands_remain_centralized_and_only_single_refresh_is_present(
|
||||
if path.file_name().and_then(std::ffi::OsStr::to_str) != std::option::Option::Some("tauri.rs") {
|
||||
assert!(!source.contains("tauri::generate_handler!"), "{} registers Tauri handlers outside tauri.rs", path.display());
|
||||
}
|
||||
assert!(!source.contains("refresh_market_prices"), "{} advances multi-refresh before pre.006", path.display());
|
||||
assert!(!source.contains("refresh_all_market_prices"), "{} advances global refresh before pre.006", path.display());
|
||||
}
|
||||
assert_eq!(command_count, 5);
|
||||
assert!(command_count >= 5);
|
||||
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
||||
assert!(tauri.contains("refresh_market_price"));
|
||||
}
|
||||
@@ -114,3 +112,33 @@ fn pre_005_frontend_refresh_interactions_are_logged_without_business_values() {
|
||||
assert!(!main.contains(forbidden), "main frontend logging must not serialize business/secret values: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_selected_and_global_refresh_commands_are_centralized_and_bounded() {
|
||||
let root = app_root();
|
||||
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
||||
assert_eq!(tauri.matches("#[tauri::command]").count(), 7);
|
||||
assert!(tauri.contains("refresh_market_price"));
|
||||
assert!(tauri.contains("refresh_market_prices"));
|
||||
assert!(tauri.contains("refresh_all_market_prices"));
|
||||
let runtime = read_text(root.join("src/market_price_runtime.rs").as_path());
|
||||
assert!(runtime.contains("ERROR_CODE_MARKET_PRICE_REFRESH_CONFLICT"));
|
||||
assert!(runtime.contains("ERROR_CODE_MARKET_PRICE_SELECTION_INVALID"));
|
||||
assert!(runtime.contains("refresh_many(provider_ids.as_slice()).await"));
|
||||
assert!(runtime.contains("refresh_all().await"));
|
||||
assert!(!runtime.contains("tokio::time::sleep"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_frontend_batch_controls_are_logged_without_price_or_provider_payloads() {
|
||||
let root = app_root();
|
||||
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||
assert!(main.contains("SOL Prices Desk market-price selection control changed"));
|
||||
assert!(main.contains("SOL Prices Desk selected market-price refresh control clicked"));
|
||||
assert!(main.contains("SOL Prices Desk global market-price refresh control clicked"));
|
||||
assert!(main.contains("requestedCount: result.requestedCount"));
|
||||
assert!(main.contains("refreshedCount: result.refreshedCount"));
|
||||
for forbidden in ["JSON.stringify(result)", "JSON.stringify(currentMarketPriceRows)", "price: provider.price", "apiKey", "authorization"] {
|
||||
assert!(!main.contains(forbidden), "batch frontend logging must not serialize price/provider/secret values: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-solprices-desk/unit_tests/market_price_runtime.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
fn descriptor() -> std::option::Option<ksp_offchain_transport_lib::MarketPriceProviderDescriptor> {
|
||||
let provider_id = ksp_offchain_transport_lib::MarketPriceProviderId::new("test-provider");
|
||||
@@ -230,3 +230,51 @@ async fn non_refresh_outcome_preserves_last_successful_observation() -> ksp_core
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_loading_validation_rejects_duplicates_without_partial_state_mutation() -> ksp_core_lib::Result<()> {
|
||||
let provider_id = match ksp_offchain_transport_lib::MarketPriceProviderId::new("coinpaprika") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut entries = std::collections::BTreeMap::new();
|
||||
entries.insert(provider_id.as_str().to_owned(), super::MarketPricePresentationEntry { loading: false, observation: std::option::Option::None });
|
||||
let mut presentation = super::MarketPricePresentationState { entries };
|
||||
let result = super::begin_refresh_in_state(&mut presentation, &[provider_id.clone(), provider_id.clone()]);
|
||||
assert!(result.is_err());
|
||||
let entry = presentation.entries.get(provider_id.as_str());
|
||||
assert!(entry.is_some());
|
||||
if let std::option::Option::Some(entry) = entry {
|
||||
assert!(!entry.loading);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_loading_validation_rejects_in_flight_provider_without_marking_other_rows() -> ksp_core_lib::Result<()> {
|
||||
let coinpaprika = match ksp_offchain_transport_lib::MarketPriceProviderId::new("coinpaprika") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let kraken = match ksp_offchain_transport_lib::MarketPriceProviderId::new("kraken") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut entries = std::collections::BTreeMap::new();
|
||||
entries.insert(coinpaprika.as_str().to_owned(), super::MarketPricePresentationEntry { loading: false, observation: std::option::Option::None });
|
||||
entries.insert(kraken.as_str().to_owned(), super::MarketPricePresentationEntry { loading: true, observation: std::option::Option::None });
|
||||
let mut presentation = super::MarketPricePresentationState { entries };
|
||||
let result = super::begin_refresh_in_state(&mut presentation, &[coinpaprika.clone(), kraken.clone()]);
|
||||
assert!(result.is_err());
|
||||
let coinpaprika_entry = presentation.entries.get(coinpaprika.as_str());
|
||||
let kraken_entry = presentation.entries.get(kraken.as_str());
|
||||
assert!(coinpaprika_entry.is_some());
|
||||
assert!(kraken_entry.is_some());
|
||||
if let std::option::Option::Some(entry) = coinpaprika_entry {
|
||||
assert!(!entry.loading);
|
||||
}
|
||||
if let std::option::Option::Some(entry) = kraken_entry {
|
||||
assert!(entry.loading);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user