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
+41 -34
View File
@@ -242,54 +242,61 @@ function shuffle<T>(arr: T[]): T[] {
return a
}
function sleep(ms: number) {
return new Promise(r => setTimeout(r, ms))
let cycleAbort: AbortController | null = null
function sleep(ms: number, signal?: AbortSignal) {
return new Promise<void>((resolve, reject) => {
const id = setTimeout(resolve, ms)
signal?.addEventListener('abort', () => { clearTimeout(id); reject(signal.reason) }, { once: true })
})
}
let stopCycling = false
async function cycleTaglines() {
async function cycleTaglines(signal: AbortSignal) {
const shuffled = shuffle(taglines)
let idx = 0
while (!stopCycling) {
const line = shuffled[idx % shuffled.length]
isTypingDone.value = false
try {
while (!signal.aborted) {
const line = shuffled[idx % shuffled.length]
isTypingDone.value = false
// Type in
for (let i = 0; i <= line.length; i++) {
if (stopCycling) return
tagline.value = line.slice(0, i)
await sleep(35)
}
isTypingDone.value = true
// Type in
for (let i = 0; i <= line.length; i++) {
if (signal.aborted) return
tagline.value = line.slice(0, i)
await sleep(35, signal)
}
isTypingDone.value = true
// Hold
await sleep(4000)
if (stopCycling) return
// Hold
await sleep(4000, signal)
// Erase
isTypingDone.value = false
for (let i = line.length; i >= 0; i--) {
if (stopCycling) return
tagline.value = line.slice(0, i)
await sleep(20)
}
// Erase
isTypingDone.value = false
for (let i = line.length; i >= 0; i--) {
if (signal.aborted) return
tagline.value = line.slice(0, i)
await sleep(20, signal)
}
await sleep(300)
idx++
await sleep(300, signal)
idx++
// Reshuffle when we've gone through all
if (idx >= shuffled.length) {
idx = 0
const reshuffled = shuffle(taglines)
shuffled.splice(0, shuffled.length, ...reshuffled)
// Reshuffle when we've gone through all
if (idx >= shuffled.length) {
idx = 0
const reshuffled = shuffle(taglines)
shuffled.splice(0, shuffled.length, ...reshuffled)
}
}
} catch {
// AbortError — expected on unmount
}
}
onMounted(async () => {
cycleTaglines()
cycleAbort = new AbortController()
cycleTaglines(cycleAbort.signal)
try {
const res = await fetch('/api/fights')
@@ -301,7 +308,7 @@ onMounted(async () => {
})
onUnmounted(() => {
stopCycling = true
if (cycleAbort) { cycleAbort.abort(); cycleAbort = null }
})
</script>