fix: QA sprint v1 — 7 issues resolved
Fixes: - #22 OrbitControls damping: call updateControls() in animate loop - #23 Empty catch blocks: add console.warn + error surfacing to chat panel - #24 escapeHtml: add quote escaping (" '), use in renderAgentList - #25 WS reconnect: check close code (1000/1001) before reconnecting, add exponential backoff + heartbeat zombie detection - #26 IDLE state visibility: brighten from near-invisible to #005500 - #5 PWA: manifest.json, service worker (network-first), theme-color, favicon, loading screen, safe-area-inset padding, apple-mobile-web-app - #14 Adaptive render quality: new quality.js hardware detection (low/ medium/high tiers), tiered particle counts, grid density, antialias, pixel ratio caps New files: - js/quality.js — hardware detection + quality tier logic - manifest.json — PWA manifest - public/sw.js — service worker (network-first with offline cache) - public/favicon.svg — SVG favicon - icons/icon-192.svg, icons/icon-512.svg — PWA icons
This commit is contained in:
@@ -10,7 +10,15 @@ let ws = null;
|
||||
let connectionState = 'disconnected';
|
||||
let jobCount = 0;
|
||||
let reconnectTimer = null;
|
||||
const RECONNECT_DELAY_MS = 5000;
|
||||
let reconnectAttempts = 0;
|
||||
|
||||
const RECONNECT_BASE_MS = 2000;
|
||||
const RECONNECT_MAX_MS = 30000;
|
||||
const HEARTBEAT_INTERVAL_MS = 30000;
|
||||
const HEARTBEAT_TIMEOUT_MS = 5000;
|
||||
|
||||
let heartbeatTimer = null;
|
||||
let heartbeatTimeout = null;
|
||||
|
||||
export function initWebSocket(_scene) {
|
||||
if (!WS_URL) {
|
||||
@@ -30,7 +38,9 @@ function connect() {
|
||||
|
||||
try {
|
||||
ws = new WebSocket(WS_URL);
|
||||
} catch {
|
||||
} catch (err) {
|
||||
console.warn('[Matrix WS] Connection failed:', err.message || err);
|
||||
logEvent('WebSocket connection failed');
|
||||
connectionState = 'disconnected';
|
||||
scheduleReconnect();
|
||||
return;
|
||||
@@ -38,7 +48,9 @@ function connect() {
|
||||
|
||||
ws.onopen = () => {
|
||||
connectionState = 'connected';
|
||||
reconnectAttempts = 0;
|
||||
clearTimeout(reconnectTimer);
|
||||
startHeartbeat();
|
||||
ws.send(JSON.stringify({
|
||||
type: 'subscribe',
|
||||
channel: 'agents',
|
||||
@@ -47,27 +59,75 @@ function connect() {
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
// Any message counts as a heartbeat response
|
||||
resetHeartbeatTimeout();
|
||||
try {
|
||||
handleMessage(JSON.parse(event.data));
|
||||
} catch {
|
||||
} catch (err) {
|
||||
console.warn('[Matrix WS] Message parse/handle error:', err.message, '| raw:', event.data?.slice?.(0, 200));
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
ws.onerror = (event) => {
|
||||
console.warn('[Matrix WS] Error event:', event);
|
||||
connectionState = 'disconnected';
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
ws.onclose = (event) => {
|
||||
connectionState = 'disconnected';
|
||||
stopHeartbeat();
|
||||
|
||||
// Don't reconnect on clean close (1000) or going away (1001)
|
||||
if (event.code === 1000 || event.code === 1001) {
|
||||
console.info('[Matrix WS] Clean close (code ' + event.code + '), not reconnecting');
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn('[Matrix WS] Unexpected close — code:', event.code, 'reason:', event.reason || '(none)');
|
||||
scheduleReconnect();
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleReconnect() {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = setTimeout(connect, RECONNECT_DELAY_MS);
|
||||
// Exponential backoff: 2s, 4s, 8s, 16s, capped at 30s
|
||||
const delay = Math.min(RECONNECT_BASE_MS * Math.pow(2, reconnectAttempts), RECONNECT_MAX_MS);
|
||||
reconnectAttempts++;
|
||||
console.info('[Matrix WS] Reconnecting in', Math.round(delay / 1000), 's (attempt', reconnectAttempts + ')');
|
||||
reconnectTimer = setTimeout(connect, delay);
|
||||
}
|
||||
|
||||
/* ── Heartbeat / zombie connection detection ── */
|
||||
|
||||
function startHeartbeat() {
|
||||
stopHeartbeat();
|
||||
heartbeatTimer = setInterval(() => {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
ws.send(JSON.stringify({ type: 'ping' }));
|
||||
} catch { /* ignore send failures, onclose will fire */ }
|
||||
heartbeatTimeout = setTimeout(() => {
|
||||
console.warn('[Matrix WS] Heartbeat timeout — closing zombie connection');
|
||||
if (ws) ws.close(4000, 'heartbeat timeout');
|
||||
}, HEARTBEAT_TIMEOUT_MS);
|
||||
}
|
||||
}, HEARTBEAT_INTERVAL_MS);
|
||||
}
|
||||
|
||||
function stopHeartbeat() {
|
||||
clearInterval(heartbeatTimer);
|
||||
clearTimeout(heartbeatTimeout);
|
||||
heartbeatTimer = null;
|
||||
heartbeatTimeout = null;
|
||||
}
|
||||
|
||||
function resetHeartbeatTimeout() {
|
||||
clearTimeout(heartbeatTimeout);
|
||||
heartbeatTimeout = null;
|
||||
}
|
||||
|
||||
/* ── Message handler ── */
|
||||
|
||||
function handleMessage(msg) {
|
||||
switch (msg.type) {
|
||||
case 'agent_state': {
|
||||
@@ -95,15 +155,17 @@ function handleMessage(msg) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'pong':
|
||||
case 'agent_count':
|
||||
break;
|
||||
default:
|
||||
console.debug('[Matrix WS] Unhandled message type:', msg.type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function logEvent(text) {
|
||||
appendChatMessage('SYS', text, colorToCss(0x003300));
|
||||
appendChatMessage('SYS', text, '#005500');
|
||||
}
|
||||
|
||||
export function getConnectionState() {
|
||||
|
||||
Reference in New Issue
Block a user