CLAUDE: Fix WebSocket SSR hydration and add connection debugging
Problem: WebSocket wouldn't connect on page load because Pinia auth state doesn't automatically transfer from SSR to client hydration. The auth store showed isAuthenticated=false on client mount. Solution: - useWebSocket.onMounted now proactively calls checkAuth() if not authenticated - This ensures auth state is initialized before attempting WebSocket connection - Added forceReconnect() function to clear stale singleton connections Debug UI (temporary): - Added connection status debug info to loading overlay and banner - Shows Auth/Connected/Connecting/Error states - Retry button triggers auth check + reconnect 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b12905a71b
commit
ee12f6210e
@ -139,6 +139,38 @@ export function useWebSocket() {
|
||||
reconnectionAttempts = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Force reconnect - clears the singleton and creates a fresh connection.
|
||||
* Use this when the connection is in a bad state and normal reconnection isn't working.
|
||||
*/
|
||||
function forceReconnect() {
|
||||
console.log('[WebSocket] Force reconnecting - clearing singleton')
|
||||
|
||||
// Clear reconnection timer
|
||||
if (reconnectionTimeout) {
|
||||
clearTimeout(reconnectionTimeout)
|
||||
reconnectionTimeout = null
|
||||
}
|
||||
|
||||
// Fully disconnect and destroy the socket instance
|
||||
if (socketInstance) {
|
||||
socketInstance.removeAllListeners()
|
||||
socketInstance.disconnect()
|
||||
socketInstance = null
|
||||
}
|
||||
|
||||
// Reset all state
|
||||
isConnected.value = false
|
||||
isConnecting.value = false
|
||||
connectionError.value = null
|
||||
reconnectionAttempts = 0
|
||||
|
||||
// Reconnect after a short delay
|
||||
setTimeout(() => {
|
||||
connect()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnect with exponential backoff
|
||||
*/
|
||||
@ -473,8 +505,10 @@ export function useWebSocket() {
|
||||
// Watch for auth changes (not immediate - onMounted handles initial state)
|
||||
watch(
|
||||
() => authStore.isAuthenticated,
|
||||
(authenticated) => {
|
||||
(authenticated, oldValue) => {
|
||||
console.log('[WebSocket] Auth changed:', oldValue, '->', authenticated)
|
||||
if (authenticated && !isConnected.value && !isConnecting.value) {
|
||||
console.log('[WebSocket] Auth became true, connecting...')
|
||||
connect()
|
||||
startHeartbeat()
|
||||
} else if (!authenticated && isConnected.value) {
|
||||
@ -489,9 +523,18 @@ export function useWebSocket() {
|
||||
// ============================================================================
|
||||
|
||||
// Initial connection on client-side mount (handles SSR hydration case)
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
console.log('[WebSocket] onMounted - isAuthenticated:', authStore.isAuthenticated)
|
||||
|
||||
// If not authenticated, try to check auth first (handles SSR hydration case)
|
||||
if (!authStore.isAuthenticated) {
|
||||
console.log('[WebSocket] Not authenticated on mount, checking auth...')
|
||||
await authStore.checkAuth()
|
||||
console.log('[WebSocket] After checkAuth - isAuthenticated:', authStore.isAuthenticated)
|
||||
}
|
||||
|
||||
if (authStore.isAuthenticated && !isConnected.value && !isConnecting.value) {
|
||||
console.log('[WebSocket] Auto-connecting on mount (already authenticated)')
|
||||
console.log('[WebSocket] Auto-connecting on mount (authenticated)')
|
||||
connect()
|
||||
startHeartbeat()
|
||||
}
|
||||
@ -519,5 +562,6 @@ export function useWebSocket() {
|
||||
// Actions
|
||||
connect,
|
||||
disconnect,
|
||||
forceReconnect,
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,18 +19,32 @@
|
||||
v-if="!isConnected"
|
||||
class="mb-4 bg-yellow-50 border-l-4 border-yellow-400 p-4 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-yellow-400 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-yellow-700">
|
||||
{{ connectionStatus === 'connecting' ? 'Connecting to game server...' : 'Disconnected from server. Attempting to reconnect...' }}
|
||||
</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-yellow-400 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-yellow-700">
|
||||
{{ connectionStatus === 'connecting' ? 'Connecting to game server...' : 'Disconnected from server. Attempting to reconnect...' }}
|
||||
</p>
|
||||
<!-- Debug info -->
|
||||
<p class="text-xs text-yellow-600 mt-1">
|
||||
Auth: {{ authStore.isAuthenticated ? 'Yes' : 'No' }} |
|
||||
Connecting: {{ isConnecting ? 'Yes' : 'No' }} |
|
||||
Error: {{ connectionError || 'None' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="forceReconnect"
|
||||
class="ml-4 px-3 py-1 text-sm font-medium text-yellow-700 bg-yellow-100 hover:bg-yellow-200 rounded-md transition"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -169,6 +183,21 @@
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl p-8 shadow-2xl text-center">
|
||||
<div class="w-16 h-16 mx-auto mb-4 border-4 border-primary border-t-transparent rounded-full animate-spin"/>
|
||||
<p class="text-gray-900 dark:text-white font-semibold">Loading game...</p>
|
||||
<!-- Debug info -->
|
||||
<p class="text-xs text-gray-500 mt-4">
|
||||
Auth: {{ authStore.isAuthenticated ? 'Yes' : 'No' }} |
|
||||
Connected: {{ isConnected ? 'Yes' : 'No' }} |
|
||||
Connecting: {{ isConnecting ? 'Yes' : 'No' }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Error: {{ connectionError || 'None' }}
|
||||
</p>
|
||||
<button
|
||||
@click="retryWithAuth"
|
||||
class="mt-4 px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition"
|
||||
>
|
||||
Retry Connection
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -298,7 +327,7 @@ const uiStore = useUiStore()
|
||||
const gameId = computed(() => route.params.id as string)
|
||||
|
||||
// WebSocket connection
|
||||
const { socket, isConnected, connectionError, connect } = useWebSocket()
|
||||
const { socket, isConnected, isConnecting, connectionError, connect, forceReconnect } = useWebSocket()
|
||||
|
||||
// Pass the raw string value from route params, not computed value
|
||||
// useGameActions will create its own computed internally if needed
|
||||
@ -571,6 +600,27 @@ const handleUndoLastPlay = () => {
|
||||
undoLastPlay(1)
|
||||
}
|
||||
|
||||
// Retry connection with auth check
|
||||
const retryWithAuth = async () => {
|
||||
console.log('[Game Page] Retry with auth check')
|
||||
isLoading.value = true
|
||||
connectionStatus.value = 'connecting'
|
||||
|
||||
// First check auth
|
||||
const isAuthed = await authStore.checkAuth()
|
||||
console.log('[Game Page] Auth result:', isAuthed, 'isAuthenticated:', authStore.isAuthenticated)
|
||||
|
||||
if (!isAuthed) {
|
||||
console.error('[Game Page] Auth failed')
|
||||
isLoading.value = false
|
||||
connectionStatus.value = 'disconnected'
|
||||
return
|
||||
}
|
||||
|
||||
// Force reconnect
|
||||
forceReconnect()
|
||||
}
|
||||
|
||||
// Measure ScoreBoard height dynamically
|
||||
const updateScoreBoardHeight = () => {
|
||||
if (scoreBoardRef.value) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user