fix: track and clear all intervals and event listeners
- FightPage.vue: store SSE listener refs in array, removeEventListener on each before close() in disconnectSSE() - FightScene.ts: add cleanupTimers Set with trackedInterval/trackedTimeout helpers; replace key setInterval calls (projectile trails, entrance flames, talking animation, exhaust effects, dimensional shift, hole fade) with tracked versions; clear all on scene destroy() - rate-limit.ts: export cleanupInterval handle for graceful shutdown Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
010e18fbd3
commit
7bd110684d
@@ -347,6 +347,23 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
|
|
||||||
const W = k.width()
|
const W = k.width()
|
||||||
const H = k.height()
|
const H = k.height()
|
||||||
|
// Track all intervals/timeouts for cleanup on scene destroy
|
||||||
|
const cleanupTimers = new Set<ReturnType<typeof setTimeout>>()
|
||||||
|
function trackedTimeout(fn: () => void, ms: number): ReturnType<typeof setTimeout> {
|
||||||
|
const id = setTimeout(() => { cleanupTimers.delete(id); fn() }, ms)
|
||||||
|
cleanupTimers.add(id)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
function trackedInterval(fn: () => void, ms: number): ReturnType<typeof setInterval> {
|
||||||
|
const id = setInterval(fn, ms)
|
||||||
|
cleanupTimers.add(id)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
function clearTracked(id: ReturnType<typeof setTimeout>) {
|
||||||
|
clearInterval(id)
|
||||||
|
clearTimeout(id)
|
||||||
|
cleanupTimers.delete(id)
|
||||||
|
}
|
||||||
// Scale factor so sprites shrink on small canvases (reference: 500px tall)
|
// Scale factor so sprites shrink on small canvases (reference: 500px tall)
|
||||||
const SF = Math.min(1, H / 500)
|
const SF = Math.min(1, H / 500)
|
||||||
const GROUND_Y = H * 0.78
|
const GROUND_Y = H * 0.78
|
||||||
@@ -392,10 +409,10 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
k.z(9), // behind fighters
|
k.z(9), // behind fighters
|
||||||
])
|
])
|
||||||
// Fade out after a bit
|
// Fade out after a bit
|
||||||
setTimeout(() => {
|
trackedTimeout(() => {
|
||||||
const fadeInterval = setInterval(() => {
|
const fadeInterval = trackedInterval(() => {
|
||||||
hole.opacity -= 0.02
|
hole.opacity -= 0.02
|
||||||
if (hole.opacity <= 0) { hole.destroy(); clearInterval(fadeInterval) }
|
if (hole.opacity <= 0) { hole.destroy(); clearTracked(fadeInterval) }
|
||||||
}, 50)
|
}, 50)
|
||||||
}, 1500)
|
}, 1500)
|
||||||
}
|
}
|
||||||
@@ -403,8 +420,8 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
|
|
||||||
function spawnExhaust(x: number, y: number, duration: number) {
|
function spawnExhaust(x: number, y: number, duration: number) {
|
||||||
const endTime = performance.now() + duration * 1000
|
const endTime = performance.now() + duration * 1000
|
||||||
const interval = setInterval(() => {
|
const interval = trackedInterval(() => {
|
||||||
if (performance.now() > endTime) { clearInterval(interval); return }
|
if (performance.now() > endTime) { clearTracked(interval); return }
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
const p = k.add([
|
const p = k.add([
|
||||||
k.circle(3 + Math.random() * 5),
|
k.circle(3 + Math.random() * 5),
|
||||||
@@ -434,7 +451,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
k.z(15),
|
k.z(15),
|
||||||
])
|
])
|
||||||
// Trail
|
// Trail
|
||||||
const trail = setInterval(() => {
|
const trail = trackedInterval(() => {
|
||||||
const t = k.add([
|
const t = k.add([
|
||||||
k.circle(size * 0.6),
|
k.circle(size * 0.6),
|
||||||
k.pos(proj.pos.x, proj.pos.y),
|
k.pos(proj.pos.x, proj.pos.y),
|
||||||
@@ -455,7 +472,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
proj.pos.x = fromX + dx * t
|
proj.pos.x = fromX + dx * t
|
||||||
proj.pos.y = fromY + dy * t
|
proj.pos.y = fromY + dy * t
|
||||||
}, k.easings.linear).then(() => {
|
}, k.easings.linear).then(() => {
|
||||||
clearInterval(trail)
|
clearTracked(trail)
|
||||||
proj.destroy()
|
proj.destroy()
|
||||||
spawnSparks(toX, toY, 8, color)
|
spawnSparks(toX, toY, 8, color)
|
||||||
resolve()
|
resolve()
|
||||||
@@ -569,12 +586,12 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
k.rect(W, H), k.pos(0, 0),
|
k.rect(W, H), k.pos(0, 0),
|
||||||
k.color(safeColor(k,colors[0])), k.opacity(0.08), k.z(1),
|
k.color(safeColor(k,colors[0])), k.opacity(0.08), k.z(1),
|
||||||
])
|
])
|
||||||
const interval = setInterval(() => {
|
const interval = trackedInterval(() => {
|
||||||
idx = (idx + 1) % colors.length
|
idx = (idx + 1) % colors.length
|
||||||
overlay.color = safeColor(k,colors[idx])
|
overlay.color = safeColor(k,colors[idx])
|
||||||
overlay.opacity = 0.05 + Math.random() * 0.06
|
overlay.opacity = 0.05 + Math.random() * 0.06
|
||||||
}, 80)
|
}, 80)
|
||||||
setTimeout(() => { clearInterval(interval); if (overlay.exists()) overlay.destroy() }, duration * 1000)
|
trackedTimeout(() => { clearTracked(interval); if (overlay.exists()) overlay.destroy() }, duration * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schizo cut — rapid zoom/position jitter simulating jump cuts
|
// Schizo cut — rapid zoom/position jitter simulating jump cuts
|
||||||
@@ -2549,8 +2566,8 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
spawnSparks(def.pos.x, def.pos.y - 30, 15, '#ff6600')
|
spawnSparks(def.pos.x, def.pos.y - 30, 15, '#ff6600')
|
||||||
if (isCritical) { screenFlash('#ff6600'); sfxExplosion() }
|
if (isCritical) { screenFlash('#ff6600'); sfxExplosion() }
|
||||||
// Defender on fire briefly
|
// Defender on fire briefly
|
||||||
const fireTimer = setInterval(() => {
|
const fireTimer = trackedInterval(() => {
|
||||||
if (!def.exists()) { clearInterval(fireTimer); return }
|
if (!def.exists()) { clearTracked(fireTimer); return }
|
||||||
k.add([
|
k.add([
|
||||||
k.circle(3),
|
k.circle(3),
|
||||||
k.pos(def.pos.x + (Math.random() - 0.5) * 25, def.pos.y - Math.random() * 60),
|
k.pos(def.pos.x + (Math.random() - 0.5) * 25, def.pos.y - Math.random() * 60),
|
||||||
@@ -2562,7 +2579,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
const push = dir * (isCritical ? 110 : 50)
|
const push = dir * (isCritical ? 110 : 50)
|
||||||
k.tween(def.pos.x, origDX + push, 0.3, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
|
k.tween(def.pos.x, origDX + push, 0.3, (v) => { def.pos.x = v }, k.easings.easeOutQuad)
|
||||||
await k.wait(0.6)
|
await k.wait(0.6)
|
||||||
clearInterval(fireTimer)
|
clearTracked(fireTimer)
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad),
|
k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad),
|
||||||
k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad),
|
k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad),
|
||||||
@@ -3322,14 +3339,14 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
const nose = k.add([k.rect(6, 6), k.pos(rocketX + dir * 12, rocketY + 1), k.color(safeColor(k,'#ff2d2d')), k.z(17)])
|
const nose = k.add([k.rect(6, 6), k.pos(rocketX + dir * 12, rocketY + 1), k.color(safeColor(k,'#ff2d2d')), k.z(17)])
|
||||||
sfxJetpack()
|
sfxJetpack()
|
||||||
// Trail exhaust as rocket flies
|
// Trail exhaust as rocket flies
|
||||||
const exhaustInt = setInterval(() => {
|
const exhaustInt = trackedInterval(() => {
|
||||||
const p = k.add([k.circle(3 + Math.random() * 4), k.pos(rocket.pos.x - dir * 12, rocket.pos.y + (Math.random() - 0.5) * 8), k.color(safeColor(k,Math.random() > 0.5 ? '#ff6600' : '#ffcc00')), k.opacity(0.7), k.z(16)])
|
const p = k.add([k.circle(3 + Math.random() * 4), k.pos(rocket.pos.x - dir * 12, rocket.pos.y + (Math.random() - 0.5) * 8), k.color(safeColor(k,Math.random() > 0.5 ? '#ff6600' : '#ffcc00')), k.opacity(0.7), k.z(16)])
|
||||||
p.onUpdate(() => { p.pos.x -= dir * 100 * k.dt(); p.opacity -= 3 * k.dt(); if (p.opacity <= 0) p.destroy() })
|
p.onUpdate(() => { p.pos.x -= dir * 100 * k.dt(); p.opacity -= 3 * k.dt(); if (p.opacity <= 0) p.destroy() })
|
||||||
}, 30)
|
}, 30)
|
||||||
// Fly rocket to target
|
// Fly rocket to target
|
||||||
const dur = 0.3
|
const dur = 0.3
|
||||||
await k.tween(0, 1, dur, (t) => { rocket.pos.x = rocketX + (def.pos.x - rocketX) * t; nose.pos.x = rocket.pos.x + dir * 12; rocket.pos.y = rocketY + Math.sin(t * Math.PI * 3) * 8; nose.pos.y = rocket.pos.y + 1 }, k.easings.linear)
|
await k.tween(0, 1, dur, (t) => { rocket.pos.x = rocketX + (def.pos.x - rocketX) * t; nose.pos.x = rocket.pos.x + dir * 12; rocket.pos.y = rocketY + Math.sin(t * Math.PI * 3) * 8; nose.pos.y = rocket.pos.y + 1 }, k.easings.linear)
|
||||||
clearInterval(exhaustInt)
|
clearTracked(exhaustInt)
|
||||||
rocket.destroy(); nose.destroy()
|
rocket.destroy(); nose.destroy()
|
||||||
// EXPLOSION!
|
// EXPLOSION!
|
||||||
sfxExplosion()
|
sfxExplosion()
|
||||||
@@ -3610,7 +3627,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
const wheelR = k.add([k.circle(8), k.pos(startX - dir * 16, bikeY + 12), k.color(safeColor(k,'#333333')), k.z(16)])
|
const wheelR = k.add([k.circle(8), k.pos(startX - dir * 16, bikeY + 12), k.color(safeColor(k,'#333333')), k.z(16)])
|
||||||
sfxJetpack()
|
sfxJetpack()
|
||||||
// Tire marks + exhaust as it zooms across
|
// Tire marks + exhaust as it zooms across
|
||||||
const exh = setInterval(() => {
|
const exh = trackedInterval(() => {
|
||||||
const p = k.add([k.circle(3), k.pos(bike.pos.x - dir * 25, bikeY + 5), k.color(safeColor(k,'#888888')), k.opacity(0.5), k.z(4)])
|
const p = k.add([k.circle(3), k.pos(bike.pos.x - dir * 25, bikeY + 5), k.color(safeColor(k,'#888888')), k.opacity(0.5), k.z(4)])
|
||||||
p.onUpdate(() => { p.opacity -= 2 * k.dt(); if (p.opacity <= 0) p.destroy() })
|
p.onUpdate(() => { p.opacity -= 2 * k.dt(); if (p.opacity <= 0) p.destroy() })
|
||||||
}, 30)
|
}, 30)
|
||||||
@@ -3619,7 +3636,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
await k.tween(startX, targetX, 0.3, (v) => {
|
await k.tween(startX, targetX, 0.3, (v) => {
|
||||||
bike.pos.x = v; wheelF.pos.x = v + dir * 18; wheelR.pos.x = v - dir * 16
|
bike.pos.x = v; wheelF.pos.x = v + dir * 18; wheelR.pos.x = v - dir * 16
|
||||||
}, k.easings.easeInQuad)
|
}, k.easings.easeInQuad)
|
||||||
clearInterval(exh)
|
clearTracked(exh)
|
||||||
// IMPACT
|
// IMPACT
|
||||||
sfxExplosion()
|
sfxExplosion()
|
||||||
sfxCritical()
|
sfxCritical()
|
||||||
@@ -4574,7 +4591,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
extras.push(k.add([k.rect(p.w, p.h), k.pos(startX + p.ox * dir, vY + p.oy), k.color(safeColor(k,p.color)), k.z(17)]))
|
extras.push(k.add([k.rect(p.w, p.h), k.pos(startX + p.ox * dir, vY + p.oy), k.color(safeColor(k,p.color)), k.z(17)]))
|
||||||
}
|
}
|
||||||
sfxJetpack()
|
sfxJetpack()
|
||||||
const exh = setInterval(() => {
|
const exh = trackedInterval(() => {
|
||||||
const ep = k.add([k.circle(3), k.pos(body.pos.x - dir * bodyW / 2, vY + 5), k.color(safeColor(k,'#888888')), k.opacity(0.5), k.z(4)])
|
const ep = k.add([k.circle(3), k.pos(body.pos.x - dir * bodyW / 2, vY + 5), k.color(safeColor(k,'#888888')), k.opacity(0.5), k.z(4)])
|
||||||
ep.onUpdate(() => { ep.opacity -= 2 * k.dt(); if (ep.opacity <= 0) ep.destroy() })
|
ep.onUpdate(() => { ep.opacity -= 2 * k.dt(); if (ep.opacity <= 0) ep.destroy() })
|
||||||
}, 30)
|
}, 30)
|
||||||
@@ -4583,7 +4600,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
body.pos.x = v; w1.pos.x = v + dir * (bodyW / 3); w2.pos.x = v - dir * (bodyW / 3)
|
body.pos.x = v; w1.pos.x = v + dir * (bodyW / 3); w2.pos.x = v - dir * (bodyW / 3)
|
||||||
extras.forEach((e, i) => { if (parts) e.pos.x = v + parts[i].ox * dir })
|
extras.forEach((e, i) => { if (parts) e.pos.x = v + parts[i].ox * dir })
|
||||||
}, k.easings.easeInQuad)
|
}, k.easings.easeInQuad)
|
||||||
clearInterval(exh)
|
clearTracked(exh)
|
||||||
sfxExplosion(); sfxCritical()
|
sfxExplosion(); sfxCritical()
|
||||||
body.destroy(); w1.destroy(); w2.destroy(); extras.forEach(e => e.destroy())
|
body.destroy(); w1.destroy(); w2.destroy(); extras.forEach(e => e.destroy())
|
||||||
k.shake(isCritical ? 25 : 14); screenFlash(isCritical ? '#ff2d2d' : bodyColor, 0.18)
|
k.shake(isCritical ? 25 : 14); screenFlash(isCritical ? '#ff2d2d' : bodyColor, 0.18)
|
||||||
@@ -7354,7 +7371,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
])
|
])
|
||||||
|
|
||||||
// Toggle open/close every 120ms for a fast chatter effect
|
// Toggle open/close every 120ms for a fast chatter effect
|
||||||
const timer = setInterval(() => {
|
const timer = trackedInterval(() => {
|
||||||
if (!mouth.exists() || !fighter.exists()) { stopTalking(side); return }
|
if (!mouth.exists() || !fighter.exists()) { stopTalking(side); return }
|
||||||
open = !open
|
open = !open
|
||||||
mouth.pos.x = fighter.pos.x
|
mouth.pos.x = fighter.pos.x
|
||||||
@@ -7368,7 +7385,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
function stopTalking(side: 'a' | 'b') {
|
function stopTalking(side: 'a' | 'b') {
|
||||||
const anim = talkingAnims[side]
|
const anim = talkingAnims[side]
|
||||||
if (!anim) return
|
if (!anim) return
|
||||||
clearInterval(anim.timer)
|
clearTracked(anim.timer)
|
||||||
anim.objs.forEach(o => { if (o.exists()) o.destroy() })
|
anim.objs.forEach(o => { if (o.exists()) o.destroy() })
|
||||||
delete talkingAnims[side]
|
delete talkingAnims[side]
|
||||||
}
|
}
|
||||||
@@ -8803,13 +8820,13 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
sfxJetpack()
|
sfxJetpack()
|
||||||
// Fly in with flame trail
|
// Fly in with flame trail
|
||||||
const flames: any[] = []
|
const flames: any[] = []
|
||||||
const interval = setInterval(() => {
|
const interval = trackedInterval(() => {
|
||||||
const f = k.add([k.circle(5 + Math.random() * 5), k.pos(fighter.pos.x, fighter.pos.y + 25), k.color(safeColor(k,Math.random() > 0.5 ? '#ff6600' : '#ffcc00')), k.opacity(0.8), k.z(9)])
|
const f = k.add([k.circle(5 + Math.random() * 5), k.pos(fighter.pos.x, fighter.pos.y + 25), k.color(safeColor(k,Math.random() > 0.5 ? '#ff6600' : '#ffcc00')), k.opacity(0.8), k.z(9)])
|
||||||
flames.push(f)
|
flames.push(f)
|
||||||
k.tween(f.opacity, 0, 0.3, (v) => { f.opacity = v }).then(() => { if (f.exists()) f.destroy() })
|
k.tween(f.opacity, 0, 0.3, (v) => { f.opacity = v }).then(() => { if (f.exists()) f.destroy() })
|
||||||
}, 30)
|
}, 30)
|
||||||
await k.tween(startX, homeX, 0.4, (v) => { fighter.pos.x = v }, k.easings.easeOutQuad)
|
await k.tween(startX, homeX, 0.4, (v) => { fighter.pos.x = v }, k.easings.easeOutQuad)
|
||||||
clearInterval(interval)
|
clearTracked(interval)
|
||||||
// Land
|
// Land
|
||||||
sfxExplosion()
|
sfxExplosion()
|
||||||
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
|
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
|
||||||
@@ -11308,6 +11325,13 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
stopMusic()
|
stopMusic()
|
||||||
|
stopTalking('a')
|
||||||
|
stopTalking('b')
|
||||||
|
for (const id of cleanupTimers) {
|
||||||
|
clearInterval(id)
|
||||||
|
clearTimeout(id)
|
||||||
|
}
|
||||||
|
cleanupTimers.clear()
|
||||||
k.quit()
|
k.quit()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -389,17 +389,24 @@ async function initLiveScene() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- SSE connection ---
|
// --- SSE connection ---
|
||||||
|
const sseListeners: { event: string; handler: EventListener }[] = []
|
||||||
|
|
||||||
|
function addSSEListener(es: EventSource, event: string, handler: (e: MessageEvent) => void) {
|
||||||
|
es.addEventListener(event, handler as EventListener)
|
||||||
|
sseListeners.push({ event, handler: handler as EventListener })
|
||||||
|
}
|
||||||
|
|
||||||
function connectSSE() {
|
function connectSSE() {
|
||||||
eventSource = new EventSource(`/api/fights/${fightId.value}/stream`)
|
eventSource = new EventSource(`/api/fights/${fightId.value}/stream`)
|
||||||
|
|
||||||
eventSource.addEventListener('spectator_count', (e) => {
|
addSSEListener(eventSource, 'spectator_count', (e) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(e.data)
|
const data = JSON.parse(e.data)
|
||||||
spectatorCount.value = data.count || 0
|
spectatorCount.value = data.count || 0
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
})
|
})
|
||||||
|
|
||||||
eventSource.addEventListener('ping', (e) => {
|
addSSEListener(eventSource, 'ping', (e) => {
|
||||||
try {
|
try {
|
||||||
if (e.data) {
|
if (e.data) {
|
||||||
const data = JSON.parse(e.data)
|
const data = JSON.parse(e.data)
|
||||||
@@ -408,21 +415,19 @@ function connectSSE() {
|
|||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
})
|
})
|
||||||
|
|
||||||
eventSource.addEventListener('reaction', (e) => {
|
addSSEListener(eventSource, 'reaction', (e) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(e.data)
|
const data = JSON.parse(e.data)
|
||||||
if (data.emoji && data.counts) {
|
if (data.emoji && data.counts) {
|
||||||
// Spawn floating emoji in the live view
|
|
||||||
spawnLiveReactionEmoji(data.emoji)
|
spawnLiveReactionEmoji(data.emoji)
|
||||||
}
|
}
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
})
|
})
|
||||||
|
|
||||||
eventSource.addEventListener('round_start', (e) => {
|
addSSEListener(eventSource, 'round_start', (e) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(e.data)
|
const data = JSON.parse(e.data)
|
||||||
currentChallengeInfo.value = { type: data.challenge.type, label: data.challenge.label }
|
currentChallengeInfo.value = { type: data.challenge.type, label: data.challenge.label }
|
||||||
// Show challenge question in battle log
|
|
||||||
liveLogItems.value.push({
|
liveLogItems.value.push({
|
||||||
type: 'challenge',
|
type: 'challenge',
|
||||||
round: data.round,
|
round: data.round,
|
||||||
@@ -435,10 +440,9 @@ function connectSSE() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
eventSource.addEventListener('human_challenge', async (e) => {
|
addSSEListener(eventSource, 'human_challenge', async (e) => {
|
||||||
try {
|
try {
|
||||||
const sseData = JSON.parse(e.data)
|
const sseData = JSON.parse(e.data)
|
||||||
// Fetch from API to get choices (SSE event doesn't include them)
|
|
||||||
if (myBot.value) {
|
if (myBot.value) {
|
||||||
const res = await fetch(`/api/fights/${fightId.value}/challenge/${myBot.value.id}`)
|
const res = await fetch(`/api/fights/${fightId.value}/challenge/${myBot.value.id}`)
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -453,7 +457,6 @@ function connectSSE() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fallback: use SSE data without choices
|
|
||||||
if (roundCooldown.value > 0) {
|
if (roundCooldown.value > 0) {
|
||||||
pendingChallengeData.value = { data: sseData, receivedAt: Date.now() }
|
pendingChallengeData.value = { data: sseData, receivedAt: Date.now() }
|
||||||
} else {
|
} else {
|
||||||
@@ -464,7 +467,7 @@ function connectSSE() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
eventSource.addEventListener('round_end', (e) => {
|
addSSEListener(eventSource, 'round_end', (e) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(e.data)
|
const data = JSON.parse(e.data)
|
||||||
if (data.spectators !== undefined) spectatorCount.value = data.spectators
|
if (data.spectators !== undefined) spectatorCount.value = data.spectators
|
||||||
@@ -474,7 +477,7 @@ function connectSSE() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
eventSource.addEventListener('fight_end', (e) => {
|
addSSEListener(eventSource, 'fight_end', (e) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(e.data)
|
const data = JSON.parse(e.data)
|
||||||
if (data.spectators !== undefined) spectatorCount.value = data.spectators
|
if (data.spectators !== undefined) spectatorCount.value = data.spectators
|
||||||
@@ -486,7 +489,6 @@ function connectSSE() {
|
|||||||
|
|
||||||
let sseRetries = 0
|
let sseRetries = 0
|
||||||
eventSource.onerror = () => {
|
eventSource.onerror = () => {
|
||||||
// EventSource auto-reconnects, but if it keeps failing, reconnect manually with backoff
|
|
||||||
sseRetries++
|
sseRetries++
|
||||||
if (sseRetries > 5 && eventSource) {
|
if (sseRetries > 5 && eventSource) {
|
||||||
eventSource.close()
|
eventSource.close()
|
||||||
@@ -501,7 +503,14 @@ function connectSSE() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function disconnectSSE() {
|
function disconnectSSE() {
|
||||||
if (eventSource) { eventSource.close(); eventSource = null }
|
if (eventSource) {
|
||||||
|
for (const { event, handler } of sseListeners) {
|
||||||
|
eventSource.removeEventListener(event, handler)
|
||||||
|
}
|
||||||
|
sseListeners.length = 0
|
||||||
|
eventSource.close()
|
||||||
|
eventSource = null
|
||||||
|
}
|
||||||
spectatorCount.value = 0
|
spectatorCount.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const isDev = process.env.NODE_ENV !== 'production'
|
|||||||
const hitCounts = new Map<string, { count: number; resetAt: number }>()
|
const hitCounts = new Map<string, { count: number; resetAt: number }>()
|
||||||
|
|
||||||
// Cleanup stale entries every 5 minutes
|
// Cleanup stale entries every 5 minutes
|
||||||
setInterval(() => {
|
export const cleanupInterval = setInterval(() => {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
for (const [key, entry] of hitCounts) {
|
for (const [key, entry] of hitCounts) {
|
||||||
if (now > entry.resetAt) hitCounts.delete(key)
|
if (now > entry.resetAt) hitCounts.delete(key)
|
||||||
|
|||||||
Reference in New Issue
Block a user