1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-22 03:05:32 +02:00

fix(frontend): guard j/k uncached-child fetch with monotonic token

Rapid j/k over child rows that aren't in entryCacheRef triggers
fetchEntryDetail calls in parallel. Without a guard the last one to
settle wins, desyncing selectedUids (highlight) from selectedEntry
(detail panel/URL).

Fix:
- jkSeqRef (monotonic counter) incremented before each server fetch;
  the .then() guard tok === jkSeqRef.current drops results from
  superseded navigations
- handleRowClick increments jkSeqRef.current so any click also cancels
  an in-flight j/k fetch
This commit is contained in:
TheGeneralist 2026-07-21 15:50:17 +02:00
parent dea8b27fba
commit bd90a4b77f
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4
3 changed files with 23 additions and 17 deletions

View file

@ -132,6 +132,8 @@ export default function App() {
const pendingSearchFocus = useRef(false)
const firstArchiveLoad = useRef(true)
const lastAnchorIndexRef = useRef(null)
// Monotonic token for j/k keyboard navigation; cancels stale fetchEntryDetail calls.
const jkSeqRef = useRef(0)
// uid entry object cache; populated on every row click so ctrl/shift
// selections can resolve child entries that aren't in the root entries array.
const entryCacheRef = useRef(new Map())
@ -267,6 +269,8 @@ export default function App() {
// Cache every clicked entry so shift/ctrl can resolve child entries
// that are not present in the root `entries` array.
entryCacheRef.current.set(entry.entry_uid, entry)
// Invalidate any in-flight j/k uncached-child fetch.
++jkSeqRef.current
if (e.shiftKey && lastAnchorIndexRef.current !== null) {
e.preventDefault()
@ -479,9 +483,11 @@ export default function App() {
const nextUid = nextNode.dataset.entryUid
lastAnchorIndexRef.current = nextUid
const tok = ++jkSeqRef.current
setSelectedUids(new Set([nextUid]))
// Resolve entry object: cache root entries array server fetch
// Resolve entry object: cache root entries array server fetch.
// Token guards against a slow fetch settling after the user has moved on.
const cached = entryCacheRef.current.get(nextUid)
?? entries.find(x => x.entry_uid === nextUid)
?? null
@ -489,7 +495,7 @@ export default function App() {
selectEntry(cached)
} else {
fetchEntryDetail(archiveId, nextUid)
.then(det => { if (det?.summary) selectEntry(det.summary) })
.then(det => { if (tok === jkSeqRef.current && det?.summary) selectEntry(det.summary) })
.catch(() => {})
}