diff --git a/client/public/version.js b/client/public/version.js index dc268b3..118229c 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -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.25 R266"; +window.CHGRID_WEB_VERSION = "2026.02.25 R267"; // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid. window.CHGRID_TIME_ZONE = "America/Detroit"; diff --git a/client/src/main.ts b/client/src/main.ts index 5493340..5184dd3 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -257,6 +257,8 @@ let heartbeatAwaitingPong = false; let reconnectInFlight = false; let activeServerInstanceId: string | null = null; let reloadScheduledForVersionMismatch = false; +let peerNegotiationReady = false; +let pendingSignalMessages: Array> = []; let peerListenGainByNickname = settings.loadPeerListenGains(); let audioLayers: AudioLayerState = { voice: true, @@ -1571,9 +1573,27 @@ function disconnect(): void { lastSubscriptionRefreshTileY = Math.round(state.player.y); stopTeleportLoopAudio(); activeTeleport = null; + peerNegotiationReady = false; + pendingSignalMessages = []; itemBehaviorRegistry.cleanup(); } +/** Starts peer negotiation only after welcome + media setup sequencing is complete. */ +async function activatePeerNegotiation(): Promise { + if (!state.running) return; + if (peerNegotiationReady) return; + peerNegotiationReady = true; + for (const peer of state.peers.values()) { + await peerManager.createOrGetPeer(peer.id, true, peer); + } + if (pendingSignalMessages.length === 0) return; + const queued = pendingSignalMessages; + pendingSignalMessages = []; + for (const signal of queued) { + await onAppMessage(signal); + } +} + const onAppMessage = createOnMessageHandler({ getWorldGridSize: () => worldGridSize, setWorldGridSize: (size) => { @@ -1640,6 +1660,13 @@ const onAppMessage = createOnMessageHandler({ }, handleAuthRequired, handleAuthResult, + isPeerNegotiationReady: () => peerNegotiationReady, + enqueuePendingSignal: (message) => { + pendingSignalMessages.push(message); + if (pendingSignalMessages.length > 500) { + pendingSignalMessages.splice(0, pendingSignalMessages.length - 500); + } + }, }); /** Handles signaling packets with heartbeat/restart metadata before app-level dispatch. */ @@ -1701,12 +1728,14 @@ async function setupMediaAfterAuth(): Promise { const canProceed = await checkMicPermission(); if (!canProceed) { setConnectionStatus('Microphone access is required.'); + await activatePeerNegotiation(); return; } try { await populateAudioDevices(); if (dom.audioInputSelect.options.length === 0) { setConnectionStatus('No audio input device found. Open Audio setup or connect a microphone.'); + await activatePeerNegotiation(); return; } const inputDeviceId = dom.audioInputSelect.value || mediaSession.getPreferredInputDeviceId(); @@ -1714,6 +1743,8 @@ async function setupMediaAfterAuth(): Promise { } catch (error) { console.error(error); setConnectionStatus(describeMediaError(error)); + } finally { + await activatePeerNegotiation(); } } diff --git a/client/src/network/messageHandlers.ts b/client/src/network/messageHandlers.ts index 2b809d4..800d78c 100644 --- a/client/src/network/messageHandlers.ts +++ b/client/src/network/messageHandlers.ts @@ -71,6 +71,8 @@ type MessageHandlerDeps = { playIncomingItemUseSound: (url: string, x: number, y: number) => void; handleAuthRequired: (message: Extract) => void; handleAuthResult: (message: Extract) => Promise; + isPeerNegotiationReady: () => boolean; + enqueuePendingSignal: (message: Extract) => void; }; /** @@ -117,7 +119,6 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco for (const user of message.users) { deps.state.peers.set(user.id, { ...user }); - await deps.peerManager.createOrGetPeer(user.id, true, user); } deps.state.items.clear(); for (const item of message.items || []) { @@ -132,6 +133,18 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco break; case 'signal': { + if (!deps.isPeerNegotiationReady()) { + deps.enqueuePendingSignal(message); + if (!deps.state.peers.has(message.senderId)) { + deps.state.peers.set(message.senderId, { + id: message.senderId, + nickname: deps.sanitizeName(message.senderNickname || 'user...') || 'user...', + x: Number.isFinite(message.x) ? message.x : 20, + y: Number.isFinite(message.y) ? message.y : 20, + }); + } + break; + } const peer = await deps.peerManager.handleSignal(message); if (!deps.state.peers.has(peer.id)) { deps.state.peers.set(peer.id, {