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)
|
||||
}, [loadFlows])
|
||||
|
||||
// Debounced search
|
||||
// Debounced search with staleness guard
|
||||
const searchRequestId = useRef(0)
|
||||
useEffect(() => {
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current)
|
||||
if (query.length < 2) {
|
||||
@@ -188,13 +189,16 @@ export function QuickStartPage() {
|
||||
setIsSearching(true)
|
||||
setShowResults(true)
|
||||
debounceRef.current = setTimeout(async () => {
|
||||
const requestId = ++searchRequestId.current
|
||||
try {
|
||||
const results = await treesApi.search(query, 8)
|
||||
if (requestId !== searchRequestId.current) return
|
||||
setSearchResults(results)
|
||||
} catch {
|
||||
if (requestId !== searchRequestId.current) return
|
||||
setSearchResults([])
|
||||
} finally {
|
||||
setIsSearching(false)
|
||||
if (requestId === searchRequestId.current) setIsSearching(false)
|
||||
}
|
||||
}, 300)
|
||||
return () => { if (debounceRef.current) clearTimeout(debounceRef.current) }
|
||||
|
||||
@@ -60,7 +60,59 @@ export function SessionHistoryPage() {
|
||||
|
||||
// Load sessions when filters change
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
const loadSessions = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const params: Record<string, string | boolean> = {}
|
||||
|
||||
// Tab filter (all/active/completed)
|
||||
if (filter !== 'all') {
|
||||
params.completed = filter === 'completed'
|
||||
}
|
||||
|
||||
// Search/filter params
|
||||
if (filters.ticketNumber) {
|
||||
params.ticket_number = filters.ticketNumber
|
||||
}
|
||||
if (filters.clientName) {
|
||||
params.client_name = filters.clientName
|
||||
}
|
||||
if (filters.treeName) {
|
||||
params.tree_name = filters.treeName
|
||||
}
|
||||
|
||||
// Date range params
|
||||
if (filters.dateRange?.from) {
|
||||
const fromDate = filters.dateRange.from
|
||||
const toDate = filters.dateRange.to || filters.dateRange.from
|
||||
|
||||
if (filters.dateType === 'started') {
|
||||
params.started_after = fromDate.toISOString()
|
||||
params.started_before = toDate.toISOString()
|
||||
} else {
|
||||
params.completed_after = fromDate.toISOString()
|
||||
params.completed_before = toDate.toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
const sessionsData = await sessionsApi.list({ ...params, size: 51 })
|
||||
if (cancelled) return
|
||||
const truncated = sessionsData.length > 50
|
||||
setHasMore(truncated)
|
||||
setSessions(truncated ? sessionsData.slice(0, 50) : sessionsData)
|
||||
} catch (err) {
|
||||
if (cancelled) return
|
||||
toast.error('Failed to load sessions')
|
||||
console.error(err)
|
||||
} finally {
|
||||
if (!cancelled) setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadSessions()
|
||||
return () => { cancelled = true }
|
||||
}, [filter, filters])
|
||||
|
||||
// Update URL params when filters change
|
||||
@@ -79,53 +131,6 @@ export function SessionHistoryPage() {
|
||||
setSearchParams(params, { replace: true })
|
||||
}, [filters, setSearchParams])
|
||||
|
||||
const loadSessions = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const params: Record<string, string | boolean> = {}
|
||||
|
||||
// Tab filter (all/active/completed)
|
||||
if (filter !== 'all') {
|
||||
params.completed = filter === 'completed'
|
||||
}
|
||||
|
||||
// Search/filter params
|
||||
if (filters.ticketNumber) {
|
||||
params.ticket_number = filters.ticketNumber
|
||||
}
|
||||
if (filters.clientName) {
|
||||
params.client_name = filters.clientName
|
||||
}
|
||||
if (filters.treeName) {
|
||||
params.tree_name = filters.treeName
|
||||
}
|
||||
|
||||
// Date range params
|
||||
if (filters.dateRange?.from) {
|
||||
const fromDate = filters.dateRange.from
|
||||
const toDate = filters.dateRange.to || filters.dateRange.from
|
||||
|
||||
if (filters.dateType === 'started') {
|
||||
params.started_after = fromDate.toISOString()
|
||||
params.started_before = toDate.toISOString()
|
||||
} else {
|
||||
params.completed_after = fromDate.toISOString()
|
||||
params.completed_before = toDate.toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
const sessionsData = await sessionsApi.list({ ...params, size: 51 })
|
||||
const truncated = sessionsData.length > 50
|
||||
setHasMore(truncated)
|
||||
setSessions(truncated ? sessionsData.slice(0, 50) : sessionsData)
|
||||
} catch (err) {
|
||||
toast.error('Failed to load sessions')
|
||||
console.error(err)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFilterChange = (newFilters: SessionFilterState) => {
|
||||
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 { X, RotateCcw, Play, FileUp } from 'lucide-react'
|
||||
import { PageMeta } from '@/components/common/PageMeta'
|
||||
@@ -158,20 +158,11 @@ export function TreeLibraryPage() {
|
||||
.catch((err) => console.error('Failed to load categories:', err))
|
||||
}, [])
|
||||
|
||||
// Load trees when filters change
|
||||
useEffect(() => {
|
||||
loadTrees()
|
||||
}, [selectedCategoryId, selectedTags, selectedFolderId, treeLibrarySortBy, typeFilter])
|
||||
// Request ID ref to discard stale responses when filters change rapidly
|
||||
const loadTreesRequestId = useRef(0)
|
||||
|
||||
// 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 loadTrees = async () => {
|
||||
const loadTrees = useCallback(async () => {
|
||||
const requestId = ++loadTreesRequestId.current
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const treesData = await treesApi.list({
|
||||
@@ -181,14 +172,29 @@ export function TreeLibraryPage() {
|
||||
folder_id: selectedFolderId || undefined,
|
||||
sort_by: treeLibrarySortBy,
|
||||
})
|
||||
if (requestId !== loadTreesRequestId.current) return
|
||||
setTrees(treesData)
|
||||
} catch (err) {
|
||||
if (requestId !== loadTreesRequestId.current) return
|
||||
toast.error('Failed to load flows')
|
||||
console.error(err)
|
||||
} 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 () => {
|
||||
if (!searchQuery.trim()) {
|
||||
|
||||
Reference in New Issue
Block a user