From b2d1a2da02ac2666ca18c0aba486e86384d40fbd Mon Sep 17 00:00:00 2001 From: Dorian Date: Sat, 7 Mar 2026 23:52:11 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20memory=20leaks=20=E2=80=94=20cleanup=20t?= =?UTF-8?q?imers=20on=20unmount,=20tighten=20speech=20queue,=20fix=20isOwn?= =?UTF-8?q?er=20reactivity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- frontend/src/components/FightViewer.vue | 7 ++++++- frontend/src/game/sounds.ts | 4 ++-- frontend/src/pages/BotProfilePage.vue | 6 ++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/FightViewer.vue b/frontend/src/components/FightViewer.vue index 24fd4d7..a8377c1 100644 --- a/frontend/src/components/FightViewer.vue +++ b/frontend/src/components/FightViewer.vue @@ -46,6 +46,7 @@ const canvasContainer = ref() const logEl = ref() let scene: FightSceneController | null = null const sceneReady = ref(false) +let cleanupTimerHandle: ReturnType | null = null const isReplaying = ref(false) const displayHpA = ref(100) @@ -98,6 +99,8 @@ onUnmounted(() => { stopAllAudio() sceneReady.value = false if (scene) { scene.destroy(); scene = null } + if (cleanupTimerHandle) { clearTimeout(cleanupTimerHandle); cleanupTimerHandle = null } + if (typeof speechSynthesis !== 'undefined') speechSynthesis.cancel() }) async function initScene() { @@ -398,8 +401,10 @@ async function replay() { } // Kill any remaining queued speech — fight is over - setTimeout(() => { + if (cleanupTimerHandle) clearTimeout(cleanupTimerHandle) + cleanupTimerHandle = setTimeout(() => { if (typeof speechSynthesis !== 'undefined') speechSynthesis.cancel() + cleanupTimerHandle = null }, 3000) isReplaying.value = false diff --git a/frontend/src/game/sounds.ts b/frontend/src/game/sounds.ts index 6ac3ddc..da50175 100644 --- a/frontend/src/game/sounds.ts +++ b/frontend/src/game/sounds.ts @@ -290,9 +290,9 @@ function speak(text: string, profileName: string, cancelPrevious: boolean = fals if (!voicesLoaded) loadVoices() // Chrome bug: speechSynthesis can get stuck. Nudge it. 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 (_speechQueueDepth > 3) { + if (_speechQueueDepth > 2) { speechSynthesis.cancel() _speechQueueDepth = 0 } diff --git a/frontend/src/pages/BotProfilePage.vue b/frontend/src/pages/BotProfilePage.vue index 871fba7..e0dc295 100644 --- a/frontend/src/pages/BotProfilePage.vue +++ b/frontend/src/pages/BotProfilePage.vue @@ -76,7 +76,6 @@ const showChoose = ref(false) const waitingFighters = ref([]) let pollHandle: ReturnType | null = null -const isOwner = ref(false) const loadError = ref('') const fightError = ref('') const showCustomize = ref(false) @@ -102,6 +101,8 @@ const ARCHETYPES = [ 'broom_man', ] +const isOwner = computed(() => isLoggedIn.value && nostrBot.value?.name === botName) + const custForm = reactive({ archetype: '', primaryColor: '#3388cc', @@ -216,9 +217,6 @@ onMounted(async () => { } isLoading.value = false - // Check ownership - isOwner.value = isLoggedIn.value && nostrBot.value?.name === botName - // Poll queue for "choose your fight" pollQueue() pollHandle = setInterval(pollQueue, 4000)