diff --git a/CHANGELOG.md b/CHANGELOG.md index 2acc6a0..3dd04aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,3 +10,4 @@ 0.3.2 - Ajout des helpers typed et du parsing typed basé sur solana-rpc-client-api 0.3.3 - Ajout du suffixe _raw aux helpers raw pour distinguer typed et raw 0.3.4 - Ajout de la fenêtre Demo Ws dans kb_app pour tester les souscriptions live +0.3.5 - Stabilisation de Demo Ws, lecture correcte des endpoints activés depuis la config, limitation/throttling de l’affichage UI sous fort débit diff --git a/Cargo.toml b/Cargo.toml index 04e0dbb..8bc7147 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "0.3.4" +version = "0.3.5" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot" diff --git a/kb_app/frontend/demo_ws.html b/kb_app/frontend/demo_ws.html index 37836d0..86e743b 100644 --- a/kb_app/frontend/demo_ws.html +++ b/kb_app/frontend/demo_ws.html @@ -43,9 +43,14 @@

Connexion

- - -
+ + +
+ Seuls les endpoints définis dans config.solana.ws_endpoints et marqués + enabled: true apparaissent ici. Les endpoints HTTP ne sont pas utilisés + par cette fenêtre. +
+
@@ -56,6 +61,14 @@
State: Disconnected
Endpoint: -
+ +
+
Events: 0
+
Notifications: 0
+
UI logs: 0
+
Suppressed: 0
+
Last event: -
+

diff --git a/kb_app/frontend/ts/demo_ws.ts b/kb_app/frontend/ts/demo_ws.ts index 58ef059..07c12c4 100644 --- a/kb_app/frontend/ts/demo_ws.ts +++ b/kb_app/frontend/ts/demo_ws.ts @@ -26,6 +26,11 @@ interface DemoWsStatusPayload { currentSubscribeMethod: string | null; currentUnsubscribeMethod: string | null; currentNotificationMethod: string | null; + eventCountTotal: number; + notificationCountTotal: number; + uiLogCount: number; + suppressedLogCount: number; + lastEventKind: string | null; } interface DemoWsSubscribeRequest { @@ -195,6 +200,11 @@ function applyStatusToUi( stateText: HTMLSpanElement, endpointText: HTMLSpanElement, subscriptionText: HTMLSpanElement, + eventCountText: HTMLSpanElement, + notificationCountText: HTMLSpanElement, + uiLogCountText: HTMLSpanElement, + suppressedLogCountText: HTMLSpanElement, + lastEventKindText: HTMLSpanElement, connectButton: HTMLButtonElement, disconnectButton: HTMLButtonElement, subscribeButton: HTMLButtonElement, @@ -213,6 +223,12 @@ function applyStatusToUi( subscriptionText.textContent = "-"; } + eventCountText.textContent = String(status.eventCountTotal); + notificationCountText.textContent = String(status.notificationCountTotal); + uiLogCountText.textContent = String(status.uiLogCount); + suppressedLogCountText.textContent = String(status.suppressedLogCount); + lastEventKindText.textContent = status.lastEventKind ?? "-"; + const isConnected = status.connectionState === "Connected"; const isBusy = status.connectionState === "Connecting" || status.connectionState === "Disconnecting"; @@ -273,6 +289,11 @@ document.addEventListener("DOMContentLoaded", async () => { const endpointText = document.querySelector("#demoWsEndpointText"); const subscriptionText = document.querySelector("#demoWsSubscriptionText"); const requestText = document.querySelector("#demoWsRequestText"); + const eventCountText = document.querySelector("#demoWsEventCountText"); +const notificationCountText = document.querySelector("#demoWsNotificationCountText"); +const uiLogCountText = document.querySelector("#demoWsUiLogCountText"); +const suppressedLogCountText = document.querySelector("#demoWsSuppressedLogCountText"); +const lastEventKindText = document.querySelector("#demoWsLastEventKindText"); const connectButton = document.querySelector("#demoWsConnectButton"); const disconnectButton = document.querySelector("#demoWsDisconnectButton"); const subscribeButton = document.querySelector("#demoWsSubscribeButton"); @@ -281,6 +302,7 @@ document.addEventListener("DOMContentLoaded", async () => { const logTextarea = document.querySelector("#demoWsLogTextarea"); if ( + !eventCountText || !notificationCountText || !uiLogCountText || !suppressedLogCountText || !lastEventKindText || !endpointSelect || !methodSelect || !modeSelect || !targetGroup || !targetLabel || !targetInput || !filterGroup || !filterTextarea || !configGroup || !configTextarea || !statusBadge || !stateText || !endpointText || !subscriptionText || !requestText || !connectButton || @@ -299,18 +321,23 @@ document.addEventListener("DOMContentLoaded", async () => { }); unlistenStatusEvent = await listen("demo-ws-status", (event) => { - applyStatusToUi( - event.payload, - statusBadge, - stateText, - endpointText, - subscriptionText, - connectButton, - disconnectButton, - subscribeButton, - unsubscribeButton, - ); - }); + applyStatusToUi( + event.payload, + statusBadge, + stateText, + endpointText, + subscriptionText, + eventCountText, + notificationCountText, + uiLogCountText, + suppressedLogCountText, + lastEventKindText, + connectButton, + disconnectButton, + subscribeButton, + unsubscribeButton, + ); +}); } catch (error) { appendLogLine(logTextarea, `[ui] event listen error: ${String(error)}`); } @@ -373,16 +400,21 @@ document.addEventListener("DOMContentLoaded", async () => { try { const status = await invoke("demo_ws_get_status"); applyStatusToUi( - status, - statusBadge, - stateText, - endpointText, - subscriptionText, - connectButton, - disconnectButton, - subscribeButton, - unsubscribeButton, - ); + status, + statusBadge, + stateText, + endpointText, + subscriptionText, + eventCountText, + notificationCountText, + uiLogCountText, + suppressedLogCountText, + lastEventKindText, + connectButton, + disconnectButton, + subscribeButton, + unsubscribeButton, +); } catch (error) { appendLogLine(logTextarea, `[ui] initial status error: ${String(error)}`); } @@ -396,16 +428,21 @@ document.addEventListener("DOMContentLoaded", async () => { }); applyStatusToUi( - status, - statusBadge, - stateText, - endpointText, - subscriptionText, - connectButton, - disconnectButton, - subscribeButton, - unsubscribeButton, - ); + status, + statusBadge, + stateText, + endpointText, + subscriptionText, + eventCountText, + notificationCountText, + uiLogCountText, + suppressedLogCountText, + lastEventKindText, + connectButton, + disconnectButton, + subscribeButton, + unsubscribeButton, +); } catch (error) { appendLogLine(logTextarea, `[ui] connect error: ${String(error)}`); } @@ -416,16 +453,21 @@ document.addEventListener("DOMContentLoaded", async () => { const status = await invoke("demo_ws_disconnect"); applyStatusToUi( - status, - statusBadge, - stateText, - endpointText, - subscriptionText, - connectButton, - disconnectButton, - subscribeButton, - unsubscribeButton, - ); + status, + statusBadge, + stateText, + endpointText, + subscriptionText, + eventCountText, + notificationCountText, + uiLogCountText, + suppressedLogCountText, + lastEventKindText, + connectButton, + disconnectButton, + subscribeButton, + unsubscribeButton, +); } catch (error) { appendLogLine(logTextarea, `[ui] disconnect error: ${String(error)}`); } diff --git a/kb_app/package.json b/kb_app/package.json index b01bb77..a74bb9f 100644 --- a/kb_app/package.json +++ b/kb_app/package.json @@ -1,7 +1,7 @@ { "name": "kb-app", "private": true, - "version": "0.3.0", + "version": "0.3.5", "type": "module", "scripts": { "dev": "vite", diff --git a/kb_app/src/demo_ws.rs b/kb_app/src/demo_ws.rs index 96e38c7..0368893 100644 --- a/kb_app/src/demo_ws.rs +++ b/kb_app/src/demo_ws.rs @@ -30,6 +30,11 @@ pub(crate) struct KbDemoWsStatusPayload { current_subscribe_method: std::option::Option, current_unsubscribe_method: std::option::Option, current_notification_method: std::option::Option, + event_count_total: u64, + notification_count_total: u64, + ui_log_count: u64, + suppressed_log_count: u64, + last_event_kind: std::option::Option, } /// Subscribe request sent by the demo frontend. @@ -53,6 +58,12 @@ pub(crate) struct KbDemoWsRuntimeState { endpoint_url: std::option::Option, connection_state: kb_lib::KbConnectionState, current_subscription: std::option::Option, + event_count_total: u64, + notification_count_total: u64, + ui_log_count: u64, + suppressed_log_count: u64, + last_event_kind: std::option::Option, + last_status_emit_at: std::option::Option, } impl KbDemoWsRuntimeState { @@ -66,6 +77,12 @@ impl KbDemoWsRuntimeState { endpoint_url: None, connection_state: kb_lib::KbConnectionState::Disconnected, current_subscription: None, + event_count_total: 0, + notification_count_total: 0, + ui_log_count: 0, + suppressed_log_count: 0, + last_event_kind: None, + last_status_emit_at: None, } } @@ -94,6 +111,11 @@ impl KbDemoWsRuntimeState { current_subscribe_method, current_unsubscribe_method, current_notification_method, + event_count_total: self.event_count_total, + notification_count_total: self.notification_count_total, + ui_log_count: self.ui_log_count, + suppressed_log_count: self.suppressed_log_count, + last_event_kind: self.last_event_kind.clone(), } } @@ -105,6 +127,12 @@ impl KbDemoWsRuntimeState { self.endpoint_url = None; self.connection_state = kb_lib::KbConnectionState::Disconnected; self.current_subscription = None; + self.event_count_total = 0; + self.notification_count_total = 0; + self.ui_log_count = 0; + self.suppressed_log_count = 0; + self.last_event_kind = None; + self.last_status_emit_at = None; } } @@ -154,10 +182,20 @@ pub(crate) async fn demo_ws_list_endpoints( ) -> Result, std::string::String> { let mut endpoints = std::vec::Vec::new(); for endpoint in &state.config.solana.ws_endpoints { + if !endpoint.enabled { + continue; + } let resolved_url_result = endpoint.resolved_url(); let resolved_url = match resolved_url_result { Ok(resolved_url) => resolved_url, - Err(_) => endpoint.url.clone(), + Err(error) => { + tracing::warn!( + endpoint_name = %endpoint.name, + "cannot resolve ws endpoint url from environment: {}", + error + ); + format!("UNRESOLVED_ENV [{}] {}", endpoint.name, endpoint.url) + } }; endpoints.push(KbDemoWsEndpointSummary { name: endpoint.name.clone(), @@ -231,9 +269,14 @@ pub(crate) async fn demo_ws_connect( let recv_result = event_receiver.recv().await; match recv_result { Ok(event) => { - kb_apply_demo_ws_event_to_runtime(&relay_runtime, &event).await; - kb_emit_demo_ws_log(&relay_app_handle, &kb_format_demo_ws_event(&event)); - kb_emit_demo_ws_status(&relay_app_handle, &relay_runtime).await; + let (emit_ui_log, emit_ui_status) = + kb_register_demo_ws_event_and_decide_emission(&relay_runtime, &event).await; + if emit_ui_log { + kb_emit_demo_ws_log(&relay_app_handle, &kb_format_demo_ws_event(&event)); + } + if emit_ui_status { + kb_emit_demo_ws_status(&relay_app_handle, &relay_runtime).await; + } } Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => { kb_emit_demo_ws_log( @@ -306,10 +349,7 @@ pub(crate) async fn demo_ws_disconnect( if let Some(client) = &client_option { let disconnect_result = client.disconnect().await; if let Err(error) = disconnect_result { - kb_emit_demo_ws_log( - &app_handle, - &format!("[demo] disconnect error: {}", error), - ); + kb_emit_demo_ws_log(&app_handle, &format!("[demo] disconnect error: {}", error)); } } if let Some(relay_task) = relay_task_option { @@ -581,11 +621,22 @@ where } } -async fn kb_apply_demo_ws_event_to_runtime( +async fn kb_register_demo_ws_event_and_decide_emission( runtime_arc: &std::sync::Arc>, event: &kb_lib::WsEvent, -) { +) -> (bool, bool) { let mut runtime_guard = runtime_arc.lock().await; + runtime_guard.event_count_total = runtime_guard.event_count_total.saturating_add(1); + runtime_guard.last_event_kind = Some(kb_demo_ws_event_kind_name(event).to_string()); + let mut emit_ui_log = true; + let force_status_emit = matches!( + event, + kb_lib::WsEvent::Connected { .. } + | kb_lib::WsEvent::Disconnected { .. } + | kb_lib::WsEvent::SubscriptionRegistered { .. } + | kb_lib::WsEvent::SubscriptionUnregistered { .. } + | kb_lib::WsEvent::Error { .. } + ); match event { kb_lib::WsEvent::Connected { endpoint_name, @@ -597,6 +648,35 @@ async fn kb_apply_demo_ws_event_to_runtime( } kb_lib::WsEvent::SubscriptionRegistered { subscription, .. } => { runtime_guard.current_subscription = Some(subscription.clone()); + runtime_guard.notification_count_total = 0; + } + kb_lib::WsEvent::SubscriptionNotification { subscription, .. } => { + runtime_guard.notification_count_total = + runtime_guard.notification_count_total.saturating_add(1); + let subscribe_method = subscription.subscribe_method.as_str(); + let notif_count = runtime_guard.notification_count_total; + if subscribe_method == "logsSubscribe" || subscribe_method == "programSubscribe" { + emit_ui_log = notif_count % 100 == 1; + } else if subscribe_method == "slotsUpdatesSubscribe" { + emit_ui_log = notif_count % 20 == 1; + } + } + kb_lib::WsEvent::TextMessage { .. } | kb_lib::WsEvent::JsonRpcMessage { .. } => { + let subscribe_method_option = runtime_guard + .current_subscription + .as_ref() + .map(|subscription| subscription.subscribe_method.as_str()); + if let Some(subscribe_method) = subscribe_method_option { + if subscribe_method == "logsSubscribe" + || subscribe_method == "programSubscribe" + || subscribe_method == "slotsUpdatesSubscribe" + { + emit_ui_log = false; + } + } + } + kb_lib::WsEvent::Pong { .. } => { + emit_ui_log = false; } kb_lib::WsEvent::SubscriptionUnregistered { subscription_id, .. @@ -605,9 +685,9 @@ async fn kb_apply_demo_ws_event_to_runtime( .current_subscription .as_ref() .map(|subscription| subscription.subscription_id); - if current_subscription_id == Some(*subscription_id) { runtime_guard.current_subscription = None; + runtime_guard.notification_count_total = 0; } } kb_lib::WsEvent::Disconnected { .. } => { @@ -616,9 +696,30 @@ async fn kb_apply_demo_ws_event_to_runtime( runtime_guard.keepalive_task = None; runtime_guard.connection_state = kb_lib::KbConnectionState::Disconnected; runtime_guard.current_subscription = None; + runtime_guard.notification_count_total = 0; } _ => {} } + if emit_ui_log { + runtime_guard.ui_log_count = runtime_guard.ui_log_count.saturating_add(1); + } else { + runtime_guard.suppressed_log_count = runtime_guard.suppressed_log_count.saturating_add(1); + } + let now = std::time::Instant::now(); + let emit_ui_status = if force_status_emit { + true + } else { + match runtime_guard.last_status_emit_at { + Some(last_status_emit_at) => { + now.duration_since(last_status_emit_at) >= std::time::Duration::from_millis(250) + } + None => true, + } + }; + if emit_ui_status { + runtime_guard.last_status_emit_at = Some(now); + } + (emit_ui_log, emit_ui_status) } async fn kb_emit_demo_ws_status( @@ -846,3 +947,22 @@ async fn kb_demo_ws_keepalive_loop(app_handle: &tauri::AppHandle, client: &kb_li } } } + +fn kb_demo_ws_event_kind_name(event: &kb_lib::WsEvent) -> &'static str { + match event { + kb_lib::WsEvent::Connected { .. } => "connected", + kb_lib::WsEvent::TextMessage { .. } => "text_message", + kb_lib::WsEvent::JsonRpcMessage { .. } => "json_rpc_message", + kb_lib::WsEvent::JsonRpcParseError { .. } => "json_rpc_parse_error", + kb_lib::WsEvent::SubscriptionRegistered { .. } => "subscription_registered", + kb_lib::WsEvent::SubscriptionNotification { .. } => "subscription_notification", + kb_lib::WsEvent::JsonRpcNotificationWithoutSubscription { .. } => "untracked_notification", + kb_lib::WsEvent::SubscriptionUnregistered { .. } => "subscription_unregistered", + kb_lib::WsEvent::BinaryMessage { .. } => "binary_message", + kb_lib::WsEvent::Ping { .. } => "ping", + kb_lib::WsEvent::Pong { .. } => "pong", + kb_lib::WsEvent::CloseReceived { .. } => "close_received", + kb_lib::WsEvent::Disconnected { .. } => "disconnected", + kb_lib::WsEvent::Error { .. } => "error", + } +} diff --git a/kb_app/tauri.conf.json b/kb_app/tauri.conf.json index 59896c9..c3b96e0 100644 --- a/kb_app/tauri.conf.json +++ b/kb_app/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "kb-bapp", - "version": "0.3.0", + "version": "0.3.5", "identifier": "com.sasedev.kb-app", "build": { "beforeDevCommand": "npm run dev",