fix: page-breaking bugs — duplicate onMounted, SSE race, canvas lifecycle, ghost animation

- ArenaPage: merge duplicate onMounted hooks into single parallel fetch
- FightPage: queue SSE events until liveFightData ready, add reconnect with backoff
- FightViewer: fix stale canvas ref with container ref, add sceneReady guard
- HomePage: replace stopCycling boolean with AbortController for clean unmount

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-07 23:37:43 +00:00
co-authored by Claude Opus 4.6
parent 84625bc4d2
commit e31b15ed28
4 changed files with 104 additions and 65 deletions
+29 -3
View File
@@ -64,6 +64,7 @@ const liveAnnouncementColor = ref('#ffffff')
const liveAnnouncementVisible = ref(false)
const currentChallengeInfo = ref<{ type: string; label: string } | null>(null)
const pendingChallengeData = ref<{ data: any; receivedAt: number } | null>(null)
const pendingSSEEvents = ref<{ type: string; data: any }[]>([])
const humanChoices = ref<string[]>([])
const humanFightDone = ref(false)
const humanFightResult = ref<{ winnerId: string; winnerName: string; isPerfect: boolean } | null>(null)
@@ -341,7 +342,20 @@ function connectSSE() {
} catch { /* */ }
})
eventSource.onerror = () => { /* SSE reconnects automatically */ }
let sseRetries = 0
eventSource.onerror = () => {
// EventSource auto-reconnects, but if it keeps failing, reconnect manually with backoff
sseRetries++
if (sseRetries > 5 && eventSource) {
eventSource.close()
eventSource = null
const delay = Math.min(1000 * 2 ** (sseRetries - 5), 10000)
setTimeout(() => {
if (isLive.value && !eventSource) connectSSE()
}, delay)
}
}
eventSource.onopen = () => { sseRetries = 0 }
}
function disconnectSSE() {
@@ -359,7 +373,10 @@ async function showLiveOverlay(text: string, color: string, duration: number) {
async function handleRoundEnd(data: any) {
const fd = liveFightData.value
if (!fd) return
if (!fd) {
pendingSSEEvents.value.push({ type: 'round_end', data })
return
}
const round = data.round
const result = data.result
@@ -451,7 +468,10 @@ async function handleRoundEnd(data: any) {
async function handleFightEnd(data: any) {
const fd = liveFightData.value
if (!fd) return
if (!fd) {
pendingSSEEvents.value.push({ type: 'fight_end', data })
return
}
// Clear challenge
humanChallenge.value = null
@@ -529,6 +549,12 @@ watch(liveFightData, async (val) => {
await nextTick()
await nextTick()
await initLiveScene()
// Drain any SSE events that arrived before liveFightData was ready
const queued = pendingSSEEvents.value.splice(0)
for (const evt of queued) {
if (evt.type === 'round_end') await handleRoundEnd(evt.data)
else if (evt.type === 'fight_end') await handleFightEnd(evt.data)
}
}
})