Fix race condition in moderation queue

This commit is contained in:
gpt-engineer-app[bot]
2025-10-06 20:49:14 +00:00
parent 1c356ed69e
commit 38db325135

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useImperativeHandle, forwardRef } from 'react'; import { useState, useEffect, useImperativeHandle, forwardRef, useCallback, useRef } from 'react';
import { CheckCircle, XCircle, Eye, Calendar, User, Filter, MessageSquare, FileText, Image, X, Trash2, ListTree, RefreshCw, AlertCircle, Clock, Lock, Unlock, AlertTriangle, UserCog, Zap } from 'lucide-react'; import { CheckCircle, XCircle, Eye, Calendar, User, Filter, MessageSquare, FileText, Image, X, Trash2, ListTree, RefreshCw, AlertCircle, Clock, Lock, Unlock, AlertTriangle, UserCog, Zap } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
@@ -26,6 +26,7 @@ import { QueueStatsDashboard } from './QueueStatsDashboard';
import { EscalationDialog } from './EscalationDialog'; import { EscalationDialog } from './EscalationDialog';
import { ReassignDialog } from './ReassignDialog'; import { ReassignDialog } from './ReassignDialog';
import { smartMergeArray } from '@/lib/smartStateUpdate'; import { smartMergeArray } from '@/lib/smartStateUpdate';
import { useDebounce } from '@/hooks/useDebounce';
interface ModerationItem { interface ModerationItem {
id: string; id: string;
@@ -95,6 +96,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
const { isAdmin, isSuperuser } = useUserRole(); const { isAdmin, isSuperuser } = useUserRole();
const { user } = useAuth(); const { user } = useAuth();
const queue = useModerationQueue(); const queue = useModerationQueue();
const fetchInProgressRef = useRef(false);
// Get admin settings for polling configuration // Get admin settings for polling configuration
const { const {
@@ -108,11 +110,19 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
const refreshStrategy = getAutoRefreshStrategy(); const refreshStrategy = getAutoRefreshStrategy();
const preserveInteraction = getPreserveInteractionState(); const preserveInteraction = getPreserveInteractionState();
const fetchItems = async (entityFilter: EntityFilter = 'all', statusFilter: StatusFilter = 'pending', silent = false) => { const fetchItems = useCallback(async (entityFilter: EntityFilter = 'all', statusFilter: StatusFilter = 'pending', silent = false) => {
if (!user) { if (!user) {
return; return;
} }
// Prevent concurrent calls - race condition guard
if (fetchInProgressRef.current) {
console.log('⚠️ Fetch already in progress, skipping duplicate call');
return;
}
fetchInProgressRef.current = true;
console.log('🔍 fetchItems called:', { console.log('🔍 fetchItems called:', {
entityFilter, entityFilter,
statusFilter, statusFilter,
@@ -394,11 +404,26 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
variant: 'destructive', variant: 'destructive',
}); });
} finally { } finally {
fetchInProgressRef.current = false;
setLoading(false); setLoading(false);
setIsRefreshing(false); setIsRefreshing(false);
setIsInitialLoad(false); setIsInitialLoad(false);
} }
}; }, [
user,
entityCache,
profileCache,
submissionMemo,
items,
refreshStrategy,
preserveInteraction,
interactingWith,
toast
]);
// Debounced filters to prevent rapid-fire calls
const debouncedEntityFilter = useDebounce(activeEntityFilter, 300);
const debouncedStatusFilter = useDebounce(activeStatusFilter, 300);
// Expose refresh method via ref // Expose refresh method via ref
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
@@ -410,22 +435,22 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
// Initial fetch on mount and filter changes // Initial fetch on mount and filter changes
useEffect(() => { useEffect(() => {
if (user) { if (user) {
fetchItems(activeEntityFilter, activeStatusFilter, false); // Show loading fetchItems(debouncedEntityFilter, debouncedStatusFilter, false); // Show loading
} }
}, [activeEntityFilter, activeStatusFilter, user, fetchItems]); }, [debouncedEntityFilter, debouncedStatusFilter, user, fetchItems]);
// Polling for auto-refresh // Polling for auto-refresh
useEffect(() => { useEffect(() => {
if (!user || refreshMode !== 'auto' || isInitialLoad) return; if (!user || refreshMode !== 'auto' || isInitialLoad) return;
const interval = setInterval(() => { const interval = setInterval(() => {
fetchItems(activeEntityFilter, activeStatusFilter, true); // Silent refresh fetchItems(debouncedEntityFilter, debouncedStatusFilter, true); // Silent refresh
}, pollInterval); }, pollInterval);
return () => { return () => {
clearInterval(interval); clearInterval(interval);
}; };
}, [user, refreshMode, pollInterval, activeEntityFilter, activeStatusFilter, isInitialLoad, fetchItems]); }, [user, refreshMode, pollInterval, debouncedEntityFilter, debouncedStatusFilter, isInitialLoad, fetchItems]);
// Real-time subscription for lock status // Real-time subscription for lock status
useEffect(() => { useEffect(() => {