0.3.15-pre.011-fix.001
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/README.md -->
|
||||
<!-- version: 12 -->
|
||||
<!-- version: 13 -->
|
||||
|
||||
# `ksp-app-raw-transaction-ingest-desk`
|
||||
|
||||
@@ -48,7 +48,7 @@ http-block-polling
|
||||
|
||||
Les profils standard publics committés déclarent `Block` en plus de `Logs`; `standard-block-direct` est donc composable depuis Config sur Devnet, Mainnet et Testnet. Cette déclaration ne transforme pas `blockSubscribe` en méthode stable et ne remplace pas la revalidation runtime.
|
||||
|
||||
Une route `Configured` est uniquement **composable depuis Config**. La disponibilité réelle du Store et du Worker est revalidée lors du Start avant publication de l'accusé runtime. Le backend relaie désormais chaque Worker actif par un snapshot latest-value sûr : lifecycle, health, activity, admission/persistence, backpressure, reconnect/replay, continuité, gaps et repair. Les mises à jour sont émises via l'événement Tauri `ksp-raw-ingest-route-status` et peuvent être resynchronisées avec `get_route_monitoring`. Les compteurs et slots `u64` sont projetés en texte décimal afin de rester exacts côté JavaScript. Le frontend consomme maintenant ce flux en temps réel et via resynchronisation explicite : chaque route affiche lifecycle/health/activity, persisted/source/gap summaries et ouvre un détail complet pipeline/persistence, reconnect/replay, continuité, gaps et repair. Les événements monitoring mettent aussi à jour les états actifs/terminaux afin que le sélecteur réseau et le Store partagé suivent le lifecycle réellement publié par le Worker.
|
||||
Une route `Configured` est uniquement **composable depuis Config**. La disponibilité réelle du Store et du Worker est revalidée lors du Start avant publication de l'accusé runtime. Le backend relaie désormais chaque Worker actif par un snapshot latest-value sûr : lifecycle, health, activity, admission/persistence, backpressure, reconnect/replay, continuité, gaps et repair. Les mises à jour sont coalescées côté backend à une cadence bornée de `500 ms` maximum par route, émises via l'événement Tauri `ksp-raw-ingest-route-status` et peuvent être resynchronisées avec `get_route_monitoring`. Les compteurs et slots `u64` sont projetés en texte décimal afin de rester exacts côté JavaScript. Le frontend consomme maintenant ce flux en temps réel et via resynchronisation explicite : chaque route affiche lifecycle/health/activity, persisted/source/gap summaries et ouvre un détail complet pipeline/persistence, reconnect/replay, continuité, gaps et repair. Les événements monitoring mettent aussi à jour les états actifs/terminaux afin que le sélecteur réseau et le Store partagé suivent le lifecycle réellement publié par le Worker. Les snapshots steady-state patchent les champs de carte en place ; la reconstruction des contrôles Start/Stop est réservée aux changements de lifecycle, afin de préserver la réactivité des interactions.
|
||||
|
||||
Pour `yellowstone-hydrated`, la stratégie Mainnet n'effectue plus un `getTransaction` par notification transactionnelle. Le Desk construit un abonnement Yellowstone Block léger ; chaque bloc observé déclenche une reconciliation HTTP `getBlock` Full/Base64 sur le même réseau. Le profil `publicnode_mainnet` expose un pool HTTP logique `default` contenant PublicNode et le RPC public Solana comme fallback de même priorité. Le pool conserve les limites propres à chaque endpoint, son health/cooldown et son round-robin interne. La reconciliation Yellowstone tolère un décalage bref entre le gRPC et les RPC HTTP grâce à un retry borné et interruptible par Stop. Le chemin protobuf -> RAW direct reste différé tant que la canonicalisation exacte du meta n'est pas prouvée pour toutes les formes supportées.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-raw-transaction-ingest-desk/USAGE.md -->
|
||||
<!-- version: 11 -->
|
||||
<!-- version: 12 -->
|
||||
|
||||
# Utilisation de `ksp-app-raw-transaction-ingest-desk`
|
||||
|
||||
@@ -42,7 +42,7 @@ Pour une route `Configured`, choisir `confirmed` ou `finalized` puis utiliser `S
|
||||
|
||||
Plusieurs routes peuvent être actives simultanément lorsqu'elles appartiennent au même réseau logique. Chaque route conserve son Worker indépendant et son Stop ciblé. Le Store est partagé entre ces Workers ; le dernier Worker terminal déclenche la fermeture explicite du Store. Une route `Faulted` ne force pas l'arrêt des autres routes. Un Start d'un autre réseau est refusé tant que le Store partagé du réseau courant reste ouvert.
|
||||
|
||||
Le backend expose désormais un flux latest-value par route via l'événement Tauri `ksp-raw-ingest-route-status`. Chaque projection contient uniquement l'identité logique sûre de la route, lifecycle/health/activity, compteurs admission/persistence, backpressure, reconnect/replay, continuité, gaps et repair. La commande `get_route_monitoring` permet une resynchronisation explicite des routes actives et des derniers terminaux retenus dans la session runtime courante. Les séquences, slots et compteurs `u64` sont transmis en texte décimal pour préserver leur exactitude côté JavaScript. Le bouton `Resync monitoring` recharge explicitement les latest values backend ; les événements `ksp-raw-ingest-route-status` actualisent ensuite les cartes sans polling frontend. Une carte disposant d'un snapshot montre un résumé sûr (`health`, `activity`, persisted, sources actives, gaps) et le bouton `Supervision` ouvre le détail pipeline/persistence, sources, reconnect/replay, continuité et repair. Les derniers terminaux de la session restent consultables jusqu'à remplacement par un nouveau Start ou changement de session réseau.
|
||||
Le backend expose désormais un flux latest-value par route via l'événement Tauri `ksp-raw-ingest-route-status`. Chaque projection contient uniquement l'identité logique sûre de la route, lifecycle/health/activity, compteurs admission/persistence, backpressure, reconnect/replay, continuité, gaps et repair. La commande `get_route_monitoring` permet une resynchronisation explicite des routes actives et des derniers terminaux retenus dans la session runtime courante. Les séquences, slots et compteurs `u64` sont transmis en texte décimal pour préserver leur exactitude côté JavaScript. Le bouton `Resync monitoring` recharge explicitement les latest values backend ; les événements `ksp-raw-ingest-route-status` actualisent ensuite les cartes sans polling frontend. Le relais backend coalesce les changements à une cadence maximale d'une émission toutes les `500 ms` par route, et les changements steady-state mettent à jour la carte en place sans recréer les boutons Start/Stop. Une carte disposant d'un snapshot montre un résumé sûr (`health`, `activity`, persisted, sources actives, gaps) et le bouton `Supervision` ouvre le détail pipeline/persistence, sources, reconnect/replay, continuité et repair. Les derniers terminaux de la session restent consultables jusqu'à remplacement par un nouveau Start ou changement de session réseau.
|
||||
|
||||
Sur Mainnet, `yellowstone-hydrated` utilise un abonnement Yellowstone Block comme signal de temps réel puis reconcile chaque slot via `getBlock` Full/Base64. Cette stratégie évite l'hydration `getTransaction` unitaire par transaction. Le profil PublicNode peut exposer plusieurs endpoints HTTP sous le même rôle logique ; Transport distribue alors les requêtes selon ses règles de priorité, disponibilité, concurrence et cooldown. La conversion directe du payload Yellowstone vers le RAW canonique reste réservée à une évolution ultérieure tant que la parité complète du meta n'est pas prouvée.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/frontend/ts/main.ts
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
import "bootstrap";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
@@ -29,6 +29,7 @@ const viewTitles: Record<ViewId, string> = {
|
||||
const activeRuntimes = new Map<string, RawIngestRouteRuntimeDto>();
|
||||
const lastRuntimes = new Map<string, RawIngestRouteRuntimeDto>();
|
||||
const routeMonitoring = new Map<string, RawIngestRouteMonitoringDto>();
|
||||
const routeCards = new Map<string, HTMLElement>();
|
||||
let routeInventory: RawIngestRouteInventoryDto | null = null;
|
||||
let selectedProfileId: string | null = null;
|
||||
let selectedMonitoringKey: string | null = null;
|
||||
@@ -215,6 +216,7 @@ function renderSelectedProfile(): void {
|
||||
text("#headerProfile", profile.profileId);
|
||||
const routeRoot = document.querySelector<HTMLElement>("#routeFoundation");
|
||||
if (routeRoot) {
|
||||
routeCards.clear();
|
||||
routeRoot.replaceChildren();
|
||||
for (const route of profile.routes) {
|
||||
const key = runtimeKey(profile.profileId, route.routeId);
|
||||
@@ -246,6 +248,7 @@ function renderSelectedProfile(): void {
|
||||
meta.className = "d-flex flex-wrap gap-2 mt-3 app-route-meta";
|
||||
const state = document.createElement("span");
|
||||
state.className = lifecycleBadgeClass(projectedState);
|
||||
state.dataset.monitoringField = "state";
|
||||
state.textContent = projectedState;
|
||||
const network = document.createElement("span");
|
||||
network.className = "badge text-bg-light border text-dark";
|
||||
@@ -260,18 +263,23 @@ function renderSelectedProfile(): void {
|
||||
summary.className = "app-route-monitoring-summary mt-3";
|
||||
const health = document.createElement("span");
|
||||
health.className = monitoring.health === "healthy" ? "badge text-bg-success" : "badge text-bg-warning";
|
||||
health.dataset.monitoringField = "health";
|
||||
health.textContent = `health: ${monitoring.health}`;
|
||||
const activity = document.createElement("span");
|
||||
activity.className = "badge text-bg-light border text-dark";
|
||||
activity.dataset.monitoringField = "activity";
|
||||
activity.textContent = `activity: ${monitoring.activity}`;
|
||||
const persisted = document.createElement("span");
|
||||
persisted.className = "badge text-bg-light border text-dark";
|
||||
persisted.dataset.monitoringField = "persisted";
|
||||
persisted.textContent = `persisted: ${monitoring.persistedTotal}`;
|
||||
const sources = document.createElement("span");
|
||||
sources.className = "badge text-bg-light border text-dark";
|
||||
sources.dataset.monitoringField = "sources";
|
||||
sources.textContent = `sources: ${monitoring.sourceActive}/${monitoring.sourceTotal}`;
|
||||
const gaps = document.createElement("span");
|
||||
gaps.className = monitoring.openGapCount === 0 ? "badge text-bg-light border text-dark" : "badge text-bg-warning";
|
||||
gaps.dataset.monitoringField = "gaps";
|
||||
gaps.textContent = `gaps: ${monitoring.openGapCount}`;
|
||||
summary.append(health, activity, persisted, sources, gaps);
|
||||
body.append(summary);
|
||||
@@ -324,6 +332,7 @@ function renderSelectedProfile(): void {
|
||||
body.append(actions);
|
||||
}
|
||||
card.append(body);
|
||||
routeCards.set(key, card);
|
||||
column.append(card);
|
||||
routeRoot.append(column);
|
||||
}
|
||||
@@ -450,6 +459,34 @@ function renderMonitoringDetail(): void {
|
||||
});
|
||||
}
|
||||
|
||||
function renderRouteMonitoringCard(status: RawIngestRouteMonitoringDto): boolean {
|
||||
const key = runtimeKey(status.profileId, status.routeId);
|
||||
const card = routeCards.get(key);
|
||||
if (!card) {
|
||||
return false;
|
||||
}
|
||||
const field = (name: string): HTMLElement | null => card.querySelector<HTMLElement>(`[data-monitoring-field="${name}"]`);
|
||||
const state = field("state");
|
||||
const health = field("health");
|
||||
const activity = field("activity");
|
||||
const persisted = field("persisted");
|
||||
const sources = field("sources");
|
||||
const gaps = field("gaps");
|
||||
if (!state || !health || !activity || !persisted || !sources || !gaps) {
|
||||
return false;
|
||||
}
|
||||
state.className = lifecycleBadgeClass(status.state);
|
||||
state.textContent = status.state;
|
||||
health.className = status.health === "healthy" ? "badge text-bg-success" : "badge text-bg-warning";
|
||||
health.textContent = `health: ${status.health}`;
|
||||
activity.textContent = `activity: ${status.activity}`;
|
||||
persisted.textContent = `persisted: ${status.persistedTotal}`;
|
||||
sources.textContent = `sources: ${status.sourceActive}/${status.sourceTotal}`;
|
||||
gaps.className = status.openGapCount === 0 ? "badge text-bg-light border text-dark" : "badge text-bg-warning";
|
||||
gaps.textContent = `gaps: ${status.openGapCount}`;
|
||||
return true;
|
||||
}
|
||||
|
||||
function selectedCommitment(): RawIngestCommitment {
|
||||
const selector = document.querySelector<HTMLSelectElement>("#routeCommitment");
|
||||
return selector?.value === "finalized" ? "finalized" : "confirmed";
|
||||
@@ -548,13 +585,21 @@ async function stopRoute(runtime: RawIngestRouteRuntimeDto): Promise<void> {
|
||||
}
|
||||
|
||||
function applyRouteMonitoring(status: RawIngestRouteMonitoringDto, source: "event" | "startup" | "start" | "stop" | "user"): void {
|
||||
const key = runtimeKey(status.profileId, status.routeId);
|
||||
const previous = routeMonitoring.get(key) ?? null;
|
||||
recordRouteMonitoring(status);
|
||||
if (selectedMonitoringKey === null && status.profileId === selectedProfileId) {
|
||||
selectedMonitoringKey = runtimeKey(status.profileId, status.routeId);
|
||||
selectedMonitoringKey = key;
|
||||
}
|
||||
renderMultiRouteRuntime();
|
||||
renderSelectedProfile();
|
||||
const lifecycleChanged = previous === null || previous.state !== status.state;
|
||||
if (lifecycleChanged || !renderRouteMonitoringCard(status)) {
|
||||
renderSelectedProfile();
|
||||
} else if (selectedMonitoringKey === key) {
|
||||
renderMonitoringDetail();
|
||||
}
|
||||
frontendTrace("main", "Raw Transaction Ingest Desk route monitoring latest value applied", {
|
||||
lifecycleChanged,
|
||||
routeId: status.routeId,
|
||||
sequence: status.sequence,
|
||||
source,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/src/tauri.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
//! Tauri runtime assembly for the KSP Raw Transaction Ingest desktop application.
|
||||
|
||||
@@ -186,6 +186,8 @@ async fn start_route(
|
||||
return std::result::Result::Ok(acknowledgement);
|
||||
}
|
||||
|
||||
const ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS: u64 = 500;
|
||||
|
||||
async fn monitor_route_status(
|
||||
app: tauri::AppHandle,
|
||||
runtime: crate::RawIngestRouteRuntimeDto,
|
||||
@@ -223,6 +225,10 @@ async fn monitor_route_status(
|
||||
}
|
||||
let observed = snapshot.worker_snapshot().sequence();
|
||||
snapshot = source.wait_for_change(observed).await;
|
||||
if !snapshot.worker_snapshot().state().is_terminal() {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS)).await;
|
||||
snapshot = source.current();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/desktop_contract.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
//! Desktop scaffold contract canaries for Raw Transaction Ingest Desk.
|
||||
|
||||
@@ -253,3 +253,24 @@ fn pre_011_frontend_consumes_latest_value_monitoring_and_renders_full_route_supe
|
||||
assert!(styles.contains(required), "missing pre.011 bounded supervision style {required}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_011_fix_001_monitoring_bridge_is_bounded_and_steady_updates_preserve_route_controls() {
|
||||
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
|
||||
for required in [
|
||||
"ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS: u64 = 500",
|
||||
"tokio::time::sleep(std::time::Duration::from_millis(ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS)).await",
|
||||
"snapshot = source.current()",
|
||||
] {
|
||||
assert!(tauri.contains(required), "missing pre.011-fix.001 bounded relay marker {required}");
|
||||
}
|
||||
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
|
||||
for required in [
|
||||
"const routeCards = new Map<string, HTMLElement>()",
|
||||
"renderRouteMonitoringCard",
|
||||
"const lifecycleChanged = previous === null || previous.state !== status.state",
|
||||
"if (lifecycleChanged || !renderRouteMonitoringCard(status))",
|
||||
] {
|
||||
assert!(main.contains(required), "missing pre.011-fix.001 stable-control marker {required}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-raw-transaction-ingest-desk/tests/release_completeness.rs
|
||||
// version: 10
|
||||
// version: 11
|
||||
|
||||
//! Release-completeness canaries for Raw Transaction Ingest Desk Start-resource reconstruction.
|
||||
|
||||
@@ -188,3 +188,15 @@ fn pre_011_frontend_supervision_closes_planned_ui_without_backend_module_growth(
|
||||
assert!(html.contains(required), "missing pre.011 operator supervision surface {required}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_011_fix_001_preserves_latest_value_contract_without_backend_module_growth() {
|
||||
let lib = read_text(app_root().join("src/lib.rs").as_path());
|
||||
assert_eq!(lib.matches("mod ").count(), 16);
|
||||
let tauri = read_text(app_root().join("src/tauri.rs").as_path());
|
||||
assert!(tauri.contains("source.wait_for_change(observed).await"));
|
||||
assert!(tauri.contains("ROUTE_MONITORING_MIN_EMIT_INTERVAL_MS: u64 = 500"));
|
||||
let main = read_text(app_root().join("frontend/ts/main.ts").as_path());
|
||||
assert!(main.contains("renderRouteMonitoringCard"));
|
||||
assert!(main.contains("renderSelectedProfile"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user