net: prioritize close-event reconnect with 2s delay and 3 retry cap

This commit is contained in:
Jage9
2026-02-22 18:47:09 -05:00
parent d5dbb8289a
commit 7e3553dbde
4 changed files with 41 additions and 10 deletions

View File

@@ -1,5 +1,5 @@
// Maintainer-controlled web client version. // Maintainer-controlled web client version.
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) // Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
window.CHGRID_WEB_VERSION = "2026.02.22 R165"; window.CHGRID_WEB_VERSION = "2026.02.22 R166";
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid. // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
window.CHGRID_TIME_ZONE = "America/Detroit"; window.CHGRID_TIME_ZONE = "America/Detroit";

View File

@@ -78,6 +78,8 @@ const MIC_CALIBRATION_ACTIVE_RMS_THRESHOLD = 0.003;
const MIC_INPUT_GAIN_SCALE_MULTIPLIER = 2; const MIC_INPUT_GAIN_SCALE_MULTIPLIER = 2;
const MIC_INPUT_GAIN_STEP = 0.05; const MIC_INPUT_GAIN_STEP = 0.05;
const HEARTBEAT_INTERVAL_MS = 10_000; const HEARTBEAT_INTERVAL_MS = 10_000;
const RECONNECT_DELAY_MS = 2_000;
const RECONNECT_MAX_ATTEMPTS = 3;
declare global { declare global {
interface Window { interface Window {
@@ -512,6 +514,10 @@ function handleSignalingStatus(message: string): void {
if (message === 'Connected.') { if (message === 'Connected.') {
return; return;
} }
if (message === 'Disconnected.' && state.running && !reconnectInFlight) {
void reconnectAfterSocketClose();
return;
}
pushChatMessage(message); pushChatMessage(message);
} }
@@ -1109,16 +1115,37 @@ function startHeartbeat(): void {
/** Performs one reconnect attempt when heartbeat timeout indicates stale signaling. */ /** Performs one reconnect attempt when heartbeat timeout indicates stale signaling. */
async function reconnectAfterHeartbeatTimeout(): Promise<void> { async function reconnectAfterHeartbeatTimeout(): Promise<void> {
await reconnectWithRetry('heartbeat');
}
/** Performs immediate reconnect when websocket closes unexpectedly. */
async function reconnectAfterSocketClose(): Promise<void> {
await reconnectWithRetry('socketClose');
}
/** Reconnects after disconnect with delay and bounded retry attempts. */
async function reconnectWithRetry(reason: 'heartbeat' | 'socketClose'): Promise<void> {
if (reconnectInFlight || !state.running) return; if (reconnectInFlight || !state.running) return;
reconnectInFlight = true; reconnectInFlight = true;
stopHeartbeat(); stopHeartbeat();
pushChatMessage('Connection stale. Reconnecting...'); if (reason === 'heartbeat') {
disconnect(); pushChatMessage('Connection stale. Reconnecting...');
try {
await connect();
} finally {
reconnectInFlight = false;
} }
disconnect();
for (let attempt = 1; attempt <= RECONNECT_MAX_ATTEMPTS; attempt += 1) {
await new Promise((resolve) => window.setTimeout(resolve, RECONNECT_DELAY_MS));
await connect();
if (state.running) {
reconnectInFlight = false;
return;
}
if (attempt < RECONNECT_MAX_ATTEMPTS) {
pushChatMessage(`Reconnect attempt ${attempt} failed. Retrying...`);
}
}
pushChatMessage('Reconnect failed after 3 attempts. Press Connect to retry.');
audio.sfxUiCancel();
reconnectInFlight = false;
} }
/** Builds dependencies shared by connect/disconnect flow helpers. */ /** Builds dependencies shared by connect/disconnect flow helpers. */

View File

@@ -61,7 +61,9 @@ This is a behavior guide for packet semantics beyond raw schemas.
- Client sends automatic heartbeat `ping` packets every 10 seconds while connected. - Client sends automatic heartbeat `ping` packets every 10 seconds while connected.
- Heartbeat pings use negative `clientSentAt` ids and are internal (not user-visible ping status). - Heartbeat pings use negative `clientSentAt` ids and are internal (not user-visible ping status).
- If a heartbeat `pong` is missed for one interval (10 seconds), client force-disconnects and reconnects. - If websocket close is observed unexpectedly, client starts reconnect flow.
- If a heartbeat `pong` is missed for one interval (10 seconds), client also starts reconnect flow.
- Reconnect flow waits 2 seconds and retries up to 3 times before stopping.
- After reconnect, if `welcome.serverInfo.instanceId` changed, client announces `Server restarted.` - After reconnect, if `welcome.serverInfo.instanceId` changed, client announces `Server restarted.`
- Client emits `Connected. Version <version>.` after each `welcome`. - Client emits `Connected. Version <version>.` after each `welcome`.
- If `welcome.serverInfo.version` differs from running client version, client auto-reloads. - If `welcome.serverInfo.version` differs from running client version, client auto-reloads.

View File

@@ -47,8 +47,10 @@ Core incoming message effects:
## Stale Connection Recovery ## Stale Connection Recovery
- While running, client sends heartbeat `ping` every 10 seconds. - If websocket closes unexpectedly, client starts reconnect flow immediately.
- If one heartbeat `pong` is missed (10-second interval), client auto-reconnects. - While running, client also sends heartbeat `ping` every 10 seconds (fallback for silent half-open cases).
- If one heartbeat `pong` is missed (10-second interval), client starts reconnect flow.
- Reconnect flow waits 2 seconds and retries up to 3 times.
- If reconnect lands on a different `welcome.serverInfo.instanceId`, client announces server restart. - If reconnect lands on a different `welcome.serverInfo.instanceId`, client announces server restart.
- Connect/reconnect status message is emitted from `welcome` and includes server version. - Connect/reconnect status message is emitted from `welcome` and includes server version.