net: sync serverInfo version with web version and reconnect on first missed heartbeat
This commit is contained in:
@@ -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 R160";
|
window.CHGRID_WEB_VERSION = "2026.02.22 R161";
|
||||||
// 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";
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ 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 HEARTBEAT_TIMEOUT_MS = 25_000;
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -199,6 +198,7 @@ let helpViewerIndex = 0;
|
|||||||
let heartbeatTimerId: number | null = null;
|
let heartbeatTimerId: number | null = null;
|
||||||
let heartbeatLastPongAt = 0;
|
let heartbeatLastPongAt = 0;
|
||||||
let heartbeatNextPingId = -1;
|
let heartbeatNextPingId = -1;
|
||||||
|
let heartbeatAwaitingPong = false;
|
||||||
let reconnectInFlight = false;
|
let reconnectInFlight = false;
|
||||||
let activeServerInstanceId: string | null = null;
|
let activeServerInstanceId: string | null = null;
|
||||||
let audioLayers: AudioLayerState = {
|
let audioLayers: AudioLayerState = {
|
||||||
@@ -1050,23 +1050,25 @@ function stopHeartbeat(): void {
|
|||||||
heartbeatTimerId = null;
|
heartbeatTimerId = null;
|
||||||
}
|
}
|
||||||
heartbeatLastPongAt = 0;
|
heartbeatLastPongAt = 0;
|
||||||
|
heartbeatAwaitingPong = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sends one heartbeat ping packet using reserved negative ids. */
|
/** Sends one heartbeat ping packet using reserved negative ids. */
|
||||||
function sendHeartbeatPing(): void {
|
function sendHeartbeatPing(): void {
|
||||||
signaling.send({ type: 'ping', clientSentAt: heartbeatNextPingId });
|
signaling.send({ type: 'ping', clientSentAt: heartbeatNextPingId });
|
||||||
heartbeatNextPingId -= 1;
|
heartbeatNextPingId -= 1;
|
||||||
|
heartbeatAwaitingPong = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Starts heartbeat timer for stale-connection detection. */
|
/** Starts heartbeat timer for stale-connection detection. */
|
||||||
function startHeartbeat(): void {
|
function startHeartbeat(): void {
|
||||||
stopHeartbeat();
|
stopHeartbeat();
|
||||||
heartbeatLastPongAt = Date.now();
|
heartbeatLastPongAt = Date.now();
|
||||||
|
heartbeatAwaitingPong = false;
|
||||||
sendHeartbeatPing();
|
sendHeartbeatPing();
|
||||||
heartbeatTimerId = window.setInterval(() => {
|
heartbeatTimerId = window.setInterval(() => {
|
||||||
if (!state.running) return;
|
if (!state.running) return;
|
||||||
const now = Date.now();
|
if (heartbeatAwaitingPong) {
|
||||||
if (now - heartbeatLastPongAt > HEARTBEAT_TIMEOUT_MS) {
|
|
||||||
void reconnectAfterHeartbeatTimeout();
|
void reconnectAfterHeartbeatTimeout();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1188,6 +1190,7 @@ const onAppMessage = createOnMessageHandler({
|
|||||||
async function onSignalingMessage(message: IncomingMessage): Promise<void> {
|
async function onSignalingMessage(message: IncomingMessage): Promise<void> {
|
||||||
if (message.type === 'pong' && message.clientSentAt < 0) {
|
if (message.type === 'pong' && message.clientSentAt < 0) {
|
||||||
heartbeatLastPongAt = Date.now();
|
heartbeatLastPongAt = Date.now();
|
||||||
|
heartbeatAwaitingPong = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let restartAnnouncement: string | null = null;
|
let restartAnnouncement: string | null = null;
|
||||||
|
|||||||
@@ -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.
|
- 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 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:
|
- After reconnect, if `welcome.serverInfo.instanceId` changed, client announces:
|
||||||
`Server restarted, version <version>.`
|
`Server restarted, version <version>.`
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ Core incoming message effects:
|
|||||||
## Stale Connection Recovery
|
## Stale Connection Recovery
|
||||||
|
|
||||||
- While running, client sends heartbeat `ping` every 10 seconds.
|
- 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.
|
- If reconnect lands on a different `welcome.serverInfo.instanceId`, client announces server restart/version.
|
||||||
|
|
||||||
## Disconnect/Cleanup
|
## Disconnect/Cleanup
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from datetime import datetime
|
|||||||
from importlib.metadata import PackageNotFoundError, version as package_version
|
from importlib.metadata import PackageNotFoundError, version as package_version
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
import ssl
|
import ssl
|
||||||
import uuid
|
import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -90,10 +92,31 @@ class SignalingServer:
|
|||||||
self.item_last_use_ms: dict[str, int] = {}
|
self.item_last_use_ms: dict[str, int] = {}
|
||||||
self.grid_size = max(1, grid_size)
|
self.grid_size = max(1, grid_size)
|
||||||
self.instance_id = str(uuid.uuid4())
|
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:
|
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:
|
except PackageNotFoundError:
|
||||||
self.server_version = "unknown"
|
return "unknown"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def items(self) -> dict[str, WorldItem]:
|
def items(self) -> dict[str, WorldItem]:
|
||||||
|
|||||||
Reference in New Issue
Block a user