Files
khadhroony-bot3/kb-app-demo-desktop/frontend/ts/demo_ws.ts
2026-08-10 11:21:07 +02:00

336 lines
15 KiB
TypeScript

// file: kb-app-demo-desktop/frontend/ts/demo_ws.ts
// version: 10
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 {renderJsonViewer} from "./json_viewer.ts";
import type {DemoWsEndpointPayload} from "./bindings/kb_app_demo_desktop/demo_ws/DemoWsEndpointPayload.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[] = [];
let wsMessageBuffer = "Aucune connexion active.";
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 writeWsMessages(value: string): void {
wsMessageBuffer = capTextareaValue(value);
writeTextarea("#wsResultOutput", wsMessageBuffer);
const element = document.querySelector<HTMLTextAreaElement>("#wsResultOutput");
if (element) {
element.scrollTop = element.scrollHeight;
}
}
function appendTextarea(selector: string, value: string): void {
if (selector !== "#wsResultOutput") {
const element = document.querySelector<HTMLTextAreaElement>(selector);
if (element) {
const previous = element.value.trim();
const safeValue = truncateIncomingMessage(value);
element.value = capTextareaValue(previous.length > 0 ? `${previous}\n\n${safeValue}` : safeValue);
element.scrollTop = element.scrollHeight;
}
return;
}
const previous = wsMessageBuffer.trim();
const safeValue = truncateIncomingMessage(value);
writeWsMessages(previous.length > 0 ? `${previous}\n\n${safeValue}` : safeValue);
}
function clearWsMessages(): void {
writeWsMessages("");
}
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);
renderJsonViewer("#wsStatusOutput", status);
}
function formatWsExecutionPayload(payload: DemoWsExecutionPayload): string {
const header = {
endpointName: payload.endpointName,
provider: payload.provider,
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);
writeWsMessages(`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<DemoWsEndpointPayload[]>("demo_ws_list_pool_clients");
renderJsonViewer("#wsPoolOutput", snapshots);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
renderJsonViewer("#wsPoolOutput", {status: "error", 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);
renderJsonViewer("#wsStatusOutput", {status: "error", 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,
};
writeWsMessages("Souscription en cours sur la connexion WebSocket...");
try {
const response = await invoke<DemoWsExecutionPayload>("demo_ws_connect", { request });
writeWsMessages(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);
writeWsMessages(`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>("#copyWsResultButton")?.addEventListener("click", () => {
void copyTextarea("#wsResultOutput");
});
document.querySelector<HTMLButtonElement>("#clearWsResultButton")?.addEventListener("click", clearWsMessages);
void installWsEventListeners();
void refreshWsOptions();
void refreshWsPool();
void refreshWsStatus();
});