Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 33 additions & 12 deletions src/dashboard/react-components/hooks/useTrajectory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ export function useTrajectory(options: UseTrajectoryOptions = {}): UseTrajectory
const hasInitializedRef = useRef(false);
// Track the latest selection to prevent stale fetches from overwriting data
const latestSelectionRef = useRef<string | null>(selectedTrajectoryId);
// Request counter to ensure only the most recent fetch updates state
// This is more robust than trajectory ID comparison for handling race conditions
const requestCounterRef = useRef(0);

// Fetch trajectory status
const fetchStatus = useCallback(async () => {
Expand Down Expand Up @@ -111,9 +114,12 @@ export function useTrajectory(options: UseTrajectoryOptions = {}): UseTrajectory

// Fetch trajectory steps
const fetchSteps = useCallback(async () => {
// Increment request counter and capture it for this request
// This ensures only the most recent request updates state
const requestId = ++requestCounterRef.current;
const trajectoryId = selectedTrajectoryId;

try {
// Capture the ID this fetch is for
const trajectoryId = selectedTrajectoryId;
const basePath = trajectoryId
? `/api/trajectory/steps?trajectoryId=${encodeURIComponent(trajectoryId)}`
: '/api/trajectory/steps';
Expand All @@ -124,8 +130,12 @@ export function useTrajectory(options: UseTrajectoryOptions = {}): UseTrajectory
const response = await fetch(url, { credentials: 'include' });
const data = await response.json();

// Only update state if this fetch matches the current selection
// This prevents stale fetches from overwriting newer data
// Only update state if this is still the most recent request
// Check both request counter AND trajectory ID for double protection
if (requestId !== requestCounterRef.current) {
console.log('[useTrajectory] Ignoring superseded fetch (request', requestId, 'current', requestCounterRef.current, ')');
return;
}
if (trajectoryId !== latestSelectionRef.current) {
console.log('[useTrajectory] Ignoring stale fetch for', trajectoryId, 'current is', latestSelectionRef.current);
return;
Expand All @@ -138,8 +148,11 @@ export function useTrajectory(options: UseTrajectoryOptions = {}): UseTrajectory
setError(data.error || 'Failed to fetch trajectory steps');
}
} catch (err: any) {
console.error('[useTrajectory] Steps fetch error:', err);
setError(err.message);
// Only update error state if this is still the current request
if (requestId === requestCounterRef.current && trajectoryId === latestSelectionRef.current) {
console.error('[useTrajectory] Steps fetch error:', err);
setError(err.message);
}
}
}, [apiBaseUrl, selectedTrajectoryId]);

Expand All @@ -148,15 +161,21 @@ export function useTrajectory(options: UseTrajectoryOptions = {}): UseTrajectory
// Normalize empty string to null for consistency
const normalizedId = id === '' ? null : id;

// Skip if already selected (prevents unnecessary re-fetches)
if (normalizedId === selectedTrajectoryId) {
return;
}

// Increment request counter to invalidate any in-flight fetches immediately
// This is crucial - it ensures that even if an old fetch completes after this,
// its request ID won't match and it will be ignored
requestCounterRef.current++;

// Update the ref immediately so in-flight fetches for other trajectories are ignored
latestSelectionRef.current = normalizedId;

// Clear steps immediately when switching trajectories to prevent showing stale data
// This is the key fix - without this, the old trajectory's steps remain visible
// until the new fetch completes, which looks like "loading wrong trajectory"
if (normalizedId !== selectedTrajectoryId) {
setSteps([]);
}
setSteps([]);

// Set loading immediately to avoid flash of empty state before effect runs
if (normalizedId !== null) {
Expand Down Expand Up @@ -188,13 +207,15 @@ export function useTrajectory(options: UseTrajectoryOptions = {}): UseTrajectory
}, [refresh]);

// Re-fetch steps when selected trajectory changes
// Note: Initial fetch is handled by the refresh() call in the mount effect
useEffect(() => {
// Skip the initial render - refresh() handles it
if (!hasLoadedInitialStepsRef.current) {
hasLoadedInitialStepsRef.current = true;
fetchSteps();
return;
}

// For subsequent selection changes, fetch with loading state management
let cancelled = false;
setIsLoading(true);
fetchSteps().finally(() => {
Expand Down
Loading