From 76a5c1186a3fe3bf9ed83f974d438ec05b4e08a9 Mon Sep 17 00:00:00 2001 From: Jage9 Date: Fri, 20 Feb 2026 16:30:54 -0500 Subject: [PATCH] Fix reconnect/media failure paths and harden signaling parse --- AGENTS.md | 1 + client/public/version.js | 2 +- client/src/main.ts | 45 ++++++++++++++++++--------- client/src/network/signalingClient.ts | 7 ++++- 4 files changed, 38 insertions(+), 17 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6975632..1752367 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,7 @@ ## Versioning & Configuration - 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. - `server/config.toml` is deployment-local and must not be committed. - Production should use TLS (`network.allow_insecure_ws = false`). diff --git a/client/public/version.js b/client/public/version.js index 30bf9ef..c86a7ab 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -1,3 +1,3 @@ // Maintainer-controlled web client version. // 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"; diff --git a/client/src/main.ts b/client/src/main.ts index 4ea52b8..0abff58 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -577,9 +577,7 @@ async function checkMicPermission(): Promise { } async function setupLocalMedia(audioDeviceId = ''): Promise { - if (localStream) { - localStream.getTracks().forEach((track) => track.stop()); - } + stopLocalMedia(); await audio.ensureContext(); @@ -604,6 +602,14 @@ async function setupLocalMedia(audioDeviceId = ''): Promise { await peerManager.replaceOutgoingTrack(outboundStream); } +function stopLocalMedia(): void { + if (localStream) { + localStream.getTracks().forEach((track) => track.stop()); + localStream = null; + } + outboundStream = null; +} + function describeMediaError(error: unknown): string { if (error instanceof DOMException) { if (error.name === 'NotAllowedError') return 'Microphone blocked. Allow mic access in browser site settings.'; @@ -638,14 +644,23 @@ async function connect(): Promise { return; } + state.player.x = Math.floor(Math.random() * GRID_SIZE); + state.player.y = Math.floor(Math.random() * GRID_SIZE); const storedPosition = localStorage.getItem('spatialChatPosition'); if (storedPosition) { - const parsed = JSON.parse(storedPosition) as { x: number; y: number }; - state.player.x = parsed.x; - state.player.y = parsed.y; - } else { - state.player.x = Math.floor(Math.random() * GRID_SIZE); - state.player.y = Math.floor(Math.random() * GRID_SIZE); + try { + const parsed = JSON.parse(storedPosition) as { x?: number; y?: number }; + if (Number.isFinite(parsed.x) && Number.isFinite(parsed.y)) { + const x = Math.floor(parsed.x as number); + const y = Math.floor(parsed.y as number); + 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 { @@ -670,6 +685,7 @@ async function connect(): Promise { await signaling.connect(onMessage); } catch (error) { console.error(error); + stopLocalMedia(); updateStatus('Connect failed. Signaling server may be offline or unreachable.'); connecting = false; updateConnectAvailability(); @@ -686,11 +702,7 @@ function disconnect(): void { } signaling.disconnect(); - if (localStream) { - localStream.getTracks().forEach((track) => track.stop()); - localStream = null; - } - outboundStream = null; + stopLocalMedia(); peerManager.cleanupAll(); cleanupAllRadioRuntimes(); @@ -1627,8 +1639,9 @@ async function populateAudioDevices(): Promise { return; } + let temporaryStream: MediaStream | null = null; try { - await navigator.mediaDevices.getUserMedia({ audio: true }); + temporaryStream = await navigator.mediaDevices.getUserMedia({ audio: true }); const devices = await navigator.mediaDevices.enumerateDevices(); dom.audioInputSelect.innerHTML = ''; @@ -1665,6 +1678,8 @@ async function populateAudioDevices(): Promise { updateDeviceSummary(); } catch { updateStatus('Could not list devices.'); + } finally { + temporaryStream?.getTracks().forEach((track) => track.stop()); } } diff --git a/client/src/network/signalingClient.ts b/client/src/network/signalingClient.ts index cdaf0ac..189c8a1 100644 --- a/client/src/network/signalingClient.ts +++ b/client/src/network/signalingClient.ts @@ -38,7 +38,12 @@ export class SignalingClient { }; 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); if (!validated.success) return; await onMessage(validated.data);