fix: memory leaks — cleanup timers on unmount, tighten speech queue, fix isOwner reactivity

- FightViewer: track deferred speechSynthesis.cancel() timer, clear on unmount
- BotProfilePage: change isOwner from ref to computed for reactive login state
- sounds.ts: tighten speech queue depth cap from 3 to 2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-07 23:52:11 +00:00
co-authored by Claude Opus 4.6
parent c98007e95c
commit b2d1a2da02
3 changed files with 10 additions and 7 deletions
+6 -1
View File
@@ -46,6 +46,7 @@ const canvasContainer = ref<HTMLElement>()
const logEl = ref<HTMLElement>() const logEl = ref<HTMLElement>()
let scene: FightSceneController | null = null let scene: FightSceneController | null = null
const sceneReady = ref(false) const sceneReady = ref(false)
let cleanupTimerHandle: ReturnType<typeof setTimeout> | null = null
const isReplaying = ref(false) const isReplaying = ref(false)
const displayHpA = ref(100) const displayHpA = ref(100)
@@ -98,6 +99,8 @@ onUnmounted(() => {
stopAllAudio() stopAllAudio()
sceneReady.value = false sceneReady.value = false
if (scene) { scene.destroy(); scene = null } if (scene) { scene.destroy(); scene = null }
if (cleanupTimerHandle) { clearTimeout(cleanupTimerHandle); cleanupTimerHandle = null }
if (typeof speechSynthesis !== 'undefined') speechSynthesis.cancel()
}) })
async function initScene() { async function initScene() {
@@ -398,8 +401,10 @@ async function replay() {
} }
// Kill any remaining queued speech — fight is over // Kill any remaining queued speech — fight is over
setTimeout(() => { if (cleanupTimerHandle) clearTimeout(cleanupTimerHandle)
cleanupTimerHandle = setTimeout(() => {
if (typeof speechSynthesis !== 'undefined') speechSynthesis.cancel() if (typeof speechSynthesis !== 'undefined') speechSynthesis.cancel()
cleanupTimerHandle = null
}, 3000) }, 3000)
isReplaying.value = false isReplaying.value = false
+2 -2
View File
@@ -290,9 +290,9 @@ function speak(text: string, profileName: string, cancelPrevious: boolean = fals
if (!voicesLoaded) loadVoices() if (!voicesLoaded) loadVoices()
// Chrome bug: speechSynthesis can get stuck. Nudge it. // Chrome bug: speechSynthesis can get stuck. Nudge it.
if (speechSynthesis.paused) speechSynthesis.resume() if (speechSynthesis.paused) speechSynthesis.resume()
// Only flush if queue is getting deep — allows voices to overlap naturally // Flush if queue is getting deep — max 2 queued to prevent buildup
if (cancelPrevious || (speechSynthesis.pending && speechSynthesis.speaking)) { if (cancelPrevious || (speechSynthesis.pending && speechSynthesis.speaking)) {
if (_speechQueueDepth > 3) { if (_speechQueueDepth > 2) {
speechSynthesis.cancel() speechSynthesis.cancel()
_speechQueueDepth = 0 _speechQueueDepth = 0
} }
+2 -4
View File
@@ -76,7 +76,6 @@ const showChoose = ref(false)
const waitingFighters = ref<QueueEntry[]>([]) const waitingFighters = ref<QueueEntry[]>([])
let pollHandle: ReturnType<typeof setInterval> | null = null let pollHandle: ReturnType<typeof setInterval> | null = null
const isOwner = ref(false)
const loadError = ref('') const loadError = ref('')
const fightError = ref('') const fightError = ref('')
const showCustomize = ref(false) const showCustomize = ref(false)
@@ -102,6 +101,8 @@ const ARCHETYPES = [
'broom_man', 'broom_man',
] ]
const isOwner = computed(() => isLoggedIn.value && nostrBot.value?.name === botName)
const custForm = reactive({ const custForm = reactive({
archetype: '', archetype: '',
primaryColor: '#3388cc', primaryColor: '#3388cc',
@@ -216,9 +217,6 @@ onMounted(async () => {
} }
isLoading.value = false isLoading.value = false
// Check ownership
isOwner.value = isLoggedIn.value && nostrBot.value?.name === botName
// Poll queue for "choose your fight" // Poll queue for "choose your fight"
pollQueue() pollQueue()
pollHandle = setInterval(pollQueue, 4000) pollHandle = setInterval(pollQueue, 4000)