// panes/websocket.js — generic WebSocket console with URL switcher, presets, // JSON pretty-print, auto-reconnect, message history. import { registerPane } from '../app.js'; import { escapeHtml } from '../lib/api.js'; import { ok, err, copyToClipboard, historyStore } from '../lib/ui.js'; const ENDPOINTS = [ { label: 'echo', url: '/ws/echo' }, ]; const sendHistory = historyStore('cx_ws_send_history', 30); const TPL = `
WebSocket
Connect to any WebSocket endpoint and inspect frames.
idle
0 in · 0 out · 0 b

Frames

0

      

Send presets

History

0
`; let ws = null; let stats = { in: 0, out: 0, bytes: 0 }; let reconnectTimer = null; function setState(host, state, klass) { host.querySelector('#ws-state').textContent = state; const p = host.querySelector('#ws-pill'); p.classList.remove('ok', 'err', 'warn', 'info', 'muted'); p.classList.add(klass || 'muted'); } function updateStats(host) { host.querySelector('#ws-stats').textContent = `${stats.in} in · ${stats.out} out · ${stats.bytes} b`; host.querySelector('#ws-frame-count').textContent = `${stats.in + stats.out}`; } function maybePretty(host, raw) { if (!host.querySelector('#ws-pretty').checked) return raw; try { return JSON.stringify(JSON.parse(raw), null, 2); } catch { return raw; } } function append(host, dir, raw) { const pre = host.querySelector('#ws-log'); const showTs = host.querySelector('#ws-ts').checked; const text = maybePretty(host, raw); const ts = showTs ? `[${new Date().toISOString().slice(11, 23)}] ` : ''; const arrow = dir === 'in' ? '←' : dir === 'out' ? '→' : '·'; pre.textContent += `${ts}${arrow} ${text}\n`; pre.scrollTop = pre.scrollHeight; } function defaultUrl() { const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; return `${proto}//${location.host}/ws/echo`; } function connect(host) { if (ws && ws.readyState <= 1) return; let url = host.querySelector('#ws-url').value.trim(); if (url.startsWith('/')) { const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; url = `${proto}//${location.host}${url}`; } if (!url) url = defaultUrl(); setState(host, 'connecting…', 'warn'); append(host, '·', `connecting ${url}`); try { ws = new WebSocket(url); } catch (e) { setState(host, 'error', 'err'); append(host, '·', e.message); return; } ws.onopen = () => { setState(host, 'connected', 'ok'); append(host, '·', 'connected'); }; ws.onclose = (e) => { setState(host, `closed (${e.code})`, 'err'); append(host, '·', `closed code=${e.code} reason=${e.reason}`); if (host.querySelector('#ws-autoreconnect').checked) { reconnectTimer = setTimeout(() => connect(host), 2000); } }; ws.onerror = () => append(host, '·', 'error'); ws.onmessage = (e) => { stats.in++; stats.bytes += (e.data?.length || 0); updateStats(host); append(host, 'in', e.data); }; } function disconnect() { if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } try { ws?.close(); } catch {} } function renderHistory(host) { const items = sendHistory.list(); host.querySelector('#ws-hist-count').textContent = String(items.length); host.querySelector('#ws-history').innerHTML = items.length ? items.map((h, i) => ``).join('') : '
empty
'; host.querySelectorAll('#ws-history [data-hi]').forEach(b => { b.addEventListener('click', () => { host.querySelector('#ws-input').value = sendHistory.list()[+b.dataset.hi]?.text || ''; }); }); } function send(host, text) { if (!ws || ws.readyState !== 1) return err('not connected'); ws.send(text); stats.out++; stats.bytes += text.length; updateStats(host); append(host, 'out', text); sendHistory.push({ text }); renderHistory(host); } registerPane('websocket', { label: 'WebSocket', init(host) { host.innerHTML = TPL; const sel = host.querySelector('#ws-preset'); sel.innerHTML = ENDPOINTS.map((e, i) => ``).join(''); host.querySelector('#ws-url').value = defaultUrl(); sel.addEventListener('change', () => { const e = ENDPOINTS[+sel.value]; if (!e) return; const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; host.querySelector('#ws-url').value = `${proto}//${location.host}${e.url}`; }); host.querySelector('#ws-connect').addEventListener('click', () => connect(host)); host.querySelector('#ws-disconnect').addEventListener('click', disconnect); host.querySelector('#ws-clear').addEventListener('click', () => { host.querySelector('#ws-log').textContent = ''; stats = { in: 0, out: 0, bytes: 0 }; updateStats(host); }); host.querySelector('#ws-copy').addEventListener('click', () => copyToClipboard(host.querySelector('#ws-log').textContent)); host.querySelector('#ws-form').addEventListener('submit', (e) => { e.preventDefault(); const v = host.querySelector('#ws-input').value; if (!v) return; send(host, v); host.querySelector('#ws-input').value = ''; }); host.querySelector('#ws-input').addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); host.querySelector('#ws-form').requestSubmit(); } }); host.querySelectorAll('[data-q]').forEach(b => b.addEventListener('click', () => send(host, b.dataset.q))); renderHistory(host); updateStats(host); }, });