v0.1.0-pre.049
This commit is contained in:
113
kb-app-demo-desktop/frontend/demo_ws.html
Normal file
113
kb-app-demo-desktop/frontend/demo_ws.html
Normal file
@@ -0,0 +1,113 @@
|
||||
<!-- file: kb-app-demo-desktop/frontend/demo_ws.html -->
|
||||
<!-- version: 6 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Khadhroony Bot3 - WebSocket standard</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">
|
||||
<div class="navbar-brand d-flex align-items-center">
|
||||
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
|
||||
<span class="ps-2 fs-4 fw-bold text-primary font-logo">WebSocket standard</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="app-main">
|
||||
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
|
||||
<div class="container py-4">
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-xl-5">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<h1 class="h4 card-title">Souscription WebSocket Solana standard</h1>
|
||||
<p class="text-body-secondary">Cette démo garde une connexion WebSocket ouverte dans AppState. Le bouton Souscrire réutilise la même connexion tant que l’endpoint reste identique ; la déconnexion est explicite ou réalisée à la fermeture globale de l’application.</p>
|
||||
<p class="small text-warning mb-3">Les notifications très fréquentes sont limitées côté Rust puis bornées côté interface pour éviter de bloquer la WebView.</p>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="wsRoleSelect">Rôle</label>
|
||||
<select id="wsRoleSelect" class="form-select"></select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="wsMethodSelect">Méthode</label>
|
||||
<select id="wsMethodSelect" class="form-select"></select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="wsTargetInput">Target</label>
|
||||
<input id="wsTargetInput" class="form-control" placeholder="Pubkey, program id ou signature" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="wsFilterInput">Filter JSON</label>
|
||||
<textarea id="wsFilterInput" class="form-control font-monospace" rows="4" placeholder='{"mentions":["..."]}'></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="wsConfigInput">Config JSON</label>
|
||||
<textarea id="wsConfigInput" class="form-control font-monospace" rows="4" placeholder='{"commitment":"confirmed"}'></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="wsSubscriptionSelect">Subscriptions actives</label>
|
||||
<select id="wsSubscriptionSelect" class="form-select"></select>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<button id="connectWsButton" class="btn btn-primary">Souscrire</button>
|
||||
<button id="unsubscribeWsButton" class="btn btn-outline-warning">Unsubscribe sélectionnée</button>
|
||||
<button id="disconnectWsButton" class="btn btn-outline-danger">Déconnecter socket</button>
|
||||
<button id="refreshWsPoolButton" class="btn btn-outline-secondary">Rafraîchir endpoints</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-7">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h2 class="h5 card-title mb-0">Statut</h2>
|
||||
<span id="wsStatusBadge" class="badge text-bg-secondary">Déconnecté</span>
|
||||
</div>
|
||||
<textarea id="wsStatusOutput" class="form-control font-monospace" rows="6" readonly>Déconnecté.</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h2 class="h5 card-title mb-0">Endpoints WebSocket</h2>
|
||||
<button id="copyWsPoolButton" class="btn btn-sm btn-outline-secondary">Copier</button>
|
||||
</div>
|
||||
<textarea id="wsPoolOutput" class="form-control font-monospace" rows="8" readonly>Chargement...</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h2 class="h5 card-title mb-0">Messages</h2>
|
||||
<button id="copyWsResultButton" class="btn btn-sm btn-outline-secondary">Copier</button>
|
||||
</div>
|
||||
<textarea id="wsResultOutput" class="form-control font-monospace" rows="16" readonly>Aucune connexion active.</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 text-center text-small my-1 my-md-0">© 2026 SASEDEV</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="ts/demo_ws.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
325
kb-app-demo-desktop/frontend/ts/demo_ws.ts
Normal file
325
kb-app-demo-desktop/frontend/ts/demo_ws.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
// file: kb-app-demo-desktop/frontend/ts/demo_ws.ts
|
||||
// version: 7
|
||||
|
||||
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 {frontendDebug, frontendError, installFrontendConsoleBridge} from "./frontend_log.ts";
|
||||
import type {DemoWsExecutionPayload} from "./bindings/kb_app_demo_desktop/demo_ws/DemoWsExecutionPayload.ts";
|
||||
import type {DemoWsMessagePayload} from "./bindings/kb_app_demo_desktop/demo_ws/DemoWsMessagePayload.ts";
|
||||
import type {DemoWsMethodOption} from "./bindings/kb_app_demo_desktop/demo_ws/DemoWsMethodOption.ts";
|
||||
import type {DemoWsOptionsPayload} from "./bindings/kb_app_demo_desktop/demo_ws/DemoWsOptionsPayload.ts";
|
||||
import type {DemoWsRequest} from "./bindings/kb_app_demo_desktop/demo_ws/DemoWsRequest.ts";
|
||||
import type {DemoWsRoleOption} from "./bindings/kb_app_demo_desktop/demo_ws/DemoWsRoleOption.ts";
|
||||
import type {DemoWsStatusPayload} from "./bindings/kb_app_demo_desktop/demo_ws/DemoWsStatusPayload.ts";
|
||||
import type {DemoWsUnsubscribeRequest} from "./bindings/kb_app_demo_desktop/demo_ws/DemoWsUnsubscribeRequest.ts";
|
||||
|
||||
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
|
||||
let roleOptions: DemoWsRoleOption[] = [];
|
||||
let methodOptions: DemoWsMethodOption[] = [];
|
||||
|
||||
const maxResultOutputCharacters = 200_000;
|
||||
const maxIncomingMessageCharacters = 25_000;
|
||||
|
||||
function textInputValue(selector: string): string {
|
||||
const element = document.querySelector<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>(selector);
|
||||
return element ? element.value.trim() : "";
|
||||
}
|
||||
|
||||
function writeTextarea(selector: string, value: string): void {
|
||||
const element = document.querySelector<HTMLTextAreaElement>(selector);
|
||||
if (element) {
|
||||
element.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
function truncateIncomingMessage(value: string): string {
|
||||
if (value.length <= maxIncomingMessageCharacters) {
|
||||
return value;
|
||||
}
|
||||
return `${value.slice(0, maxIncomingMessageCharacters)}\n... message tronqué côté UI ...`;
|
||||
}
|
||||
|
||||
function capTextareaValue(value: string): string {
|
||||
if (value.length <= maxResultOutputCharacters) {
|
||||
return value;
|
||||
}
|
||||
return `... historique tronqué côté UI ...\n${value.slice(value.length - maxResultOutputCharacters)}`;
|
||||
}
|
||||
|
||||
function appendTextarea(selector: string, value: string): void {
|
||||
const element = document.querySelector<HTMLTextAreaElement>(selector);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
const previous = element.value.trim();
|
||||
const safeValue = truncateIncomingMessage(value);
|
||||
const nextValue = previous.length > 0 ? `${previous}\n\n${safeValue}` : safeValue;
|
||||
element.value = capTextareaValue(nextValue);
|
||||
element.scrollTop = element.scrollHeight;
|
||||
}
|
||||
|
||||
function jsonText(value: unknown): string {
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
function roleSupportsMethod(role: DemoWsRoleOption, method: DemoWsMethodOption): boolean {
|
||||
return role.requestKinds.includes("*") || role.requestKinds.includes(method.requestKind);
|
||||
}
|
||||
|
||||
function selectedRole(): DemoWsRoleOption | null {
|
||||
const role = textInputValue("#wsRoleSelect");
|
||||
return roleOptions.find(option => option.role === role) ?? null;
|
||||
}
|
||||
|
||||
function populateSelect(selector: string, values: Array<{ value: string; label: string }>): void {
|
||||
const select = document.querySelector<HTMLSelectElement>(selector);
|
||||
if (!select) {
|
||||
return;
|
||||
}
|
||||
select.textContent = "";
|
||||
for (const item of values) {
|
||||
const option = document.createElement("option");
|
||||
option.value = item.value;
|
||||
option.textContent = item.label;
|
||||
select.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshMethodList(): void {
|
||||
const role = selectedRole();
|
||||
const filtered = role ? methodOptions.filter(method => roleSupportsMethod(role, method)) : methodOptions;
|
||||
populateSelect("#wsMethodSelect", filtered.map(method => ({
|
||||
value: method.method,
|
||||
label: `${method.method} — ${method.label}`,
|
||||
})));
|
||||
refreshMethodFields();
|
||||
}
|
||||
|
||||
function refreshMethodFields(): void {
|
||||
const methodName = textInputValue("#wsMethodSelect");
|
||||
const method = methodOptions.find(option => option.method === methodName) ?? null;
|
||||
const target = document.querySelector<HTMLInputElement>("#wsTargetInput");
|
||||
const filter = document.querySelector<HTMLTextAreaElement>("#wsFilterInput");
|
||||
const config = document.querySelector<HTMLTextAreaElement>("#wsConfigInput");
|
||||
if (target) {
|
||||
target.disabled = method ? !method.requiresTarget : false;
|
||||
target.placeholder = method && method.requiresTarget ? "Target requis" : "Non requis pour cette méthode";
|
||||
}
|
||||
if (filter) {
|
||||
filter.disabled = method ? !method.requiresFilterJson : false;
|
||||
}
|
||||
if (config) {
|
||||
config.disabled = method ? !method.supportsConfigJson : false;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshSubscriptionSelect(status: DemoWsStatusPayload): void {
|
||||
const select = document.querySelector<HTMLSelectElement>("#wsSubscriptionSelect");
|
||||
const unsubscribeButton = document.querySelector<HTMLButtonElement>("#unsubscribeWsButton");
|
||||
if (!select) {
|
||||
return;
|
||||
}
|
||||
const previous = select.value;
|
||||
select.textContent = "";
|
||||
for (const subscription of status.subscriptions) {
|
||||
const option = document.createElement("option");
|
||||
option.value = String(subscription.subscriptionId);
|
||||
option.textContent = `${subscription.subscriptionId} · ${subscription.method}`;
|
||||
select.appendChild(option);
|
||||
}
|
||||
if (previous.length > 0) {
|
||||
select.value = previous;
|
||||
}
|
||||
if (unsubscribeButton) {
|
||||
unsubscribeButton.disabled = !status.connected || status.subscriptions.length === 0;
|
||||
}
|
||||
}
|
||||
|
||||
function setConnectedUi(status: DemoWsStatusPayload): void {
|
||||
const badge = document.querySelector<HTMLElement>("#wsStatusBadge");
|
||||
const connectButton = document.querySelector<HTMLButtonElement>("#connectWsButton");
|
||||
const disconnectButton = document.querySelector<HTMLButtonElement>("#disconnectWsButton");
|
||||
if (badge) {
|
||||
badge.textContent = status.connected ? `Connecté · ${status.subscriptionCount} subscription(s)` : "Déconnecté";
|
||||
badge.className = status.connected ? "badge text-bg-success" : "badge text-bg-secondary";
|
||||
}
|
||||
if (connectButton) {
|
||||
connectButton.disabled = false;
|
||||
}
|
||||
if (disconnectButton) {
|
||||
disconnectButton.disabled = !status.connected;
|
||||
}
|
||||
refreshSubscriptionSelect(status);
|
||||
writeTextarea("#wsStatusOutput", jsonText(status));
|
||||
}
|
||||
|
||||
function formatWsExecutionPayload(payload: DemoWsExecutionPayload): string {
|
||||
const header = {
|
||||
endpointName: payload.endpointName,
|
||||
provider: payload.provider,
|
||||
endpointUrl: payload.endpointUrl,
|
||||
role: payload.role,
|
||||
method: payload.method,
|
||||
requestKind: payload.requestKind,
|
||||
responseKind: payload.responseKind,
|
||||
subscriptionId: payload.subscriptionId,
|
||||
};
|
||||
return `${JSON.stringify(header, null, 2)}\n\n--- subscription response ---\n${payload.responseJson}`;
|
||||
}
|
||||
|
||||
async function copyTextarea(selector: string): Promise<void> {
|
||||
const element = document.querySelector<HTMLTextAreaElement>(selector);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
await navigator.clipboard.writeText(element.value);
|
||||
}
|
||||
|
||||
async function refreshWsOptions(): Promise<void> {
|
||||
try {
|
||||
const options = await invoke<DemoWsOptionsPayload>("demo_ws_options");
|
||||
roleOptions = options.roles;
|
||||
methodOptions = options.methods;
|
||||
populateSelect("#wsRoleSelect", roleOptions.map(role => ({
|
||||
value: role.role,
|
||||
label: role.role,
|
||||
})));
|
||||
refreshMethodList();
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
writeTextarea("#wsResultOutput", `Erreur options WebSocket : ${message}`);
|
||||
frontendError("kb-app-demo-desktop.frontend.demo_ws", `WebSocket options loading failed: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshWsPool(): Promise<void> {
|
||||
try {
|
||||
const snapshots = await invoke("demo_ws_list_pool_clients");
|
||||
writeTextarea("#wsPoolOutput", jsonText(snapshots));
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
writeTextarea("#wsPoolOutput", `Erreur : ${message}`);
|
||||
frontendError("kb-app-demo-desktop.frontend.demo_ws", `WebSocket pool refresh failed: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshWsStatus(): Promise<void> {
|
||||
try {
|
||||
const status = await invoke<DemoWsStatusPayload>("demo_ws_status");
|
||||
setConnectedUi(status);
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
writeTextarea("#wsStatusOutput", `Erreur statut WebSocket : ${message}`);
|
||||
frontendError("kb-app-demo-desktop.frontend.demo_ws", `WebSocket status failed: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function connectWsRequest(): Promise<void> {
|
||||
const request: DemoWsRequest = {
|
||||
role: textInputValue("#wsRoleSelect"),
|
||||
method: textInputValue("#wsMethodSelect"),
|
||||
target: textInputValue("#wsTargetInput") || null,
|
||||
filterJson: textInputValue("#wsFilterInput") || null,
|
||||
configJson: textInputValue("#wsConfigInput") || null,
|
||||
};
|
||||
writeTextarea("#wsResultOutput", "Souscription en cours sur la connexion WebSocket...");
|
||||
try {
|
||||
const response = await invoke<DemoWsExecutionPayload>("demo_ws_connect", { request });
|
||||
writeTextarea("#wsResultOutput", formatWsExecutionPayload(response));
|
||||
frontendDebug("kb-app-demo-desktop.frontend.demo_ws", `WebSocket subscribed: ${response.method}`);
|
||||
await refreshWsStatus();
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
writeTextarea("#wsResultOutput", `Erreur : ${message}`);
|
||||
frontendError("kb-app-demo-desktop.frontend.demo_ws", `WebSocket subscription failed: ${message}`);
|
||||
await refreshWsStatus();
|
||||
}
|
||||
}
|
||||
|
||||
async function unsubscribeWsRequest(): Promise<void> {
|
||||
const subscriptionIdText = textInputValue("#wsSubscriptionSelect");
|
||||
if (subscriptionIdText.length === 0) {
|
||||
appendTextarea("#wsResultOutput", "Aucune subscription sélectionnée.");
|
||||
return;
|
||||
}
|
||||
const subscriptionId = Number(subscriptionIdText);
|
||||
if (!Number.isSafeInteger(subscriptionId) || subscriptionId < 0) {
|
||||
appendTextarea("#wsResultOutput", "Identifiant de subscription invalide.");
|
||||
return;
|
||||
}
|
||||
const request: DemoWsUnsubscribeRequest = {
|
||||
subscriptionId,
|
||||
};
|
||||
try {
|
||||
const status = await invoke<DemoWsStatusPayload>("demo_ws_unsubscribe", { request });
|
||||
setConnectedUi(status);
|
||||
appendTextarea("#wsResultOutput", `--- unsubscribe ---\nSubscription ${request.subscriptionId} désinscrite.`);
|
||||
frontendDebug("kb-app-demo-desktop.frontend.demo_ws", `WebSocket unsubscribed: ${request.subscriptionId}`);
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
appendTextarea("#wsResultOutput", `Erreur unsubscribe : ${message}`);
|
||||
frontendError("kb-app-demo-desktop.frontend.demo_ws", `WebSocket unsubscribe failed: ${message}`);
|
||||
await refreshWsStatus();
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnectWsRequest(): Promise<void> {
|
||||
try {
|
||||
const status = await invoke<DemoWsStatusPayload>("demo_ws_disconnect");
|
||||
setConnectedUi(status);
|
||||
appendTextarea("#wsResultOutput", "--- disconnect ---\nDéconnexion demandée.");
|
||||
frontendDebug("kb-app-demo-desktop.frontend.demo_ws", "WebSocket disconnected");
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
appendTextarea("#wsResultOutput", `Erreur déconnexion : ${message}`);
|
||||
frontendError("kb-app-demo-desktop.frontend.demo_ws", `WebSocket disconnection failed: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function installWsEventListeners(): Promise<void> {
|
||||
await listen<DemoWsStatusPayload>("demo-ws-status", event => {
|
||||
setConnectedUi(event.payload);
|
||||
});
|
||||
await listen<DemoWsMessagePayload>("demo-ws-message", event => {
|
||||
appendTextarea("#wsResultOutput", `--- ${event.payload.kind} ---\n${event.payload.payloadJson}`);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
installFrontendConsoleBridge("kb-app-demo-desktop.frontend.demo_ws");
|
||||
frontendDebug("kb-app-demo-desktop.frontend.demo_ws", "WebSocket demo window loaded");
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||
document.querySelector<HTMLSelectElement>("#wsRoleSelect")?.addEventListener("change", refreshMethodList);
|
||||
document.querySelector<HTMLSelectElement>("#wsMethodSelect")?.addEventListener("change", refreshMethodFields);
|
||||
document.querySelector<HTMLButtonElement>("#connectWsButton")?.addEventListener("click", () => {
|
||||
void connectWsRequest();
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#unsubscribeWsButton")?.addEventListener("click", () => {
|
||||
void unsubscribeWsRequest();
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#disconnectWsButton")?.addEventListener("click", () => {
|
||||
void disconnectWsRequest();
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#refreshWsPoolButton")?.addEventListener("click", () => {
|
||||
void refreshWsPool();
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#copyWsPoolButton")?.addEventListener("click", () => {
|
||||
void copyTextarea("#wsPoolOutput");
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#copyWsResultButton")?.addEventListener("click", () => {
|
||||
void copyTextarea("#wsResultOutput");
|
||||
});
|
||||
void installWsEventListeners();
|
||||
void refreshWsOptions();
|
||||
void refreshWsPool();
|
||||
void refreshWsStatus();
|
||||
});
|
||||
Reference in New Issue
Block a user