fix: prevent stale API responses from overwriting newer data
- SessionHistoryPage: move loadSessions into effect with cancelled flag so rapid filter/tab changes discard outdated responses - TreeLibraryPage: add request ID ref to loadTrees so stale responses from previous filter selections are discarded - QuickStartPage: add request ID ref to debounced search so out-of-order responses don't overwrite newer search results Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -176,7 +176,8 @@ export function QuickStartPage() {
|
|||||||
return () => window.removeEventListener('focus', onFocus)
|
return () => window.removeEventListener('focus', onFocus)
|
||||||
}, [loadFlows])
|
}, [loadFlows])
|
||||||
|
|
||||||
// Debounced search
|
// Debounced search with staleness guard
|
||||||
|
const searchRequestId = useRef(0)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (debounceRef.current) clearTimeout(debounceRef.current)
|
if (debounceRef.current) clearTimeout(debounceRef.current)
|
||||||
if (query.length < 2) {
|
if (query.length < 2) {
|
||||||
@@ -188,13 +189,16 @@ export function QuickStartPage() {
|
|||||||
setIsSearching(true)
|
setIsSearching(true)
|
||||||
setShowResults(true)
|
setShowResults(true)
|
||||||
debounceRef.current = setTimeout(async () => {
|
debounceRef.current = setTimeout(async () => {
|
||||||
|
const requestId = ++searchRequestId.current
|
||||||
try {
|
try {
|
||||||
const results = await treesApi.search(query, 8)
|
const results = await treesApi.search(query, 8)
|
||||||
|
if (requestId !== searchRequestId.current) return
|
||||||
setSearchResults(results)
|
setSearchResults(results)
|
||||||
} catch {
|
} catch {
|
||||||
|
if (requestId !== searchRequestId.current) return
|
||||||
setSearchResults([])
|
setSearchResults([])
|
||||||
} finally {
|
} finally {
|
||||||
setIsSearching(false)
|
if (requestId === searchRequestId.current) setIsSearching(false)
|
||||||
}
|
}
|
||||||
}, 300)
|
}, 300)
|
||||||
return () => { if (debounceRef.current) clearTimeout(debounceRef.current) }
|
return () => { if (debounceRef.current) clearTimeout(debounceRef.current) }
|
||||||
|
|||||||
@@ -60,24 +60,7 @@ export function SessionHistoryPage() {
|
|||||||
|
|
||||||
// Load sessions when filters change
|
// Load sessions when filters change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSessions()
|
let cancelled = false
|
||||||
}, [filter, filters])
|
|
||||||
|
|
||||||
// Update URL params when filters change
|
|
||||||
useEffect(() => {
|
|
||||||
const params = new URLSearchParams()
|
|
||||||
|
|
||||||
if (filters.ticketNumber) params.set('ticket', filters.ticketNumber)
|
|
||||||
if (filters.clientName) params.set('client', filters.clientName)
|
|
||||||
if (filters.treeName) params.set('tree', filters.treeName)
|
|
||||||
if (filters.dateRange?.from) {
|
|
||||||
params.set('from', filters.dateRange.from.toISOString())
|
|
||||||
params.set('to', (filters.dateRange.to || filters.dateRange.from).toISOString())
|
|
||||||
params.set('dateType', filters.dateType)
|
|
||||||
}
|
|
||||||
|
|
||||||
setSearchParams(params, { replace: true })
|
|
||||||
}, [filters, setSearchParams])
|
|
||||||
|
|
||||||
const loadSessions = async () => {
|
const loadSessions = async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
@@ -115,17 +98,39 @@ export function SessionHistoryPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sessionsData = await sessionsApi.list({ ...params, size: 51 })
|
const sessionsData = await sessionsApi.list({ ...params, size: 51 })
|
||||||
|
if (cancelled) return
|
||||||
const truncated = sessionsData.length > 50
|
const truncated = sessionsData.length > 50
|
||||||
setHasMore(truncated)
|
setHasMore(truncated)
|
||||||
setSessions(truncated ? sessionsData.slice(0, 50) : sessionsData)
|
setSessions(truncated ? sessionsData.slice(0, 50) : sessionsData)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (cancelled) return
|
||||||
toast.error('Failed to load sessions')
|
toast.error('Failed to load sessions')
|
||||||
console.error(err)
|
console.error(err)
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
if (!cancelled) setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadSessions()
|
||||||
|
return () => { cancelled = true }
|
||||||
|
}, [filter, filters])
|
||||||
|
|
||||||
|
// Update URL params when filters change
|
||||||
|
useEffect(() => {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
|
||||||
|
if (filters.ticketNumber) params.set('ticket', filters.ticketNumber)
|
||||||
|
if (filters.clientName) params.set('client', filters.clientName)
|
||||||
|
if (filters.treeName) params.set('tree', filters.treeName)
|
||||||
|
if (filters.dateRange?.from) {
|
||||||
|
params.set('from', filters.dateRange.from.toISOString())
|
||||||
|
params.set('to', (filters.dateRange.to || filters.dateRange.from).toISOString())
|
||||||
|
params.set('dateType', filters.dateType)
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchParams(params, { replace: true })
|
||||||
|
}, [filters, setSearchParams])
|
||||||
|
|
||||||
const handleFilterChange = (newFilters: SessionFilterState) => {
|
const handleFilterChange = (newFilters: SessionFilterState) => {
|
||||||
setFilters(newFilters)
|
setFilters(newFilters)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState, useCallback, useMemo } from 'react'
|
import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { X, RotateCcw, Play, FileUp } from 'lucide-react'
|
import { X, RotateCcw, Play, FileUp } from 'lucide-react'
|
||||||
import { PageMeta } from '@/components/common/PageMeta'
|
import { PageMeta } from '@/components/common/PageMeta'
|
||||||
@@ -158,20 +158,11 @@ export function TreeLibraryPage() {
|
|||||||
.catch((err) => console.error('Failed to load categories:', err))
|
.catch((err) => console.error('Failed to load categories:', err))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Load trees when filters change
|
// Request ID ref to discard stale responses when filters change rapidly
|
||||||
useEffect(() => {
|
const loadTreesRequestId = useRef(0)
|
||||||
loadTrees()
|
|
||||||
}, [selectedCategoryId, selectedTags, selectedFolderId, treeLibrarySortBy, typeFilter])
|
|
||||||
|
|
||||||
// Load folders on mount and listen for changes
|
const loadTrees = useCallback(async () => {
|
||||||
useEffect(() => {
|
const requestId = ++loadTreesRequestId.current
|
||||||
loadFolders()
|
|
||||||
const handleFolderChange = () => loadFolders()
|
|
||||||
window.addEventListener('folder-changed', handleFolderChange)
|
|
||||||
return () => window.removeEventListener('folder-changed', handleFolderChange)
|
|
||||||
}, [loadFolders])
|
|
||||||
|
|
||||||
const loadTrees = async () => {
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
try {
|
||||||
const treesData = await treesApi.list({
|
const treesData = await treesApi.list({
|
||||||
@@ -181,14 +172,29 @@ export function TreeLibraryPage() {
|
|||||||
folder_id: selectedFolderId || undefined,
|
folder_id: selectedFolderId || undefined,
|
||||||
sort_by: treeLibrarySortBy,
|
sort_by: treeLibrarySortBy,
|
||||||
})
|
})
|
||||||
|
if (requestId !== loadTreesRequestId.current) return
|
||||||
setTrees(treesData)
|
setTrees(treesData)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (requestId !== loadTreesRequestId.current) return
|
||||||
toast.error('Failed to load flows')
|
toast.error('Failed to load flows')
|
||||||
console.error(err)
|
console.error(err)
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
if (requestId === loadTreesRequestId.current) setIsLoading(false)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}, [selectedCategoryId, selectedTags, selectedFolderId, treeLibrarySortBy, typeFilter])
|
||||||
|
|
||||||
|
// Load trees when filters change
|
||||||
|
useEffect(() => {
|
||||||
|
loadTrees()
|
||||||
|
}, [loadTrees])
|
||||||
|
|
||||||
|
// Load folders on mount and listen for changes
|
||||||
|
useEffect(() => {
|
||||||
|
loadFolders()
|
||||||
|
const handleFolderChange = () => loadFolders()
|
||||||
|
window.addEventListener('folder-changed', handleFolderChange)
|
||||||
|
return () => window.removeEventListener('folder-changed', handleFolderChange)
|
||||||
|
}, [loadFolders])
|
||||||
|
|
||||||
const handleSearch = async () => {
|
const handleSearch = async () => {
|
||||||
if (!searchQuery.trim()) {
|
if (!searchQuery.trim()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user