CLAUDE: Fix infinite WebSocket reconnection loop (HIGH-002)
After max reconnection attempts (10), the stuck state detector was continuing to retry forever. Now: - Add permanentlyFailed state flag to track when we've given up - Set flag and stop stuck state detector when max attempts reached - Add manualRetry() method for UI to reset state and try again - Expose permanentlyFailed in public API for error boundary (HIGH-001) Also marks CRIT-002 and CRIT-002-BACKEND as completed in project plan. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
89a63af2a8
commit
1a7e464990
@ -47,8 +47,8 @@
|
|||||||
"description": "myTeamId is hardcoded to null with a TODO comment, breaking the substitution panel's ability to determine which team's lineup to display. Users cannot see or manage their own team's substitutions.",
|
"description": "myTeamId is hardcoded to null with a TODO comment, breaking the substitution panel's ability to determine which team's lineup to display. Users cannot see or manage their own team's substitutions.",
|
||||||
"category": "critical",
|
"category": "critical",
|
||||||
"priority": 2,
|
"priority": 2,
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": true,
|
||||||
"dependencies": ["CRIT-002-BACKEND"],
|
"dependencies": ["CRIT-002-BACKEND"],
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
@ -67,8 +67,8 @@
|
|||||||
"description": "Backend dependency for CRIT-002. The /api/auth/me endpoint needs to return the user's team ownership information so frontend can determine if user owns home/away team.",
|
"description": "Backend dependency for CRIT-002. The /api/auth/me endpoint needs to return the user's team ownership information so frontend can determine if user owns home/away team.",
|
||||||
"category": "critical",
|
"category": "critical",
|
||||||
"priority": 2,
|
"priority": 2,
|
||||||
"completed": false,
|
"completed": true,
|
||||||
"tested": false,
|
"tested": true,
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -85,6 +85,7 @@ const isConnected = ref(false)
|
|||||||
const isConnecting = ref(false)
|
const isConnecting = ref(false)
|
||||||
const connectionError = ref<string | null>(null)
|
const connectionError = ref<string | null>(null)
|
||||||
const lastConnectionAttempt = ref<number | null>(null)
|
const lastConnectionAttempt = ref<number | null>(null)
|
||||||
|
const permanentlyFailed = ref(false) // Set when max reconnection attempts reached
|
||||||
|
|
||||||
// Reset reactive state on client hydration to ensure clean slate
|
// Reset reactive state on client hydration to ensure clean slate
|
||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
@ -93,6 +94,7 @@ if (import.meta.client) {
|
|||||||
isConnecting.value = false
|
isConnecting.value = false
|
||||||
connectionError.value = null
|
connectionError.value = null
|
||||||
lastConnectionAttempt.value = null
|
lastConnectionAttempt.value = null
|
||||||
|
permanentlyFailed.value = false
|
||||||
console.log('[WebSocket] Reactive state reset on client hydration')
|
console.log('[WebSocket] Reactive state reset on client hydration')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,6 +135,7 @@ export function useWebSocket() {
|
|||||||
const state = getClientState()
|
const state = getClientState()
|
||||||
return (
|
return (
|
||||||
!isConnected.value &&
|
!isConnected.value &&
|
||||||
|
!permanentlyFailed.value &&
|
||||||
canConnect.value &&
|
canConnect.value &&
|
||||||
state.reconnectionAttempts < MAX_RECONNECTION_ATTEMPTS
|
state.reconnectionAttempts < MAX_RECONNECTION_ATTEMPTS
|
||||||
)
|
)
|
||||||
@ -252,6 +255,7 @@ export function useWebSocket() {
|
|||||||
isConnected.value = false
|
isConnected.value = false
|
||||||
isConnecting.value = false
|
isConnecting.value = false
|
||||||
connectionError.value = null
|
connectionError.value = null
|
||||||
|
permanentlyFailed.value = false
|
||||||
state.reconnectionAttempts = 0
|
state.reconnectionAttempts = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,6 +287,7 @@ export function useWebSocket() {
|
|||||||
isConnected.value = false
|
isConnected.value = false
|
||||||
isConnecting.value = false
|
isConnecting.value = false
|
||||||
connectionError.value = null
|
connectionError.value = null
|
||||||
|
permanentlyFailed.value = false
|
||||||
state.reconnectionAttempts = 0
|
state.reconnectionAttempts = 0
|
||||||
|
|
||||||
// Reconnect after a short delay
|
// Reconnect after a short delay
|
||||||
@ -291,6 +296,30 @@ export function useWebSocket() {
|
|||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manual retry after permanent failure.
|
||||||
|
* Resets all connection state and attempts fresh connection.
|
||||||
|
* Use this from UI when user clicks "Retry" after max attempts reached.
|
||||||
|
*/
|
||||||
|
function manualRetry() {
|
||||||
|
// SSR guard
|
||||||
|
if (!import.meta.client) return
|
||||||
|
|
||||||
|
const state = getClientState()
|
||||||
|
console.log('[WebSocket] Manual retry requested - resetting failed state')
|
||||||
|
|
||||||
|
// Reset all state
|
||||||
|
permanentlyFailed.value = false
|
||||||
|
connectionError.value = null
|
||||||
|
state.reconnectionAttempts = 0
|
||||||
|
|
||||||
|
// Restart stuck state detection
|
||||||
|
startStuckStateDetection()
|
||||||
|
|
||||||
|
// Attempt connection
|
||||||
|
forceReconnect()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reconnect with exponential backoff
|
* Reconnect with exponential backoff
|
||||||
*/
|
*/
|
||||||
@ -300,8 +329,18 @@ export function useWebSocket() {
|
|||||||
|
|
||||||
const state = getClientState()
|
const state = getClientState()
|
||||||
|
|
||||||
|
// Check if we've hit the max attempts
|
||||||
|
if (state.reconnectionAttempts >= MAX_RECONNECTION_ATTEMPTS) {
|
||||||
|
console.error(`[WebSocket] Max reconnection attempts (${MAX_RECONNECTION_ATTEMPTS}) reached - giving up`)
|
||||||
|
permanentlyFailed.value = true
|
||||||
|
connectionError.value = `Connection failed after ${MAX_RECONNECTION_ATTEMPTS} attempts. Click retry to try again.`
|
||||||
|
// Stop the stuck state detector since we've given up
|
||||||
|
stopStuckStateDetection()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!shouldReconnect.value) {
|
if (!shouldReconnect.value) {
|
||||||
console.log('[WebSocket] Reconnection not needed or max attempts reached')
|
console.log('[WebSocket] Reconnection not needed')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -690,6 +729,12 @@ export function useWebSocket() {
|
|||||||
if (state.stuckStateCheckInterval) return
|
if (state.stuckStateCheckInterval) return
|
||||||
|
|
||||||
state.stuckStateCheckInterval = setInterval(() => {
|
state.stuckStateCheckInterval = setInterval(() => {
|
||||||
|
// Don't attempt if we've permanently failed - user must manually retry
|
||||||
|
if (permanentlyFailed.value) {
|
||||||
|
console.log('[WebSocket] Stuck state check skipped - permanently failed')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Try to connect if not connected - don't check auth state
|
// Try to connect if not connected - don't check auth state
|
||||||
// Backend will authenticate via cookies
|
// Backend will authenticate via cookies
|
||||||
if (!isConnected.value && !isConnecting.value) {
|
if (!isConnected.value && !isConnecting.value) {
|
||||||
@ -754,11 +799,13 @@ export function useWebSocket() {
|
|||||||
isConnected: readonly(isConnected),
|
isConnected: readonly(isConnected),
|
||||||
isConnecting: readonly(isConnecting),
|
isConnecting: readonly(isConnecting),
|
||||||
connectionError: readonly(connectionError),
|
connectionError: readonly(connectionError),
|
||||||
|
permanentlyFailed: readonly(permanentlyFailed),
|
||||||
canConnect,
|
canConnect,
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
connect,
|
connect,
|
||||||
disconnect,
|
disconnect,
|
||||||
forceReconnect,
|
forceReconnect,
|
||||||
|
manualRetry,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user