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:
parent
dea8b27fba
commit
bd90a4b77f
3 changed files with 23 additions and 17 deletions
File diff suppressed because one or more lines are too long
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Archivr</title>
|
<title>Archivr</title>
|
||||||
<script type="module" crossorigin src="/assets/index-B0h3YYO-.js"></script>
|
<script type="module" crossorigin src="/assets/index-BOyLHdsN.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-D8ic-z4p.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D8ic-z4p.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,8 @@ export default function App() {
|
||||||
const pendingSearchFocus = useRef(false)
|
const pendingSearchFocus = useRef(false)
|
||||||
const firstArchiveLoad = useRef(true)
|
const firstArchiveLoad = useRef(true)
|
||||||
const lastAnchorIndexRef = useRef(null)
|
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
|
// 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.
|
// selections can resolve child entries that aren't in the root entries array.
|
||||||
const entryCacheRef = useRef(new Map())
|
const entryCacheRef = useRef(new Map())
|
||||||
|
|
@ -267,6 +269,8 @@ export default function App() {
|
||||||
// Cache every clicked entry so shift/ctrl can resolve child entries
|
// Cache every clicked entry so shift/ctrl can resolve child entries
|
||||||
// that are not present in the root `entries` array.
|
// that are not present in the root `entries` array.
|
||||||
entryCacheRef.current.set(entry.entry_uid, entry)
|
entryCacheRef.current.set(entry.entry_uid, entry)
|
||||||
|
// Invalidate any in-flight j/k uncached-child fetch.
|
||||||
|
++jkSeqRef.current
|
||||||
|
|
||||||
if (e.shiftKey && lastAnchorIndexRef.current !== null) {
|
if (e.shiftKey && lastAnchorIndexRef.current !== null) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
@ -479,9 +483,11 @@ export default function App() {
|
||||||
const nextUid = nextNode.dataset.entryUid
|
const nextUid = nextNode.dataset.entryUid
|
||||||
|
|
||||||
lastAnchorIndexRef.current = nextUid
|
lastAnchorIndexRef.current = nextUid
|
||||||
|
const tok = ++jkSeqRef.current
|
||||||
setSelectedUids(new Set([nextUid]))
|
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)
|
const cached = entryCacheRef.current.get(nextUid)
|
||||||
?? entries.find(x => x.entry_uid === nextUid)
|
?? entries.find(x => x.entry_uid === nextUid)
|
||||||
?? null
|
?? null
|
||||||
|
|
@ -489,7 +495,7 @@ export default function App() {
|
||||||
selectEntry(cached)
|
selectEntry(cached)
|
||||||
} else {
|
} else {
|
||||||
fetchEntryDetail(archiveId, nextUid)
|
fetchEntryDetail(archiveId, nextUid)
|
||||||
.then(det => { if (det?.summary) selectEntry(det.summary) })
|
.then(det => { if (tok === jkSeqRef.current && det?.summary) selectEntry(det.summary) })
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue