CLAUDE: Fix Safari WebSocket connection issues
Frontend fixes for Safari/iPad WebSocket reliability: 1. Add Vite allowedHosts config (nuxt.config.ts) - Allow gameplay-demo.manticorum.com for external access - Fixes "Blocked request" error after Vite security update 2. Add connection timeout failsafe (useWebSocket.ts) - 10-second timeout resets stuck "isConnecting" state - Prevents Heisenbug where Safari connections hang indefinitely - Auto-schedules reconnection after timeout 3. Add visible debug info to connection modal (games/[id].vue) - Shows WS URL, socket status, and connection log - Helps diagnose Safari-specific issues without dev tools 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
acd080b437
commit
364c5149a4
@ -35,6 +35,7 @@ const MAX_RECONNECTION_ATTEMPTS = 10
|
|||||||
let socketInstance: Socket<ServerToClientEvents, ClientToServerEvents> | null = null
|
let socketInstance: Socket<ServerToClientEvents, ClientToServerEvents> | null = null
|
||||||
let reconnectionAttempts = 0
|
let reconnectionAttempts = 0
|
||||||
let reconnectionTimeout: NodeJS.Timeout | null = null
|
let reconnectionTimeout: NodeJS.Timeout | null = null
|
||||||
|
let connectionTimeoutId: NodeJS.Timeout | null = null // Timeout to prevent stuck "isConnecting" state
|
||||||
|
|
||||||
// Singleton reactive state (shared across all useWebSocket calls)
|
// Singleton reactive state (shared across all useWebSocket calls)
|
||||||
const isConnected = ref(false)
|
const isConnected = ref(false)
|
||||||
@ -84,6 +85,9 @@ export function useWebSocket() {
|
|||||||
// Connection Management
|
// Connection Management
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
// Connection timeout constant
|
||||||
|
const CONNECTION_TIMEOUT = 10000 // 10 seconds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to WebSocket server with JWT authentication
|
* Connect to WebSocket server with JWT authentication
|
||||||
*/
|
*/
|
||||||
@ -103,6 +107,21 @@ export function useWebSocket() {
|
|||||||
|
|
||||||
console.log('[WebSocket] Connecting to', wsUrl)
|
console.log('[WebSocket] Connecting to', wsUrl)
|
||||||
|
|
||||||
|
// Set a timeout to reset isConnecting if no event fires
|
||||||
|
// This prevents the "stuck connecting" state that can occur on Safari
|
||||||
|
if (connectionTimeoutId) {
|
||||||
|
clearTimeout(connectionTimeoutId)
|
||||||
|
}
|
||||||
|
connectionTimeoutId = setTimeout(() => {
|
||||||
|
if (isConnecting.value && !isConnected.value) {
|
||||||
|
console.warn('[WebSocket] Connection timeout - resetting state')
|
||||||
|
isConnecting.value = false
|
||||||
|
connectionError.value = 'Connection timeout - no response from server'
|
||||||
|
// Schedule a reconnection attempt
|
||||||
|
scheduleReconnection()
|
||||||
|
}
|
||||||
|
}, CONNECTION_TIMEOUT)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create or reuse socket instance
|
// Create or reuse socket instance
|
||||||
if (!socketInstance) {
|
if (!socketInstance) {
|
||||||
@ -121,6 +140,10 @@ export function useWebSocket() {
|
|||||||
console.error('[WebSocket] Connection error:', err)
|
console.error('[WebSocket] Connection error:', err)
|
||||||
isConnecting.value = false
|
isConnecting.value = false
|
||||||
connectionError.value = err instanceof Error ? err.message : 'Connection failed'
|
connectionError.value = err instanceof Error ? err.message : 'Connection failed'
|
||||||
|
if (connectionTimeoutId) {
|
||||||
|
clearTimeout(connectionTimeoutId)
|
||||||
|
connectionTimeoutId = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,6 +248,12 @@ export function useWebSocket() {
|
|||||||
connectionError.value = null
|
connectionError.value = null
|
||||||
reconnectionAttempts = 0
|
reconnectionAttempts = 0
|
||||||
|
|
||||||
|
// Clear connection timeout
|
||||||
|
if (connectionTimeoutId) {
|
||||||
|
clearTimeout(connectionTimeoutId)
|
||||||
|
connectionTimeoutId = null
|
||||||
|
}
|
||||||
|
|
||||||
// Update global flag for plugin failsafe
|
// Update global flag for plugin failsafe
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.__ws_connected = true
|
window.__ws_connected = true
|
||||||
|
|||||||
@ -30,6 +30,13 @@ export default defineNuxtConfig({
|
|||||||
port: 3000
|
port: 3000
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Vite config for external hostname access
|
||||||
|
vite: {
|
||||||
|
server: {
|
||||||
|
allowedHosts: ['gameplay-demo.manticorum.com', 'localhost', '127.0.0.1']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
typescript: {
|
typescript: {
|
||||||
strict: true,
|
strict: true,
|
||||||
typeCheck: false // Disable in dev - use `npm run type-check` manually
|
typeCheck: false // Disable in dev - use `npm run type-check` manually
|
||||||
|
|||||||
@ -199,7 +199,13 @@
|
|||||||
<span :class="isConnected ? 'text-green-600' : isConnecting ? 'text-yellow-600' : 'text-red-600'">●</span>
|
<span :class="isConnected ? 'text-green-600' : isConnecting ? 'text-yellow-600' : 'text-red-600'">●</span>
|
||||||
<span class="text-gray-700 dark:text-gray-300">WebSocket: {{ isConnected ? 'Connected' : isConnecting ? 'Connecting...' : 'Disconnected' }}</span>
|
<span class="text-gray-700 dark:text-gray-300">WebSocket: {{ isConnected ? 'Connected' : isConnecting ? 'Connecting...' : 'Disconnected' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="connectionError" class="text-xs text-red-600 mt-2">{{ connectionError }}</p>
|
<p v-if="connectionError" class="text-xs text-red-600 mt-2">Error: {{ connectionError }}</p>
|
||||||
|
<!-- Debug info -->
|
||||||
|
<div class="mt-2 text-xs text-gray-500 border-t pt-2">
|
||||||
|
<p>WS URL: {{ wsDebugUrl }}</p>
|
||||||
|
<p>Socket exists: {{ socketExists }}</p>
|
||||||
|
<p class="mt-1 font-mono text-[10px] max-h-24 overflow-y-auto">{{ debugLog }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action buttons -->
|
<!-- Action buttons -->
|
||||||
@ -355,6 +361,12 @@ const gameId = computed(() => route.params.id as string)
|
|||||||
// WebSocket connection
|
// WebSocket connection
|
||||||
const { socket, isConnected, isConnecting, connectionError, connect, forceReconnect } = useWebSocket()
|
const { socket, isConnected, isConnecting, connectionError, connect, forceReconnect } = useWebSocket()
|
||||||
|
|
||||||
|
// Debug info for troubleshooting Safari WebSocket issues
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const wsDebugUrl = computed(() => config.public.wsUrl || 'not set')
|
||||||
|
const socketExists = computed(() => socket.value ? 'yes' : 'no')
|
||||||
|
const debugLog = ref('Loading...')
|
||||||
|
|
||||||
// Pass the raw string value from route params, not computed value
|
// Pass the raw string value from route params, not computed value
|
||||||
// useGameActions will create its own computed internally if needed
|
// useGameActions will create its own computed internally if needed
|
||||||
const actions = useGameActions(route.params.id as string)
|
const actions = useGameActions(route.params.id as string)
|
||||||
@ -657,10 +669,18 @@ const updateScoreBoardHeight = () => {
|
|||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
// Debug logging for Safari troubleshooting
|
||||||
|
debugLog.value = `Mounted at ${new Date().toLocaleTimeString()}\n`
|
||||||
|
debugLog.value += `isConnected: ${isConnected.value}, isConnecting: ${isConnecting.value}\n`
|
||||||
|
|
||||||
// Try to connect WebSocket immediately - cookies will be sent automatically
|
// Try to connect WebSocket immediately - cookies will be sent automatically
|
||||||
// Backend will authenticate via cookies and reject if invalid
|
// Backend will authenticate via cookies and reject if invalid
|
||||||
if (!isConnected.value && !isConnecting.value) {
|
if (!isConnected.value && !isConnecting.value) {
|
||||||
|
debugLog.value += 'Calling connect()...\n'
|
||||||
connect()
|
connect()
|
||||||
|
debugLog.value += 'connect() called\n'
|
||||||
|
} else {
|
||||||
|
debugLog.value += 'Skipped connect (already connected/connecting)\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also check auth store (for display purposes)
|
// Also check auth store (for display purposes)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user