fix(ui): tag autocomplete no longer shows stale wrong-prefix results (race)
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
This commit is contained in:
@@ -97,6 +97,7 @@
|
|||||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||||
import { useTagStore } from '../../stores/tags.js'
|
import { useTagStore } from '../../stores/tags.js'
|
||||||
import { useSuggestionsStore } from '../../stores/suggestions.js'
|
import { useSuggestionsStore } from '../../stores/suggestions.js'
|
||||||
|
import { useInflightToken } from '../../composables/useInflightToken.js'
|
||||||
import FandomPicker from './FandomPicker.vue'
|
import FandomPicker from './FandomPicker.vue'
|
||||||
|
|
||||||
const emit = defineEmits(['pick-existing', 'pick-new', 'accept-suggestion', 'cancel'])
|
const emit = defineEmits(['pick-existing', 'pick-new', 'accept-suggestion', 'cancel'])
|
||||||
@@ -183,17 +184,26 @@ const parsed = computed(() => {
|
|||||||
const parsedKind = computed(() => parsed.value.kind)
|
const parsedKind = computed(() => parsed.value.kind)
|
||||||
const parsedName = computed(() => parsed.value.name)
|
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
|
let debounceId = null
|
||||||
watch(query, () => {
|
watch(query, () => {
|
||||||
highlight.value = 0
|
highlight.value = 0
|
||||||
if (debounceId) clearTimeout(debounceId)
|
if (debounceId) clearTimeout(debounceId)
|
||||||
|
acInflight.cancel()
|
||||||
debounceId = setTimeout(async () => {
|
debounceId = setTimeout(async () => {
|
||||||
const q = parsedName.value
|
const q = parsedName.value
|
||||||
if (!q) { hits.value = []; return }
|
if (!q) { hits.value = []; return }
|
||||||
// Autocomplete across ALL kinds. When the user typed a prefix the
|
// Autocomplete across ALL kinds. When the user typed a prefix the
|
||||||
// matches list is naturally narrower because the parsed name is
|
// matches list is naturally narrower because the parsed name is
|
||||||
// shorter; we don't filter server-side by kind.
|
// 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)
|
}, 200)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user