Fix reconnect/media failure paths and harden signaling parse

This commit is contained in:
Jage9
2026-02-20 16:30:54 -05:00
parent b246c9a7fd
commit 76a5c1186a
4 changed files with 38 additions and 17 deletions

View File

@@ -27,6 +27,7 @@
## Versioning & Configuration ## Versioning & Configuration
- Bump `client/public/version.js` on every user-visible change using `YYYY.MM.DD Rn`. - Bump `client/public/version.js` on every user-visible change using `YYYY.MM.DD Rn`.
- Commit each completed logical change; include the version bump in that same commit when client behavior changes.
- Do not duplicate version constants elsewhere in client code. - Do not duplicate version constants elsewhere in client code.
- `server/config.toml` is deployment-local and must not be committed. - `server/config.toml` is deployment-local and must not be committed.
- Production should use TLS (`network.allow_insecure_ws = false`). - Production should use TLS (`network.allow_insecure_ws = false`).

View File

@@ -1,3 +1,3 @@
// 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.20 R57"; window.CHGRID_WEB_VERSION = "2026.02.20 R58";

View File

@@ -577,9 +577,7 @@ async function checkMicPermission(): Promise<boolean> {
} }
async function setupLocalMedia(audioDeviceId = ''): Promise<void> { async function setupLocalMedia(audioDeviceId = ''): Promise<void> {
if (localStream) { stopLocalMedia();
localStream.getTracks().forEach((track) => track.stop());
}
await audio.ensureContext(); await audio.ensureContext();
@@ -604,6 +602,14 @@ async function setupLocalMedia(audioDeviceId = ''): Promise<void> {
await peerManager.replaceOutgoingTrack(outboundStream); await peerManager.replaceOutgoingTrack(outboundStream);
} }
function stopLocalMedia(): void {
if (localStream) {
localStream.getTracks().forEach((track) => track.stop());
localStream = null;
}
outboundStream = null;
}
function describeMediaError(error: unknown): string { function describeMediaError(error: unknown): string {
if (error instanceof DOMException) { if (error instanceof DOMException) {
if (error.name === 'NotAllowedError') return 'Microphone blocked. Allow mic access in browser site settings.'; if (error.name === 'NotAllowedError') return 'Microphone blocked. Allow mic access in browser site settings.';
@@ -638,14 +644,23 @@ async function connect(): Promise<void> {
return; return;
} }
state.player.x = Math.floor(Math.random() * GRID_SIZE);
state.player.y = Math.floor(Math.random() * GRID_SIZE);
const storedPosition = localStorage.getItem('spatialChatPosition'); const storedPosition = localStorage.getItem('spatialChatPosition');
if (storedPosition) { if (storedPosition) {
const parsed = JSON.parse(storedPosition) as { x: number; y: number }; try {
state.player.x = parsed.x; const parsed = JSON.parse(storedPosition) as { x?: number; y?: number };
state.player.y = parsed.y; if (Number.isFinite(parsed.x) && Number.isFinite(parsed.y)) {
} else { const x = Math.floor(parsed.x as number);
state.player.x = Math.floor(Math.random() * GRID_SIZE); const y = Math.floor(parsed.y as number);
state.player.y = Math.floor(Math.random() * GRID_SIZE); if (x >= 0 && x < GRID_SIZE && y >= 0 && y < GRID_SIZE) {
state.player.x = x;
state.player.y = y;
}
}
} catch {
// Ignore malformed saved positions and keep randomized defaults.
}
} }
try { try {
@@ -670,6 +685,7 @@ async function connect(): Promise<void> {
await signaling.connect(onMessage); await signaling.connect(onMessage);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
stopLocalMedia();
updateStatus('Connect failed. Signaling server may be offline or unreachable.'); updateStatus('Connect failed. Signaling server may be offline or unreachable.');
connecting = false; connecting = false;
updateConnectAvailability(); updateConnectAvailability();
@@ -686,11 +702,7 @@ function disconnect(): void {
} }
signaling.disconnect(); signaling.disconnect();
if (localStream) { stopLocalMedia();
localStream.getTracks().forEach((track) => track.stop());
localStream = null;
}
outboundStream = null;
peerManager.cleanupAll(); peerManager.cleanupAll();
cleanupAllRadioRuntimes(); cleanupAllRadioRuntimes();
@@ -1627,8 +1639,9 @@ async function populateAudioDevices(): Promise<void> {
return; return;
} }
let temporaryStream: MediaStream | null = null;
try { try {
await navigator.mediaDevices.getUserMedia({ audio: true }); temporaryStream = await navigator.mediaDevices.getUserMedia({ audio: true });
const devices = await navigator.mediaDevices.enumerateDevices(); const devices = await navigator.mediaDevices.enumerateDevices();
dom.audioInputSelect.innerHTML = ''; dom.audioInputSelect.innerHTML = '';
@@ -1665,6 +1678,8 @@ async function populateAudioDevices(): Promise<void> {
updateDeviceSummary(); updateDeviceSummary();
} catch { } catch {
updateStatus('Could not list devices.'); updateStatus('Could not list devices.');
} finally {
temporaryStream?.getTracks().forEach((track) => track.stop());
} }
} }

View File

@@ -38,7 +38,12 @@ export class SignalingClient {
}; };
this.ws.onmessage = async (event) => { this.ws.onmessage = async (event) => {
const parsed = JSON.parse(String(event.data)); let parsed: unknown;
try {
parsed = JSON.parse(String(event.data));
} catch {
return;
}
const validated = incomingMessageSchema.safeParse(parsed); const validated = incomingMessageSchema.safeParse(parsed);
if (!validated.success) return; if (!validated.success) return;
await onMessage(validated.data); await onMessage(validated.data);