net: sync serverInfo version with web version and reconnect on first missed heartbeat

This commit is contained in:
Jage9
2026-02-22 18:24:53 -05:00
parent c7c30f234d
commit 0cfba9f1a7
5 changed files with 34 additions and 8 deletions

View File

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

View File

@@ -78,7 +78,6 @@ const MIC_CALIBRATION_ACTIVE_RMS_THRESHOLD = 0.003;
const MIC_INPUT_GAIN_SCALE_MULTIPLIER = 2;
const MIC_INPUT_GAIN_STEP = 0.05;
const HEARTBEAT_INTERVAL_MS = 10_000;
const HEARTBEAT_TIMEOUT_MS = 25_000;
declare global {
interface Window {
@@ -199,6 +198,7 @@ let helpViewerIndex = 0;
let heartbeatTimerId: number | null = null;
let heartbeatLastPongAt = 0;
let heartbeatNextPingId = -1;
let heartbeatAwaitingPong = false;
let reconnectInFlight = false;
let activeServerInstanceId: string | null = null;
let audioLayers: AudioLayerState = {
@@ -1050,23 +1050,25 @@ function stopHeartbeat(): void {
heartbeatTimerId = null;
}
heartbeatLastPongAt = 0;
heartbeatAwaitingPong = false;
}
/** Sends one heartbeat ping packet using reserved negative ids. */
function sendHeartbeatPing(): void {
signaling.send({ type: 'ping', clientSentAt: heartbeatNextPingId });
heartbeatNextPingId -= 1;
heartbeatAwaitingPong = true;
}
/** Starts heartbeat timer for stale-connection detection. */
function startHeartbeat(): void {
stopHeartbeat();
heartbeatLastPongAt = Date.now();
heartbeatAwaitingPong = false;
sendHeartbeatPing();
heartbeatTimerId = window.setInterval(() => {
if (!state.running) return;
const now = Date.now();
if (now - heartbeatLastPongAt > HEARTBEAT_TIMEOUT_MS) {
if (heartbeatAwaitingPong) {
void reconnectAfterHeartbeatTimeout();
return;
}
@@ -1188,6 +1190,7 @@ const onAppMessage = createOnMessageHandler({
async function onSignalingMessage(message: IncomingMessage): Promise<void> {
if (message.type === 'pong' && message.clientSentAt < 0) {
heartbeatLastPongAt = Date.now();
heartbeatAwaitingPong = false;
return;
}
let restartAnnouncement: string | null = null;

View File

@@ -61,6 +61,6 @@ This is a behavior guide for packet semantics beyond raw schemas.
- 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).
- If no heartbeat `pong` arrives within 25 seconds, client force-disconnects and reconnects.
- If a heartbeat `pong` is missed for one interval (10 seconds), client force-disconnects and reconnects.
- After reconnect, if `welcome.serverInfo.instanceId` changed, client announces:
`Server restarted, version <version>.`

View File

@@ -47,7 +47,7 @@ Core incoming message effects:
## Stale Connection Recovery
- While running, client sends heartbeat `ping` every 10 seconds.
- If no heartbeat `pong` is observed for 25 seconds, client auto-reconnects.
- If one heartbeat `pong` is missed (10-second interval), client auto-reconnects.
- If reconnect lands on a different `welcome.serverInfo.instanceId`, client announces server restart/version.
## Disconnect/Cleanup

View File

@@ -8,6 +8,8 @@ from datetime import datetime
from importlib.metadata import PackageNotFoundError, version as package_version
import json
import logging
import os
import re
import ssl
import uuid
from pathlib import Path
@@ -90,10 +92,31 @@ class SignalingServer:
self.item_last_use_ms: dict[str, int] = {}
self.grid_size = max(1, grid_size)
self.instance_id = str(uuid.uuid4())
self.server_version = self._resolve_server_version()
@staticmethod
def _resolve_server_version() -> str:
"""Resolve serverInfo version, preferring synced web version when available."""
env_override = os.getenv("CHGRID_SERVER_VERSION", "").strip()
if env_override:
return env_override
try:
self.server_version = package_version("chgrid-server")
version_file = Path(__file__).resolve().parents[2] / "client" / "public" / "version.js"
text = version_file.read_text(encoding="utf-8")
match = re.search(r'CHGRID_WEB_VERSION\s*=\s*"([^"]+)"', text)
if match:
token = match.group(1).strip()
if token:
return token
except OSError:
pass
try:
return package_version("chgrid-server")
except PackageNotFoundError:
self.server_version = "unknown"
return "unknown"
@property
def items(self) -> dict[str, WorldItem]: