From d3984ccb0dd31dbfbeda4c345745e172dd134abe Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 6 Jul 2026 18:45:25 -0400 Subject: [PATCH] fix(ui): tag autocomplete no longer shows stale wrong-prefix results (race) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The keystroke debounce cleared the timer but not an already-fired fetch, so a slower earlier-prefix response ("s") could land after "sex" and overwrite the dropdown with wrong-prefix matches (operator-flagged with a "sex"→Stockings/ Super Mario screenshot). Gate each autocomplete response on a useInflightToken (cancel on every keystroke, isCurrent() after the await) so only the latest query's results are applied. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM --- frontend/src/components/modal/TagAutocomplete.vue | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/modal/TagAutocomplete.vue b/frontend/src/components/modal/TagAutocomplete.vue index 7888bba..bc2303e 100644 --- a/frontend/src/components/modal/TagAutocomplete.vue +++ b/frontend/src/components/modal/TagAutocomplete.vue @@ -97,6 +97,7 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue' import { useTagStore } from '../../stores/tags.js' import { useSuggestionsStore } from '../../stores/suggestions.js' +import { useInflightToken } from '../../composables/useInflightToken.js' import FandomPicker from './FandomPicker.vue' const emit = defineEmits(['pick-existing', 'pick-new', 'accept-suggestion', 'cancel']) @@ -183,17 +184,26 @@ const parsed = computed(() => { const parsedKind = computed(() => parsed.value.kind) const parsedName = computed(() => parsed.value.name) +// Inflight guard: the debounce only clears the TIMER, so once a fetch has fired +// it still races later ones — a slower earlier-prefix response ("s") could land +// after "sex" and overwrite the dropdown with stale, wrong-prefix matches +// (operator-flagged 2026-07-06). Gate each response on a token so only the latest +// query's results are applied. +const acInflight = useInflightToken() let debounceId = null watch(query, () => { highlight.value = 0 if (debounceId) clearTimeout(debounceId) + acInflight.cancel() debounceId = setTimeout(async () => { const q = parsedName.value if (!q) { hits.value = []; return } // Autocomplete across ALL kinds. When the user typed a prefix the // matches list is naturally narrower because the parsed name is // shorter; we don't filter server-side by kind. - hits.value = await store.autocomplete(q, null, 10) + const t = acInflight.claim() + const res = await store.autocomplete(q, null, 10) + if (t.isCurrent()) hits.value = res }, 200) })