This commit is contained in:
2026-04-25 18:10:40 +02:00
parent f90ca40202
commit b034fdf1c4
16 changed files with 1088 additions and 52 deletions

View File

@@ -29,3 +29,4 @@
0.6.3 - Enrichissement des notifications WebSocket utiles : extraction améliorée de pubkey, signature, owner, parsed account type et slot pour account/logs/signature notifications 0.6.3 - Enrichissement des notifications WebSocket utiles : extraction améliorée de pubkey, signature, owner, parsed account type et slot pour account/logs/signature notifications
0.6.4 - Premières règles de détection technique pour candidats pools/listings depuis programNotification en sappuyant sur les DEX connus en base 0.6.4 - Premières règles de détection technique pour candidats pools/listings depuis programNotification en sappuyant sur les DEX connus en base
0.6.5 - Ajout de ws_manager.rs pour lorchestration multi-clients WebSocket, le bus dévénements unifié et le branchement centralisé du relais de détection 0.6.5 - Ajout de ws_manager.rs pour lorchestration multi-clients WebSocket, le bus dévénements unifié et le branchement centralisé du relais de détection
0.6.6 - Ajout de la fenêtre Demo Ws Manager dans kb_app pour piloter plusieurs WsClient, visualiser le snapshot consolidé, tester le démarrage/arrêt par rôle et valider le flux unifié de WsEvent

View File

@@ -8,7 +8,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.6.5" version = "0.6.6"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot" repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot"

View File

@@ -436,7 +436,18 @@ Réalisé :
- branchement optionnel du relais de détection WS sur tous les clients orchestrés, - branchement optionnel du relais de détection WS sur tous les clients orchestrés,
- préparation des futures politiques de répartition, supervision et reconnexion. - préparation des futures politiques de répartition, supervision et reconnexion.
### 6.031. Version `0.7.0` — Résolution transactionnelle orientée DEX ### 6.031. Version `0.6.6` — Démo légère `WsManager` dans `kb_app`
Réalisé :
- ajout dune fenêtre `Demo Ws Manager` dans `kb_app`,
- ouverture depuis la fenêtre principale,
- affichage du snapshot consolidé du `WsManager`,
- pilotage des endpoints WS gérés via `start/stop all` et `start/stop role`,
- visualisation du flux unifié de `WsEvent`,
- validation UI du branchement centralisé du relais de détection,
- amélioration des messages de log UI pour les actions idempotentes déjà démarrées ou déjà arrêtées.
### 6.032. Version `0.7.0` — Résolution transactionnelle orientée DEX
Objectif : relier la détection temps réel aux transactions Solana complètes. Objectif : relier la détection temps réel aux transactions Solana complètes.
À faire : À faire :
@@ -446,7 +457,7 @@ Objectif : relier la détection temps réel aux transactions Solana complètes.
- utiliser le pool HTTP existant pour enrichir les signaux détectés côté WS, - utiliser le pool HTTP existant pour enrichir les signaux détectés côté WS,
- éviter quune notification intéressante reste au niveau dun simple signal technique sans résolution métier. - éviter quune notification intéressante reste au niveau dun simple signal technique sans résolution métier.
### 6.032. Version `0.7.1` — Modèle transactionnel Solana enrichi ### 6.033. Version `0.7.1` — Modèle transactionnel Solana enrichi
Objectif : préparer un modèle interne plus riche, inspiré dune vision `slot -> signature -> instruction`. Objectif : préparer un modèle interne plus riche, inspiré dune vision `slot -> signature -> instruction`.
À faire : À faire :
@@ -456,7 +467,7 @@ Objectif : préparer un modèle interne plus riche, inspiré dune vision `slo
- conserver la possibilité de relier plus tard un pool, un token ou un wallet à une signature fondatrice, - conserver la possibilité de relier plus tard un pool, un token ou un wallet à une signature fondatrice,
- préparer lhistorique transactionnel nécessaire aux futurs décodeurs DEX. - préparer lhistorique transactionnel nécessaire aux futurs décodeurs DEX.
### 6.033. Version `0.7.2` — Décodeurs DEX spécifiques par programme et version ### 6.034. Version `0.7.2` — Décodeurs DEX spécifiques par programme et version
Objectif : remplacer les heuristiques ponctuelles par de vrais décodeurs Rust dédiés. Objectif : remplacer les heuristiques ponctuelles par de vrais décodeurs Rust dédiés.
À faire : À faire :
@@ -466,7 +477,7 @@ Objectif : remplacer les heuristiques ponctuelles par de vrais décodeurs Rust d
- encapsuler les index de comptes et les motifs de logs propres à chaque protocole, - encapsuler les index de comptes et les motifs de logs propres à chaque protocole,
- prévoir des décodeurs séparés au minimum pour Raydium, Pump.fun / PumpSwap, Meteora, puis les autres cibles. - prévoir des décodeurs séparés au minimum pour Raydium, Pump.fun / PumpSwap, Meteora, puis les autres cibles.
### 6.034. Version `0.7.3` — Détection des nouveaux pools et paires via logs + transaction ### 6.035. Version `0.7.3` — Détection des nouveaux pools et paires via logs + transaction
Objectif : détecter rapidement les nouvelles paires/pools à partir des flux RPC et des transactions enrichies. Objectif : détecter rapidement les nouvelles paires/pools à partir des flux RPC et des transactions enrichies.
À faire : À faire :
@@ -476,7 +487,7 @@ Objectif : détecter rapidement les nouvelles paires/pools à partir des flux RP
- extraire token A, token B, LP mint, vaults et comptes utiles quand cela est possible, - extraire token A, token B, LP mint, vaults et comptes utiles quand cela est possible,
- alimenter `kb_pools`, `kb_pairs`, `kb_pool_tokens` et `kb_pool_listings` avec des données plus fiables que la seule détection de comptes. - alimenter `kb_pools`, `kb_pairs`, `kb_pool_tokens` et `kb_pool_listings` avec des données plus fiables que la seule détection de comptes.
### 6.035. Version `0.7.4` — Modèle métier DEX enrichi ### 6.036. Version `0.7.4` — Modèle métier DEX enrichi
Objectif : faire converger la détection technique et le modèle métier vers une vision proche de lactivité réelle du marché. Objectif : faire converger la détection technique et le modèle métier vers une vision proche de lactivité réelle du marché.
À faire : À faire :
@@ -486,7 +497,7 @@ Objectif : faire converger la détection technique et le modèle métier vers un
- préparer une vision cohérente `token <-> pool <-> pair <-> protocole`, - préparer une vision cohérente `token <-> pool <-> pair <-> protocole`,
- distinguer les objets de référence des événements dactivité. - distinguer les objets de référence des événements dactivité.
### 6.036. Version `0.7.5` — Wallets, holdings et participants observés ### 6.037. Version `0.7.5` — Wallets, holdings et participants observés
Objectif : préparer le suivi des acteurs on-chain autour des pools et tokens détectés. Objectif : préparer le suivi des acteurs on-chain autour des pools et tokens détectés.
À faire : À faire :
@@ -496,7 +507,7 @@ Objectif : préparer le suivi des acteurs on-chain autour des pools et tokens d
- préparer lidentification des créateurs, mint authorities, wallets dactivité et contreparties, - préparer lidentification des créateurs, mint authorities, wallets dactivité et contreparties,
- éviter de limiter lanalyse future au seul niveau token/pool sans vision des participants. - éviter de limiter lanalyse future au seul niveau token/pool sans vision des participants.
### 6.037. Version `0.7.6` — Séries de prix, volumes et agrégats DEX ### 6.038. Version `0.7.6` — Séries de prix, volumes et agrégats DEX
Objectif : préparer la couche analytique fine à partir des événements métier normalisés. Objectif : préparer la couche analytique fine à partir des événements métier normalisés.
À faire : À faire :
@@ -506,7 +517,7 @@ Objectif : préparer la couche analytique fine à partir des événements métie
- permettre plus tard le calcul dOHLCV, volume, nombre de trades et liquidité par fenêtre, - permettre plus tard le calcul dOHLCV, volume, nombre de trades et liquidité par fenêtre,
- préparer le terrain pour la couche analytique `0.8.x`. - préparer le terrain pour la couche analytique `0.8.x`.
### 6.038. Version `0.7.x` — DEX connectors v1 ### 6.039. Version `0.7.x` — DEX connectors v1
Objectif : structurer les connecteurs DEX autour dun pipeline complet de résolution, décodage et normalisation métier. Objectif : structurer les connecteurs DEX autour dun pipeline complet de résolution, décodage et normalisation métier.
Cibles initiales possibles : Cibles initiales possibles :
@@ -526,7 +537,7 @@ Résultat attendu :
- création dobjets métier riches pour tokens, pools, paires, wallets, holdings et séries de prix, - création dobjets métier riches pour tokens, pools, paires, wallets, holdings et séries de prix,
- remplacement progressif des scripts heuristiques externes par des composants Rust intégrés. - remplacement progressif des scripts heuristiques externes par des composants Rust intégrés.
### 6.039. Version `0.8.x` — Analyse et filtrage ### 6.040. Version `0.8.x` — Analyse et filtrage
Objectif : transformer les événements bruts en signaux exploitables. Objectif : transformer les événements bruts en signaux exploitables.
À faire : À faire :
@@ -537,7 +548,7 @@ Objectif : transformer les événements bruts en signaux exploitables.
- statistiques de comportement, - statistiques de comportement,
- premiers patterns. - premiers patterns.
### 6.040. Version `1.x.y` — Wallets et swap préparatoire ### 6.041. Version `1.x.y` — Wallets et swap préparatoire
Objectif : préparer la couche daction. Objectif : préparer la couche daction.
À faire : À faire :
@@ -548,7 +559,7 @@ Objectif : préparer la couche daction.
- préparation dordres et de swaps, - préparation dordres et de swaps,
- simulation et garde-fous. - simulation et garde-fous.
### 6.041. Version `2.x.y` — Trading semi-automatisé ### 6.042. Version `2.x.y` — Trading semi-automatisé
Objectif : brancher lanalyse à laction tout en gardant des garde-fous explicites. Objectif : brancher lanalyse à laction tout en gardant des garde-fous explicites.
À faire : À faire :
@@ -559,7 +570,7 @@ Objectif : brancher lanalyse à laction tout en gardant des garde-fous exp
- confirmations explicites ou semi-automatiques, - confirmations explicites ou semi-automatiques,
- journaux dexécution. - journaux dexécution.
### 6.042. Version `3.x.y` — Yellowstone gRPC ### 6.043. Version `3.x.y` — Yellowstone gRPC
Objectif : ajouter le connecteur gRPC dédié. Objectif : ajouter le connecteur gRPC dédié.
À faire : À faire :
@@ -580,10 +591,12 @@ Modules cibles à court terme :
- `constants.rs` - `constants.rs`
- `types.rs` - `types.rs`
- `ws_client.rs` - `ws_client.rs`
- `ws_manager.rs`
- `http_client.rs` - `http_client.rs`
- `rpc_json.rs` - `http_pool.rs`
- `rpc_ws.rs` - `json_rpc_ws.rs`
- `rpc_ws_solana.rs` - `solana_pubsub_ws.rs`
- `detect.rs`
### 7.2. `kb_app` ### 7.2. `kb_app`
Responsabilités cibles : Responsabilités cibles :
@@ -644,9 +657,10 @@ Le projet doit maintenir au minimum :
## 12. Priorité immédiate ## 12. Priorité immédiate
La priorité immédiate est désormais la suivante : La priorité immédiate est désormais la suivante :
1. démarrer la version `0.6.3` avec lenrichissement des notifications WS utiles, 1. démarrer la version `0.7.0` avec la résolution transactionnelle orientée DEX,
2. améliorer lextraction des métadonnées utiles depuis `accountNotification`, `logsNotification` et `signatureNotification`, 2. introduire une file de signatures ou de résolutions alimentée par les flux WS utiles,
3. produire des observations on-chain plus précises et homogènes, 3. corréler `logsNotification`, `programNotification` et `signatureNotification` avec des appels `getTransaction`,
4. préparer ensuite la version `0.6.4` pour les premières règles de détection technique, 4. utiliser le pool HTTP existant pour enrichir les signaux détectés côté WS,
5. conserver le découplage entre transport, détection et stockage, 5. préparer ensuite la version `0.7.1` pour le modèle transactionnel Solana enrichi,
6. planifier enfin `0.6.5` pour lorchestration multi-clients WebSocket via `ws_pool.rs` ou `ws_manager.rs`. 6. conserver le découplage entre transport, résolution transactionnelle, détection métier et stockage.

View File

@@ -6,7 +6,8 @@
"main", "main",
"splash", "splash",
"demo_ws", "demo_ws",
"demo_http" "demo_http",
"demo_ws_manager"
], ],
"permissions": [ "permissions": [
"core:default", "core:default",

View File

@@ -0,0 +1,109 @@
<!-- file: kb_app/frontend/demo_ws_manager.html -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Khadhroony-BoBoBot — Demo Ws Manager</title>
<link rel="stylesheet" href="sass/main.scss" />
</head>
<body class="bg-body-tertiary">
<header class="app-header">
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
<div class="container my-0">
<a class="navbar-brand d-flex align-items-center" href="/">
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Demo Ws Manager</span>
</a>
</div>
</nav>
</header>
<main class="app-main">
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
<div class="container vcentered sketchy-translucid py-4">
<div class="row g-4">
<div class="col-12 col-xxl-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h1 class="h4 mb-3">Pilotage</h1>
<p class="text-body-secondary mb-3">
Démo légère du <code>WsManager</code> : démarrage/arrêt groupé, pilotage par rôle et bus unifié dévénements.
</p>
<div class="d-flex flex-wrap gap-2 mb-4">
<button id="demoWsManagerStartAllButton" type="button" class="btn btn-primary">Start all</button>
<button id="demoWsManagerStopAllButton" type="button" class="btn btn-outline-primary">Stop all</button>
<button id="demoWsManagerRefreshButton" type="button" class="btn btn-outline-secondary">Refresh snapshot</button>
</div>
<div class="mb-3">
<label for="demoWsManagerRoleSelect" class="form-label">Rôle</label>
<select id="demoWsManagerRoleSelect" class="form-select"></select>
</div>
<div class="d-flex flex-wrap gap-2 mb-4">
<button id="demoWsManagerStartRoleButton" type="button" class="btn btn-primary">Start role</button>
<button id="demoWsManagerStopRoleButton" type="button" class="btn btn-outline-primary">Stop role</button>
</div>
<div class="small text-body-secondary">
<div><strong>Managed endpoints:</strong> <span id="demoWsManagerEndpointCountText">0</span></div>
<div><strong>Started endpoints:</strong> <span id="demoWsManagerStartedCountText">0</span></div>
</div>
</div>
</div>
</div>
<div class="col-12 col-xxl-8">
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<h2 class="h5 mb-3">Snapshot</h2>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead>
<tr>
<th>Endpoint</th>
<th>Provider</th>
<th>Roles</th>
<th>State</th>
<th>Subs.</th>
</tr>
</thead>
<tbody id="demoWsManagerTableBody"></tbody>
</table>
</div>
</div>
</div>
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="h5 mb-0">Unified event log</h2>
<button id="demoWsManagerClearLogButton" type="button" class="btn btn-outline-secondary btn-sm">Clear log</button>
</div>
<textarea id="demoWsManagerLogTextarea" class="form-control font-monospace" rows="18" readonly spellcheck="false"></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<footer class="app-footer bg-dark text-light">
<div class="container h-100 d-flex align-items-center">
<div class="row flex-grow-1 align-items-center">
<div class="col-12 col-md-6 text-center text-small my-1 my-md-0">
&copy; 2026 SASEDEV — Demo Ws Manager
</div>
</div>
</div>
</footer>
<script type="module" src="ts/demo_ws_manager.ts" defer></script>
</body>
</html>

View File

@@ -25,6 +25,9 @@
<button id="openDemoHttpButton" class="btn btn-outline-primary"> <button id="openDemoHttpButton" class="btn btn-outline-primary">
Demo Http Demo Http
</button> </button>
<button id="openDemoWsManagerButton" type="button" class="btn btn-outline-primary">
Demo Ws Manager
</button>
</div> </div>
</div> </div>
</nav> </nav>
@@ -54,6 +57,10 @@
Les tests PRC Http manuels sont disponibles dans la fenêtre dédiée Les tests PRC Http manuels sont disponibles dans la fenêtre dédiée
<strong>Demo Http</strong>. <strong>Demo Http</strong>.
</p> </p>
<p class="text-body-secondary mb-3">
La démonstration légère de pilotage multi-clients est disponible dans la fenêtre
<strong>Demo Ws Manager</strong>.
</p>
<div class="d-flex flex-wrap gap-2"> <div class="d-flex flex-wrap gap-2">
<button id="openDemoWsButtonSecondary" type="button" class="btn btn-primary"> <button id="openDemoWsButtonSecondary" type="button" class="btn btn-primary">
@@ -62,6 +69,9 @@
<button id="openDemoHttpButtonSecondary" type="button" class="btn btn-primary"> <button id="openDemoHttpButtonSecondary" type="button" class="btn btn-primary">
Ouvrir Demo Http Ouvrir Demo Http
</button> </button>
<button id="openDemoWsManagerButtonSecondary" type="button" class="btn btn-primary">
Ouvrir Demo Ws Manager
</button>
</div> </div>
<hr /> <hr />

View File

@@ -268,6 +268,8 @@ async function refreshPoolSnapshot(
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
void takeoverConsole(); void takeoverConsole();
debug("demo_http window loaded");
const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle'); const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
if (sidebarToggle) { if (sidebarToggle) {
// restaurer létat depuis localStorage // restaurer létat depuis localStorage
@@ -432,6 +434,4 @@ document.addEventListener("DOMContentLoaded", async () => {
appendLogLine(logTextarea, "[ui] demo_http window loaded"); appendLogLine(logTextarea, "[ui] demo_http window loaded");
await refreshPoolSnapshot(poolTableBody, logTextarea, true); await refreshPoolSnapshot(poolTableBody, logTextarea, true);
debug("demo_http window loaded");
}); });

View File

@@ -240,6 +240,9 @@ function applyStatusToUi(
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
void takeoverConsole(); void takeoverConsole();
debug("demo_ws window loaded");
const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle'); const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
if (sidebarToggle) { if (sidebarToggle) {
// restaurer létat depuis localStorage // restaurer létat depuis localStorage
@@ -514,6 +517,4 @@ document.addEventListener("DOMContentLoaded", async () => {
unlistenStatusEvent(); unlistenStatusEvent();
} }
}); });
debug("demo_ws window loaded");
}); });

View File

@@ -0,0 +1,237 @@
// file: kb_app/frontend/ts/demo_ws_manager.ts
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { debug, takeoverConsole } from "@fltsci/tauri-plugin-tracing";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
type DemoWsManagerEndpointSummary = {
name: string;
resolvedUrl: string;
provider: string;
roles: string[];
connectionState: string;
activeSubscriptionCount: number;
};
type DemoWsManagerSnapshotPayload = {
endpointCount: number;
startedCount: number;
endpoints: DemoWsManagerEndpointSummary[];
};
const endpointCountText = document.querySelector<HTMLSpanElement>("#demoWsManagerEndpointCountText");
const startedCountText = document.querySelector<HTMLSpanElement>("#demoWsManagerStartedCountText");
const roleSelect = document.querySelector<HTMLSelectElement>("#demoWsManagerRoleSelect");
const tableBody = document.querySelector<HTMLTableSectionElement>("#demoWsManagerTableBody");
const logTextarea = document.querySelector<HTMLTextAreaElement>("#demoWsManagerLogTextarea");
const startAllButton = document.querySelector<HTMLButtonElement>("#demoWsManagerStartAllButton");
const stopAllButton = document.querySelector<HTMLButtonElement>("#demoWsManagerStopAllButton");
const refreshButton = document.querySelector<HTMLButtonElement>("#demoWsManagerRefreshButton");
const startRoleButton = document.querySelector<HTMLButtonElement>("#demoWsManagerStartRoleButton");
const stopRoleButton = document.querySelector<HTMLButtonElement>("#demoWsManagerStopRoleButton");
const clearLogButton = document.querySelector<HTMLButtonElement>("#demoWsManagerClearLogButton");
function appendLogLine(line: string): void {
if (!logTextarea) {
return;
}
const prefix = logTextarea.value.length > 0 ? "\n" : "";
logTextarea.value += `${prefix}${line}`;
logTextarea.scrollTop = logTextarea.scrollHeight;
}
function renderSnapshot(snapshot: DemoWsManagerSnapshotPayload): void {
if (endpointCountText) {
endpointCountText.textContent = String(snapshot.endpointCount);
}
if (startedCountText) {
startedCountText.textContent = String(snapshot.startedCount);
}
if (!tableBody) {
return;
}
tableBody.innerHTML = "";
for (const endpoint of snapshot.endpoints) {
const row = document.createElement("tr");
row.innerHTML = `
<td>
<div class="fw-semibold">${endpoint.name}</div>
<div class="small text-body-secondary">${endpoint.resolvedUrl}</div>
</td>
<td>${endpoint.provider}</td>
<td>${endpoint.roles.join(", ")}</td>
<td>${endpoint.connectionState}</td>
<td>${endpoint.activeSubscriptionCount}</td>
`;
tableBody.appendChild(row);
}
}
async function refreshSnapshot(): Promise<void> {
try {
const snapshot = await invoke<DemoWsManagerSnapshotPayload>("demo_ws_manager_get_snapshot");
renderSnapshot(snapshot);
appendLogLine("[ui] refreshed manager snapshot");
} catch (error) {
appendLogLine(`[ui] snapshot error: ${String(error)}`);
}
}
async function loadRoles(): Promise<void> {
if (!roleSelect) {
return;
}
roleSelect.innerHTML = "";
try {
const roles = await invoke<string[]>("demo_ws_manager_list_roles");
for (const role of roles) {
const option = document.createElement("option");
option.value = role;
option.textContent = role;
roleSelect.appendChild(option);
}
} catch (error) {
appendLogLine(`[ui] list roles error: ${String(error)}`);
}
}
async function startAll(): Promise<void> {
try {
const snapshot = await invoke<DemoWsManagerSnapshotPayload>("demo_ws_manager_start_all");
renderSnapshot(snapshot);
} catch (error) {
appendLogLine(`[ui] start all error: ${String(error)}`);
}
}
async function stopAll(): Promise<void> {
try {
const snapshot = await invoke<DemoWsManagerSnapshotPayload>("demo_ws_manager_stop_all");
renderSnapshot(snapshot);
} catch (error) {
appendLogLine(`[ui] stop all error: ${String(error)}`);
}
}
async function startRole(): Promise<void> {
if (!roleSelect || roleSelect.value.trim().length === 0) {
appendLogLine("[ui] no role selected");
return;
}
try {
const snapshot = await invoke<DemoWsManagerSnapshotPayload>("demo_ws_manager_start_role", {
role: roleSelect.value,
});
renderSnapshot(snapshot);
} catch (error) {
appendLogLine(`[ui] start role error: ${String(error)}`);
}
}
async function stopRole(): Promise<void> {
if (!roleSelect || roleSelect.value.trim().length === 0) {
appendLogLine("[ui] no role selected");
return;
}
try {
const snapshot = await invoke<DemoWsManagerSnapshotPayload>("demo_ws_manager_stop_role", {
role: roleSelect.value,
});
renderSnapshot(snapshot);
} catch (error) {
appendLogLine(`[ui] stop role error: ${String(error)}`);
}
}
document.addEventListener("DOMContentLoaded", async () => {
void takeoverConsole();
debug("demo_ws_manager window loaded");
const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
if (sidebarToggle) {
// restaurer létat depuis localStorage
if (localStorage.getItem('sidebar-toggle') === 'true') {
document.body.classList.add('sidenav-toggled');
}
sidebarToggle.addEventListener('click', (event) => {
event.preventDefault();
document.body.classList.toggle('sidenav-toggled');
localStorage.setItem('sidebar-toggle', document.body.classList.contains('sidenav-toggled') ? 'true' : 'false');
});
}
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
const toastElList = document.querySelectorAll('.toast');
Array.from(toastElList).map(toastEl => new bootstrap.Toast(toastEl));
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
Array.from(popoverTriggerList).map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl));
const gobackto = location.pathname + location.search;
document.querySelectorAll<HTMLAnchorElement>('a[data-setlang]').forEach((a) => {
const href = a.getAttribute("href");
if (!href) return; // pas de href => on ignore
const url = new URL(href, location.origin);
url.searchParams.set("gobackto", gobackto);
// conserve une URL relative (path + query)
a.setAttribute("href", url.pathname + "?" + url.searchParams.toString());
});
if (startAllButton) {
startAllButton.addEventListener("click", () => {
void startAll();
});
}
if (stopAllButton) {
stopAllButton.addEventListener("click", () => {
void stopAll();
});
}
if (refreshButton) {
refreshButton.addEventListener("click", () => {
void refreshSnapshot();
});
}
if (startRoleButton) {
startRoleButton.addEventListener("click", () => {
void startRole();
});
}
if (stopRoleButton) {
stopRoleButton.addEventListener("click", () => {
void stopRole();
});
}
if (clearLogButton && logTextarea) {
clearLogButton.addEventListener("click", () => {
logTextarea.value = "";
});
}
await listen<string>("kb-demo-ws-manager-log", (event) => {
appendLogLine(event.payload);
});
await listen<DemoWsManagerSnapshotPayload>("kb-demo-ws-manager-snapshot", (event) => {
renderSnapshot(event.payload);
});
await loadRoles();
await refreshSnapshot();
});

View File

@@ -23,6 +23,14 @@ async function openDemoHttpWindow(): Promise<void> {
console.error("open_demo_http_window failed:", error); console.error("open_demo_http_window failed:", error);
} }
} }
async function openDemoWsManagerWindow(): Promise<void> {
try {
await invoke("open_demo_ws_manager_window");
} catch (error) {
console.error("open_demo_ws_manager_window failed:", error);
}
}
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
void takeoverConsole(); void takeoverConsole();
const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle'); const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
@@ -64,6 +72,8 @@ document.addEventListener("DOMContentLoaded", async () => {
const openDemoHttpButton = document.querySelector<HTMLButtonElement>("#openDemoHttpButton"); const openDemoHttpButton = document.querySelector<HTMLButtonElement>("#openDemoHttpButton");
const openDemoHttpButtonSecondary = document.querySelector<HTMLButtonElement>("#openDemoHttpButtonSecondary"); const openDemoHttpButtonSecondary = document.querySelector<HTMLButtonElement>("#openDemoHttpButtonSecondary");
const openDemoWsManagerButton = document.querySelector<HTMLButtonElement>("#openDemoWsManagerButton");
const openDemoWsManagerButtonSecondary = document.querySelector<HTMLButtonElement>("#openDemoWsManagerButtonSecondary");
if (openDemoWsButton) { if (openDemoWsButton) {
openDemoWsButton.addEventListener("click", () => { openDemoWsButton.addEventListener("click", () => {
@@ -89,6 +99,18 @@ document.addEventListener("DOMContentLoaded", async () => {
}); });
} }
if (openDemoWsManagerButton) {
openDemoWsManagerButton.addEventListener("click", () => {
void openDemoWsManagerWindow();
});
}
if (openDemoWsManagerButtonSecondary) {
openDemoWsManagerButtonSecondary.addEventListener("click", () => {
void openDemoWsManagerWindow();
});
}
debug("window loaded"); debug("window loaded");
}); });

View File

@@ -1 +1 @@
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main","splash","demo_ws","demo_http"],"permissions":["core:default","tracing:default"]}} {"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main","splash","demo_ws","demo_http","demo_ws_manager"],"permissions":["core:default","tracing:default"]}}

View File

@@ -1,7 +1,7 @@
{ {
"name": "kb-app", "name": "kb-app",
"private": true, "private": true,
"version": "0.4.4", "version": "0.6.6",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -0,0 +1,565 @@
// file: kb_app/src/demo_ws_manager.rs
//! Demo WebSocket manager window commands and runtime state.
//!
//! This module provides a lightweight test bench for `kb_lib::WsManager`.
use tauri::Emitter;
use tauri::Manager;
/// Static endpoint summary enriched with current manager state.
#[derive(Clone, Debug, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct KbDemoWsManagerEndpointSummary {
name: std::string::String,
resolved_url: std::string::String,
provider: std::string::String,
roles: std::vec::Vec<std::string::String>,
connection_state: std::string::String,
active_subscription_count: usize,
}
/// Global demo manager snapshot payload.
#[derive(Clone, Debug, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct KbDemoWsManagerSnapshotPayload {
endpoint_count: usize,
started_count: usize,
endpoints: std::vec::Vec<KbDemoWsManagerEndpointSummary>,
}
/// Runtime state for the demo WebSocket manager window.
#[derive(Debug)]
pub(crate) struct KbDemoWsManagerRuntimeState {
relay_task: std::option::Option<tauri::async_runtime::JoinHandle<()>>,
}
impl KbDemoWsManagerRuntimeState {
/// Creates a new empty runtime state.
pub(crate) fn new() -> Self {
Self { relay_task: None }
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct DemoWsManagerActionResult {
action: std::string::String,
target: std::string::String,
matched_count: usize,
changed_count: usize,
unchanged_count: usize,
}
/// Shows and focuses the preconfigured `demo_ws_manager` window.
#[tauri::command]
pub(crate) async fn open_demo_ws_manager_window(
app_handle: tauri::AppHandle,
state: tauri::State<'_, crate::KbAppState>,
) -> Result<(), std::string::String> {
kb_ensure_demo_ws_manager_relay(&app_handle, &state).await;
let existing_window_option = app_handle.get_webview_window("demo_ws_manager");
let demo_window = match existing_window_option {
Some(demo_window) => demo_window,
None => {
let builder = tauri::WebviewWindowBuilder::new(
&app_handle,
"demo_ws_manager",
tauri::WebviewUrl::App("demo_ws_manager.html".into()),
)
.title("Demo Ws Manager")
.inner_size(1280.0, 800.0)
.min_inner_size(900.0, 620.0)
.center()
.visible(true)
.transparent(false)
.decorations(true);
let build_result = builder.build();
match build_result {
Ok(window) => window,
Err(error) => {
return Err(format!("cannot create demo_ws_manager window: {error:?}"));
}
}
}
};
let show_result = demo_window.show();
if let Err(error) = show_result {
return Err(format!("cannot show demo_ws_manager window: {error:?}"));
}
let focus_result = demo_window.set_focus();
if let Err(error) = focus_result {
return Err(format!("cannot focus demo_ws_manager window: {error:?}"));
}
kb_emit_demo_ws_manager_log(&app_handle, "[ui] demo_ws_manager window loaded");
kb_emit_demo_ws_manager_snapshot(&app_handle, &state).await;
Ok(())
}
/// Returns the current manager snapshot.
#[tauri::command]
pub(crate) async fn demo_ws_manager_get_snapshot(
state: tauri::State<'_, crate::KbAppState>,
) -> Result<KbDemoWsManagerSnapshotPayload, std::string::String> {
kb_build_demo_ws_manager_snapshot(&state).await
}
/// Returns the distinct configured roles for enabled websocket endpoints.
#[tauri::command]
pub(crate) async fn demo_ws_manager_list_roles(
state: tauri::State<'_, crate::KbAppState>,
) -> Result<std::vec::Vec<std::string::String>, std::string::String> {
let mut roles = std::collections::BTreeSet::new();
for endpoint in &state.config.solana.ws_endpoints {
if !endpoint.enabled {
continue;
}
for role in &endpoint.roles {
roles.insert(role.clone());
}
}
Ok(roles.into_iter().collect())
}
/// Starts all managed websocket endpoints.
#[tauri::command]
pub(crate) async fn demo_ws_manager_start_all(
app_handle: tauri::AppHandle,
state: tauri::State<'_, crate::KbAppState>,
) -> Result<KbDemoWsManagerSnapshotPayload, std::string::String> {
kb_ensure_demo_ws_manager_relay(&app_handle, &state).await;
let matched_count = state.ws_manager.endpoint_names().await.len();
let start_result = state.ws_manager.start_all().await;
let changed_count = match start_result {
Ok(changed_count) => changed_count,
Err(error) => return Err(error.to_string()),
};
let action_result = kb_build_action_result("start", "all", matched_count, changed_count);
kb_emit_demo_ws_manager_log(
&app_handle,
kb_format_action_result_for_log(&action_result).as_str(),
);
kb_emit_demo_ws_manager_snapshot(&app_handle, &state).await;
kb_build_demo_ws_manager_snapshot(&state).await
}
/// Stops all managed websocket endpoints.
#[tauri::command]
pub(crate) async fn demo_ws_manager_stop_all(
app_handle: tauri::AppHandle,
state: tauri::State<'_, crate::KbAppState>,
) -> Result<KbDemoWsManagerSnapshotPayload, std::string::String> {
let matched_count = state.ws_manager.endpoint_names().await.len();
let stop_result = state.ws_manager.stop_all().await;
let changed_count = match stop_result {
Ok(changed_count) => changed_count,
Err(error) => return Err(error.to_string()),
};
let action_result = kb_build_action_result("stop", "all", matched_count, changed_count);
kb_emit_demo_ws_manager_log(
&app_handle,
kb_format_action_result_for_log(&action_result).as_str(),
);
kb_emit_demo_ws_manager_snapshot(&app_handle, &state).await;
kb_build_demo_ws_manager_snapshot(&state).await
}
/// Starts all managed websocket endpoints having the selected role.
#[tauri::command]
pub(crate) async fn demo_ws_manager_start_role(
app_handle: tauri::AppHandle,
state: tauri::State<'_, crate::KbAppState>,
role: std::string::String,
) -> Result<KbDemoWsManagerSnapshotPayload, std::string::String> {
kb_ensure_demo_ws_manager_relay(&app_handle, &state).await;
let matched_count = state
.ws_manager
.endpoint_names_for_role(role.as_str())
.await
.len();
let start_result = state.ws_manager.start_role(role.as_str()).await;
let changed_count = match start_result {
Ok(changed_count) => changed_count,
Err(error) => return Err(error.to_string()),
};
let action_result =
kb_build_action_result("start", role.as_str(), matched_count, changed_count);
kb_emit_demo_ws_manager_log(
&app_handle,
kb_format_action_result_for_log(&action_result).as_str(),
);
kb_emit_demo_ws_manager_snapshot(&app_handle, &state).await;
kb_build_demo_ws_manager_snapshot(&state).await
}
/// Stops all managed websocket endpoints having the selected role.
#[tauri::command]
pub(crate) async fn demo_ws_manager_stop_role(
app_handle: tauri::AppHandle,
state: tauri::State<'_, crate::KbAppState>,
role: std::string::String,
) -> Result<KbDemoWsManagerSnapshotPayload, std::string::String> {
let matched_count = state
.ws_manager
.endpoint_names_for_role(role.as_str())
.await
.len();
let stop_result = state.ws_manager.stop_role(role.as_str()).await;
let changed_count = match stop_result {
Ok(changed_count) => changed_count,
Err(error) => return Err(error.to_string()),
};
let action_result = kb_build_action_result("stop", role.as_str(), matched_count, changed_count);
kb_emit_demo_ws_manager_log(
&app_handle,
kb_format_action_result_for_log(&action_result).as_str(),
);
kb_emit_demo_ws_manager_snapshot(&app_handle, &state).await;
kb_build_demo_ws_manager_snapshot(&state).await
}
async fn kb_build_demo_ws_manager_snapshot(
state: &tauri::State<'_, crate::KbAppState>,
) -> Result<KbDemoWsManagerSnapshotPayload, std::string::String> {
let snapshot_result = state.ws_manager.snapshot().await;
let snapshot = match snapshot_result {
Ok(snapshot) => snapshot,
Err(error) => return Err(error.to_string()),
};
let mut endpoints = std::vec::Vec::new();
for managed_endpoint in snapshot.endpoints {
let config_endpoint_option = state
.config
.find_ws_endpoint(&managed_endpoint.endpoint_name);
let config_endpoint = match config_endpoint_option {
Some(config_endpoint) => config_endpoint,
None => {
return Err(format!(
"managed websocket endpoint '{}' is missing from config",
managed_endpoint.endpoint_name
));
}
};
endpoints.push(KbDemoWsManagerEndpointSummary {
name: managed_endpoint.endpoint_name,
resolved_url: managed_endpoint.resolved_url,
provider: managed_endpoint.provider,
roles: config_endpoint.roles.clone(),
connection_state: kb_connection_state_to_string(managed_endpoint.state),
active_subscription_count: managed_endpoint.active_subscription_count,
});
}
Ok(KbDemoWsManagerSnapshotPayload {
endpoint_count: snapshot.endpoint_count,
started_count: snapshot.started_count,
endpoints,
})
}
async fn kb_emit_demo_ws_manager_snapshot(
app_handle: &tauri::AppHandle,
state: &tauri::State<'_, crate::KbAppState>,
) {
let snapshot_result = kb_build_demo_ws_manager_snapshot(state).await;
let snapshot = match snapshot_result {
Ok(snapshot) => snapshot,
Err(error) => {
kb_emit_demo_ws_manager_log(app_handle, &format!("[ui] snapshot error: {error}"));
return;
}
};
let emit_result = app_handle.emit("kb-demo-ws-manager-snapshot", snapshot);
if let Err(error) = emit_result {
tracing::error!("error emitting demo_ws_manager snapshot event: {error:?}");
}
}
async fn kb_ensure_demo_ws_manager_relay(
app_handle: &tauri::AppHandle,
state: &tauri::State<'_, crate::KbAppState>,
) {
let mut runtime_guard = state.demo_ws_manager_runtime.lock().await;
if runtime_guard.relay_task.is_some() {
return;
}
let mut receiver = state.ws_manager.subscribe_events();
let relay_app_handle = app_handle.clone();
let relay_state = state.demo_ws_manager_runtime.clone();
let relay_task = tauri::async_runtime::spawn(async move {
loop {
let recv_result = receiver.recv().await;
match recv_result {
Ok(event) => {
let line = kb_format_ws_event(&event);
kb_emit_demo_ws_manager_log(&relay_app_handle, line.as_str());
}
Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
kb_emit_demo_ws_manager_log(
&relay_app_handle,
&format!(
"[manager] event receiver lagged and skipped {} message(s)",
skipped
),
);
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
break;
}
}
}
let mut runtime_guard = relay_state.lock().await;
runtime_guard.relay_task = None;
});
runtime_guard.relay_task = Some(relay_task);
}
fn kb_emit_demo_ws_manager_log(app_handle: &tauri::AppHandle, message: &str) {
let emit_result = app_handle.emit("kb-demo-ws-manager-log", message.to_string());
if let Err(error) = emit_result {
tracing::error!("error emitting demo_ws_manager log event: {error:?}");
}
}
fn kb_connection_state_to_string(state: kb_lib::KbConnectionState) -> std::string::String {
match state {
kb_lib::KbConnectionState::Disconnected => "Disconnected".to_string(),
kb_lib::KbConnectionState::Connecting => "Connecting".to_string(),
kb_lib::KbConnectionState::Connected => "Connected".to_string(),
kb_lib::KbConnectionState::Disconnecting => "Disconnecting".to_string(),
}
}
fn kb_format_ws_event(event: &kb_lib::WsEvent) -> std::string::String {
match event {
kb_lib::WsEvent::Connected {
endpoint_name,
endpoint_url,
} => {
format!("[ws:{endpoint_name}] connected to {endpoint_url}")
}
kb_lib::WsEvent::TextMessage {
endpoint_name,
text,
} => {
format!("[ws:{endpoint_name}] text: {text}")
}
kb_lib::WsEvent::JsonRpcMessage {
endpoint_name,
message,
} => match message {
kb_lib::KbJsonRpcWsIncomingMessage::SuccessResponse(response) => {
format!(
"[ws:{endpoint_name}] json-rpc success id={} result={}",
response.id, response.result
)
}
kb_lib::KbJsonRpcWsIncomingMessage::ErrorResponse(response) => {
format!(
"[ws:{endpoint_name}] json-rpc error id={} code={} message={}",
response.id, response.error.code, response.error.message
)
}
kb_lib::KbJsonRpcWsIncomingMessage::Notification(notification) => {
format!(
"[ws:{endpoint_name}] json-rpc notification method={} subscription={} result={}",
notification.method,
notification.params.subscription,
notification.params.result
)
}
},
kb_lib::WsEvent::JsonRpcParseError {
endpoint_name,
text,
error,
} => {
format!(
"[ws:{endpoint_name}] json-rpc parse error: {} | raw={}",
error, text
)
}
kb_lib::WsEvent::SubscriptionRegistered {
endpoint_name,
subscription,
} => {
format!(
"[ws:{endpoint_name}] subscription registered subscribe_method={} unsubscribe_method={} notification_method={} request_id={} subscription_id={}",
subscription.subscribe_method,
subscription.unsubscribe_method,
subscription.notification_method,
subscription.request_id,
subscription.subscription_id
)
}
kb_lib::WsEvent::SubscriptionNotification {
endpoint_name,
subscription,
notification,
method_matches_registry,
} => {
format!(
"[ws:{endpoint_name}] tracked notification subscription_id={} method={} expected={} matches={} result={}",
subscription.subscription_id,
notification.method,
subscription.notification_method,
method_matches_registry,
notification.params.result
)
}
kb_lib::WsEvent::JsonRpcNotificationWithoutSubscription {
endpoint_name,
notification,
} => {
format!(
"[ws:{endpoint_name}] untracked notification method={} subscription={} result={}",
notification.method, notification.params.subscription, notification.params.result
)
}
kb_lib::WsEvent::SubscriptionUnregistered {
endpoint_name,
subscription_id,
unsubscribe_method,
was_active,
} => {
format!(
"[ws:{endpoint_name}] subscription unregistered subscription_id={} unsubscribe_method={} was_active={}",
subscription_id, unsubscribe_method, was_active
)
}
kb_lib::WsEvent::BinaryMessage {
endpoint_name,
data,
} => {
format!("[{endpoint_name}] binary ({} bytes)", data.len())
}
kb_lib::WsEvent::Ping {
endpoint_name,
data,
} => {
format!("[{endpoint_name}] ping ({} bytes)", data.len())
}
kb_lib::WsEvent::Pong {
endpoint_name,
data,
} => {
format!("[{endpoint_name}] pong ({} bytes)", data.len())
}
kb_lib::WsEvent::CloseReceived {
endpoint_name,
code,
reason,
} => {
format!(
"[ws:{endpoint_name}] close received code={:?} reason={:?}",
code, reason
)
}
kb_lib::WsEvent::Disconnected { endpoint_name } => {
format!("[ws:{endpoint_name}] disconnected")
}
kb_lib::WsEvent::Error {
endpoint_name,
error,
} => {
format!("[ws:{endpoint_name}] error: {error}")
}
}
}
fn kb_build_action_result(
action: &str,
target: &str,
matched_count: usize,
changed_count: usize,
) -> DemoWsManagerActionResult {
let unchanged_count = matched_count.saturating_sub(changed_count);
DemoWsManagerActionResult {
action: action.to_string(),
target: target.to_string(),
matched_count,
changed_count,
unchanged_count,
}
}
fn kb_action_past_tense(action: &str) -> &'static str {
match action {
"start" => "started",
"stop" => "stopped",
_ => "processed",
}
}
fn kb_format_action_result_for_log(result: &DemoWsManagerActionResult) -> std::string::String {
let is_all = result.target == "all";
let past = kb_action_past_tense(result.action.as_str());
if result.matched_count == 0 {
if is_all {
return "[ui] no managed websocket endpoint is configured".to_string();
}
return format!(
"[ui] no managed websocket endpoint matches role '{}'",
result.target
);
}
if result.changed_count == 0 {
if is_all {
return format!(
"[ui] all managed websocket endpoints were already {}",
if result.action == "start" {
"started"
} else {
"stopped"
}
);
}
return format!(
"[ui] role '{}' was already {} on {} endpoint(s)",
result.target,
if result.action == "start" {
"started"
} else {
"stopped"
},
result.unchanged_count
);
}
if result.unchanged_count == 0 {
if is_all {
return format!(
"[ui] {}ed {} managed websocket endpoint(s)",
past, result.changed_count
);
}
return format!(
"[ui] {}ed role '{}' on {} endpoint(s)",
past, result.target, result.changed_count
);
}
if is_all {
return format!(
"[ui] {}ed {} managed websocket endpoint(s); {} already {}",
past,
result.changed_count,
result.unchanged_count,
if result.action == "start" {
"started"
} else {
"stopped"
}
);
}
format!(
"[ui] {}ed role '{}' on {} endpoint(s); {} already {}",
result.action,
result.target,
result.changed_count,
result.unchanged_count,
if result.action == "start" {
"started"
} else {
"stopped"
}
)
}

View File

@@ -11,6 +11,7 @@
mod demo_http; mod demo_http;
mod demo_ws; mod demo_ws;
mod demo_ws_manager;
mod splash; mod splash;
pub use crate::splash::SplashOrder; pub use crate::splash::SplashOrder;
@@ -37,6 +38,8 @@ struct KbAppState {
config: kb_lib::KbConfig, config: kb_lib::KbConfig,
ws_runtime: tokio::sync::Mutex<KbWsRuntimeState>, ws_runtime: tokio::sync::Mutex<KbWsRuntimeState>,
demo_ws_runtime: std::sync::Arc<tokio::sync::Mutex<crate::demo_ws::KbDemoWsRuntimeState>>, demo_ws_runtime: std::sync::Arc<tokio::sync::Mutex<crate::demo_ws::KbDemoWsRuntimeState>>,
demo_ws_manager_runtime: std::sync::Arc<tokio::sync::Mutex<crate::demo_ws_manager::KbDemoWsManagerRuntimeState>>,
ws_manager: std::sync::Arc<kb_lib::WsManager>,
http_pool: kb_lib::HttpEndpointPool, http_pool: kb_lib::HttpEndpointPool,
} }
@@ -82,12 +85,24 @@ pub fn run() {
panic!("cannot create http endpoint pool: {}", error); panic!("cannot create http endpoint pool: {}", error);
} }
}; };
let ws_manager_result = kb_lib::WsManager::from_config(&config);
let ws_manager = match ws_manager_result {
Ok(ws_manager) => ws_manager,
Err(error) => {
tracing::error!("cannot create websocket manager: {}", error);
panic!("cannot create websocket manager: {}", error);
}
};
let app_state = KbAppState { let app_state = KbAppState {
config: config.clone(), config: config.clone(),
ws_runtime: tokio::sync::Mutex::new(KbWsRuntimeState::new()), ws_runtime: tokio::sync::Mutex::new(KbWsRuntimeState::new()),
demo_ws_runtime: std::sync::Arc::new(tokio::sync::Mutex::new( demo_ws_runtime: std::sync::Arc::new(tokio::sync::Mutex::new(
crate::demo_ws::KbDemoWsRuntimeState::new(), crate::demo_ws::KbDemoWsRuntimeState::new(),
)), )),
demo_ws_manager_runtime: std::sync::Arc::new(tokio::sync::Mutex::new(
crate::demo_ws_manager::KbDemoWsManagerRuntimeState::new(),
)),
ws_manager: std::sync::Arc::new(ws_manager),
http_pool, http_pool,
}; };
let tracing_builder = tauri_plugin_tracing::Builder::new(); let tracing_builder = tauri_plugin_tracing::Builder::new();
@@ -106,6 +121,13 @@ pub fn run() {
crate::demo_http::open_demo_http_window, crate::demo_http::open_demo_http_window,
crate::demo_http::demo_http_list_pool_clients, crate::demo_http::demo_http_list_pool_clients,
crate::demo_http::demo_http_execute_request, crate::demo_http::demo_http_execute_request,
crate::demo_ws_manager::open_demo_ws_manager_window,
crate::demo_ws_manager::demo_ws_manager_get_snapshot,
crate::demo_ws_manager::demo_ws_manager_list_roles,
crate::demo_ws_manager::demo_ws_manager_start_all,
crate::demo_ws_manager::demo_ws_manager_stop_all,
crate::demo_ws_manager::demo_ws_manager_start_role,
crate::demo_ws_manager::demo_ws_manager_stop_role,
]); ]);
tauri_builder = tauri_builder.plugin(tracing_builder.build::<tauri::Wry>()); tauri_builder = tauri_builder.plugin(tracing_builder.build::<tauri::Wry>());
tauri_builder = tauri_builder.setup(|app| { tauri_builder = tauri_builder.setup(|app| {

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "kb-bapp", "productName": "kb-bapp",
"version": "0.4.4", "version": "0.6.6",
"identifier": "com.sasedev.kb-app", "identifier": "com.sasedev.kb-app",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",
@@ -64,6 +64,20 @@
"create": false, "create": false,
"transparent": false, "transparent": false,
"decorations": true "decorations": true
},
{
"label": "demo_ws_manager",
"url": "demo_ws_manager.html",
"title": "Demo Ws Manager",
"width": 1280,
"height": 800,
"minWidth": 900,
"minHeight": 620,
"center": true,
"visible": false,
"create": false,
"transparent": false,
"decorations": true
} }
], ],
"security": { "security": {

View File

@@ -139,11 +139,14 @@ impl WsManager {
let endpoint_names = self.endpoint_names_for_role(role).await; let endpoint_names = self.endpoint_names_for_role(role).await;
let mut started_count = 0_usize; let mut started_count = 0_usize;
for endpoint_name in endpoint_names { for endpoint_name in endpoint_names {
let start_result = self.start_endpoint(endpoint_name.as_str()).await; let start_result = self.start_endpoint_inner(endpoint_name.as_str()).await;
if let Err(error) = start_result { let started = match start_result {
return Err(error); Ok(started) => started,
Err(error) => return Err(error),
};
if started {
started_count += 1;
} }
started_count += 1;
} }
Ok(started_count) Ok(started_count)
} }
@@ -153,11 +156,14 @@ impl WsManager {
let endpoint_names = self.endpoint_names_for_role(role).await; let endpoint_names = self.endpoint_names_for_role(role).await;
let mut stopped_count = 0_usize; let mut stopped_count = 0_usize;
for endpoint_name in endpoint_names { for endpoint_name in endpoint_names {
let stop_result = self.stop_endpoint(endpoint_name.as_str()).await; let stop_result = self.stop_endpoint_inner(endpoint_name.as_str()).await;
if let Err(error) = stop_result { let stopped = match stop_result {
return Err(error); Ok(stopped) => stopped,
Err(error) => return Err(error),
};
if stopped {
stopped_count += 1;
} }
stopped_count += 1;
} }
Ok(stopped_count) Ok(stopped_count)
} }
@@ -172,8 +178,7 @@ impl WsManager {
} }
} }
/// Starts one managed endpoint. async fn start_endpoint_inner(&self, endpoint_name: &str) -> Result<bool, crate::KbError> {
pub async fn start_endpoint(&self, endpoint_name: &str) -> Result<(), crate::KbError> {
let client_option = self.client(endpoint_name).await; let client_option = self.client(endpoint_name).await;
let client = match client_option { let client = match client_option {
Some(client) => client, Some(client) => client,
@@ -184,6 +189,12 @@ impl WsManager {
))); )));
} }
}; };
let state = client.connection_state().await;
if state == crate::KbConnectionState::Connected
|| state == crate::KbConnectionState::Connecting
{
return Ok(false);
}
let sender_option = { let sender_option = {
let sender_guard = self.detection_relay_sender.lock().await; let sender_guard = self.detection_relay_sender.lock().await;
sender_guard.clone() sender_guard.clone()
@@ -195,11 +206,19 @@ impl WsManager {
if let Err(error) = connect_result { if let Err(error) = connect_result {
return Err(error); return Err(error);
} }
Ok(()) Ok(true)
} }
/// Stops one managed endpoint. /// Starts one managed endpoint.
pub async fn stop_endpoint(&self, endpoint_name: &str) -> Result<(), crate::KbError> { pub async fn start_endpoint(&self, endpoint_name: &str) -> Result<(), crate::KbError> {
let start_result = self.start_endpoint_inner(endpoint_name).await;
match start_result {
Ok(_) => Ok(()),
Err(error) => Err(error),
}
}
async fn stop_endpoint_inner(&self, endpoint_name: &str) -> Result<bool, crate::KbError> {
let client_option = self.client(endpoint_name).await; let client_option = self.client(endpoint_name).await;
let client = match client_option { let client = match client_option {
Some(client) => client, Some(client) => client,
@@ -210,12 +229,27 @@ impl WsManager {
))); )));
} }
}; };
let state = client.connection_state().await;
if state == crate::KbConnectionState::Disconnected
|| state == crate::KbConnectionState::Disconnecting
{
return Ok(false);
}
client.clear_detection_notification_forwarder().await; client.clear_detection_notification_forwarder().await;
let disconnect_result = client.disconnect().await; let disconnect_result = client.disconnect().await;
if let Err(error) = disconnect_result { if let Err(error) = disconnect_result {
return Err(error); return Err(error);
} }
Ok(()) Ok(true)
}
/// Stops one managed endpoint.
pub async fn stop_endpoint(&self, endpoint_name: &str) -> Result<(), crate::KbError> {
let stop_result = self.stop_endpoint_inner(endpoint_name).await;
match stop_result {
Ok(_) => Ok(()),
Err(error) => Err(error),
}
} }
/// Starts all managed endpoints. /// Starts all managed endpoints.
@@ -223,11 +257,14 @@ impl WsManager {
let endpoint_names = self.endpoint_names().await; let endpoint_names = self.endpoint_names().await;
let mut started_count = 0_usize; let mut started_count = 0_usize;
for endpoint_name in endpoint_names { for endpoint_name in endpoint_names {
let start_result = self.start_endpoint(endpoint_name.as_str()).await; let start_result = self.start_endpoint_inner(endpoint_name.as_str()).await;
if let Err(error) = start_result { let started = match start_result {
return Err(error); Ok(started) => started,
Err(error) => return Err(error),
};
if started {
started_count += 1;
} }
started_count += 1;
} }
Ok(started_count) Ok(started_count)
} }
@@ -237,11 +274,14 @@ impl WsManager {
let endpoint_names = self.endpoint_names().await; let endpoint_names = self.endpoint_names().await;
let mut stopped_count = 0_usize; let mut stopped_count = 0_usize;
for endpoint_name in endpoint_names { for endpoint_name in endpoint_names {
let stop_result = self.stop_endpoint(endpoint_name.as_str()).await; let stop_result = self.stop_endpoint_inner(endpoint_name.as_str()).await;
if let Err(error) = stop_result { let stopped = match stop_result {
return Err(error); Ok(stopped) => stopped,
Err(error) => return Err(error),
};
if stopped {
stopped_count += 1;
} }
stopped_count += 1;
} }
Ok(stopped_count) Ok(stopped_count)
} }