// file: kb_app_demo/frontend/ts/demo_ws.ts // version: 6 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"; import type {DemoWsExecutionPayload} from "./bindings/kb_app_demo/demo_ws/DemoWsExecutionPayload"; import type {DemoWsMessagePayload} from "./bindings/kb_app_demo/demo_ws/DemoWsMessagePayload"; import type {DemoWsMethodOption} from "./bindings/kb_app_demo/demo_ws/DemoWsMethodOption"; import type {DemoWsOptionsPayload} from "./bindings/kb_app_demo/demo_ws/DemoWsOptionsPayload"; import type {DemoWsRequest} from "./bindings/kb_app_demo/demo_ws/DemoWsRequest"; import type {DemoWsRoleOption} from "./bindings/kb_app_demo/demo_ws/DemoWsRoleOption"; import type {DemoWsStatusPayload} from "./bindings/kb_app_demo/demo_ws/DemoWsStatusPayload"; import type {DemoWsUnsubscribeRequest} from "./bindings/kb_app_demo/demo_ws/DemoWsUnsubscribeRequest"; (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(selector); return element ? element.value.trim() : ""; } function writeTextarea(selector: string, value: string): void { const element = document.querySelector(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(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(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("#wsTargetInput"); const filter = document.querySelector("#wsFilterInput"); const config = document.querySelector("#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("#wsSubscriptionSelect"); const unsubscribeButton = document.querySelector("#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("#wsStatusBadge"); const connectButton = document.querySelector("#connectWsButton"); const disconnectButton = document.querySelector("#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 { const element = document.querySelector(selector); if (!element) { return; } await navigator.clipboard.writeText(element.value); } async function refreshWsOptions(): Promise { try { const options = await invoke("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.frontend.demo_ws", `WebSocket options loading failed: ${message}`); } } async function refreshWsPool(): Promise { 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.frontend.demo_ws", `WebSocket pool refresh failed: ${message}`); } } async function refreshWsStatus(): Promise { try { const status = await invoke("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.frontend.demo_ws", `WebSocket status failed: ${message}`); } } async function connectWsRequest(): Promise { 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("demo_ws_connect", { request }); writeTextarea("#wsResultOutput", formatWsExecutionPayload(response)); frontendDebug("kb_app_demo.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.frontend.demo_ws", `WebSocket subscription failed: ${message}`); await refreshWsStatus(); } } async function unsubscribeWsRequest(): Promise { 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("demo_ws_unsubscribe", { request }); setConnectedUi(status); appendTextarea("#wsResultOutput", `--- unsubscribe ---\nSubscription ${request.subscriptionId} désinscrite.`); frontendDebug("kb_app_demo.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.frontend.demo_ws", `WebSocket unsubscribe failed: ${message}`); await refreshWsStatus(); } } async function disconnectWsRequest(): Promise { try { const status = await invoke("demo_ws_disconnect"); setConnectedUi(status); appendTextarea("#wsResultOutput", "--- disconnect ---\nDéconnexion demandée."); frontendDebug("kb_app_demo.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.frontend.demo_ws", `WebSocket disconnection failed: ${message}`); } } async function installWsEventListeners(): Promise { await listen("demo-ws-status", event => { setConnectedUi(event.payload); }); await listen("demo-ws-message", event => { appendTextarea("#wsResultOutput", `--- ${event.payload.kind} ---\n${event.payload.payloadJson}`); }); } document.addEventListener("DOMContentLoaded", () => { installFrontendConsoleBridge("kb_app_demo.frontend.demo_ws"); frontendDebug("kb_app_demo.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("#wsRoleSelect")?.addEventListener("change", refreshMethodList); document.querySelector("#wsMethodSelect")?.addEventListener("change", refreshMethodFields); document.querySelector("#connectWsButton")?.addEventListener("click", () => { void connectWsRequest(); }); document.querySelector("#unsubscribeWsButton")?.addEventListener("click", () => { void unsubscribeWsRequest(); }); document.querySelector("#disconnectWsButton")?.addEventListener("click", () => { void disconnectWsRequest(); }); document.querySelector("#refreshWsPoolButton")?.addEventListener("click", () => { void refreshWsPool(); }); document.querySelector("#copyWsPoolButton")?.addEventListener("click", () => { void copyTextarea("#wsPoolOutput"); }); document.querySelector("#copyWsResultButton")?.addEventListener("click", () => { void copyTextarea("#wsResultOutput"); }); void installWsEventListeners(); void refreshWsOptions(); void refreshWsPool(); void refreshWsStatus(); });