Fix: Reduce maintenance data request frequency.

Fix: The maintenance list should refresh immediately after a new maintenance entry is created.
This commit is contained in:
Tola Leng
2025-05-24 18:20:33 +08:00
parent df047541b9
commit 23be1fca0f
4 changed files with 172 additions and 86 deletions
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback, useMemo } from 'react'; import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { maintenanceService, MaintenanceItem } from '@/services/maintenance'; import { maintenanceService, MaintenanceItem } from '@/services/maintenance';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { useLanguage } from '@/contexts/LanguageContext'; import { useLanguage } from '@/contexts/LanguageContext';
@@ -16,7 +16,10 @@ export const useMaintenanceData = ({ refreshTrigger = 0 }: UseMaintenanceDataPro
const [filter, setFilter] = useState("upcoming"); const [filter, setFilter] = useState("upcoming");
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [initialized, setInitialized] = useState(false); const [initialized, setInitialized] = useState(false);
const [lastFetchTime, setLastFetchTime] = useState(0);
// Refs for cleanup and request management
const mountedRef = useRef(true);
const currentRequestRef = useRef<Promise<void> | null>(null);
// Memoize categorized data to prevent unnecessary recalculations // Memoize categorized data to prevent unnecessary recalculations
const categorizedData = useMemo(() => { const categorizedData = useMemo(() => {
@@ -48,80 +51,91 @@ export const useMaintenanceData = ({ refreshTrigger = 0 }: UseMaintenanceDataPro
return { upcoming, ongoing, completed }; return { upcoming, ongoing, completed };
}, [allMaintenanceData]); }, [allMaintenanceData]);
// Optimized fetch function with improved debouncing // Simple fetch function - only called when explicitly requested
const fetchMaintenanceData = useCallback(async (force = false) => { const fetchMaintenanceData = useCallback(async (force = false) => {
// Prevent excessive fetching with improved debounce logic // Check if component is still mounted
const now = Date.now(); if (!mountedRef.current) return;
if (!force && now - lastFetchTime < 10000 && lastFetchTime > 0 && initialized) {
console.log("Skipping fetch - recently fetched (within 10s)"); // Prevent duplicate requests
if (currentRequestRef.current) {
console.log("Request already in progress, waiting...");
await currentRequestRef.current;
return; return;
} }
// Only show loading state for initial load, not refreshes // Only show loading state for initial load or forced refresh
if (!initialized) { if (!initialized || force) {
setLoading(true); setLoading(true);
} }
setError(null); setError(null);
setLastFetchTime(now);
try { const requestPromise = (async () => {
console.log("Fetching maintenance data from service...");
const data = await maintenanceService.getMaintenanceRecords();
console.log("Fetched maintenance data, count:", data.length);
// Log a sample of the first item's assigned_users for debugging
if (data.length > 0) {
console.log("Sample maintenance item assigned_users:", data[0].id, data[0].assigned_users);
}
// Update state with fetched data
setAllMaintenanceData(data);
setInitialized(true);
} catch (error) {
console.error('Error fetching maintenance data:', error);
setError('Failed to load maintenance data');
toast({
title: t('error'),
description: t('errorFetchingMaintenanceData'),
variant: 'destructive',
});
} finally {
setLoading(false);
}
}, [t, toast, lastFetchTime, initialized]);
// Initial fetch on mount with proper cleanup
useEffect(() => {
console.log("useMaintenanceData hook mounted, fetching data");
let isMounted = true;
const fetchData = async () => {
try { try {
await fetchMaintenanceData(true); console.log("Fetching maintenance data...", force ? "(forced)" : "");
const data = await maintenanceService.getMaintenanceRecords();
// Check if component is still mounted before updating state
if (!mountedRef.current) return;
console.log(`Fetched ${data.length} maintenance records`);
// Update state with fetched data
setAllMaintenanceData(data);
setInitialized(true);
// Clear any previous error
if (error) {
setError(null);
}
} catch (err) { } catch (err) {
console.error("Error in initial fetch:", err); console.error('Error fetching maintenance data:', err);
// Only update error state if component is still mounted
if (!mountedRef.current) return;
const errorMessage = 'Failed to load maintenance data. Please try again.';
setError(errorMessage);
// Show toast for errors
toast({
title: t('error'),
description: t('errorFetchingMaintenanceData'),
variant: 'destructive',
});
} finally {
// Only update loading state if component is still mounted
if (mountedRef.current) {
setLoading(false);
}
currentRequestRef.current = null;
} }
}; })();
fetchData(); currentRequestRef.current = requestPromise;
await requestPromise;
}, [t, toast, error, initialized]);
// Set up polling with longer interval (5 minutes instead of 3) // Initial fetch on mount - NO AUTOMATIC POLLING
const intervalId = setInterval(() => { useEffect(() => {
if (isMounted) fetchMaintenanceData(false); console.log("useMaintenanceData hook mounted, fetching initial data");
}, 300000); // 5 minutes mountedRef.current = true;
// Only fetch initial data, no polling
fetchMaintenanceData(true);
return () => { return () => {
isMounted = false; mountedRef.current = false;
clearInterval(intervalId); currentRequestRef.current = null;
}; };
}, [fetchMaintenanceData]); }, []); // Remove fetchMaintenanceData from dependencies to prevent re-runs
// Handle refresh trigger changes // Handle refresh trigger changes - ONLY when explicitly triggered
useEffect(() => { useEffect(() => {
if (refreshTrigger > 0) { if (refreshTrigger > 0) {
console.log("Refresh trigger changed, forcing data fetch"); console.log("Manual refresh triggered, forcing data fetch");
fetchMaintenanceData(true); fetchMaintenanceData(true); // Force refresh to bypass cache
} }
}, [refreshTrigger, fetchMaintenanceData]); }, [refreshTrigger, fetchMaintenanceData]);
@@ -6,14 +6,21 @@ import { MaintenanceItem } from '../../types/maintenance.types';
// Cache for maintenance records // Cache for maintenance records
let cachedMaintenanceRecords: MaintenanceItem[] | null = null; let cachedMaintenanceRecords: MaintenanceItem[] | null = null;
let lastFetchTimestamp = 0; let lastFetchTimestamp = 0;
const CACHE_EXPIRY_MS = 60000; // 1 minute cache expiry const CACHE_EXPIRY_MS = 600000; // 10 minutes cache expiry (increased significantly)
/** /**
* Check if cache is valid and can be used * Check if cache is valid and can be used
*/ */
export const isCacheValid = (): boolean => { export const isCacheValid = (): boolean => {
const now = Date.now(); const now = Date.now();
return !!(cachedMaintenanceRecords && now - lastFetchTimestamp < CACHE_EXPIRY_MS); const isValid = !!(cachedMaintenanceRecords && now - lastFetchTimestamp < CACHE_EXPIRY_MS);
// Minimal logging to reduce console spam
if (isValid) {
console.log("Using cached maintenance data");
}
return isValid;
}; };
/** /**
@@ -21,7 +28,7 @@ export const isCacheValid = (): boolean => {
*/ */
export const getCachedRecords = () => { export const getCachedRecords = () => {
if (cachedMaintenanceRecords) { if (cachedMaintenanceRecords) {
console.log("Using cached maintenance records", cachedMaintenanceRecords.length); console.log("Returning cached maintenance records:", cachedMaintenanceRecords.length);
return [...cachedMaintenanceRecords]; // Return a copy to prevent mutation return [...cachedMaintenanceRecords]; // Return a copy to prevent mutation
} }
return null; return null;
@@ -44,3 +51,20 @@ export const clearCache = (): void => {
lastFetchTimestamp = 0; lastFetchTimestamp = 0;
console.log("Maintenance cache cleared"); console.log("Maintenance cache cleared");
}; };
/**
* Get cache statistics for debugging
*/
export const getCacheStats = () => {
const now = Date.now();
const cacheAge = lastFetchTimestamp ? now - lastFetchTimestamp : 0;
const timeUntilExpiry = lastFetchTimestamp ? CACHE_EXPIRY_MS - cacheAge : 0;
return {
hasCache: !!cachedMaintenanceRecords,
cacheSize: cachedMaintenanceRecords?.length || 0,
cacheAge: Math.round(cacheAge / 1000), // in seconds
timeUntilExpiry: Math.round(timeUntilExpiry / 1000), // in seconds
isValid: isCacheValid()
};
};
@@ -2,58 +2,104 @@
import { pb } from '@/lib/pocketbase'; import { pb } from '@/lib/pocketbase';
import { MaintenanceItem } from '../../types/maintenance.types'; import { MaintenanceItem } from '../../types/maintenance.types';
import { normalizeMaintenanceItem } from '../maintenanceUtils'; import { normalizeMaintenanceItem } from '../maintenanceUtils';
import { isCacheValid, getCachedRecords, updateCache } from './maintenanceCache'; import { isCacheValid, getCachedRecords, updateCache, clearCache } from './maintenanceCache';
// Request management
let currentRequest: Promise<MaintenanceItem[]> | null = null;
let lastRequestTime = 0;
const MIN_REQUEST_INTERVAL = 30000; // 30 seconds minimum between requests
/** /**
* Get all maintenance records from the API with optimized caching * Get all maintenance records from the API with strict request control
*/ */
export const fetchAllMaintenanceRecords = async (forceRefresh = false): Promise<MaintenanceItem[]> => { export const fetchAllMaintenanceRecords = async (forceRefresh = false): Promise<MaintenanceItem[]> => {
try { try {
const now = Date.now(); const now = Date.now();
// If forced refresh, clear cache first
if (forceRefresh) {
clearCache();
console.log('Cache cleared for forced refresh');
}
// Return cached data if available and not forced refresh // Return cached data if available and not forced refresh
if (!forceRefresh && isCacheValid()) { if (!forceRefresh && isCacheValid()) {
return getCachedRecords() as MaintenanceItem[]; return getCachedRecords() as MaintenanceItem[];
} }
// Use a unique requestKey to prevent caching issues // Request deduplication: if a request is already in progress, wait for it
const timestamp = now; if (currentRequest) {
console.log('Request already in progress, waiting for completion');
return await currentRequest;
}
// Increase items per page and use fields parameter to fetch only needed data // Strict rate limiting - prevent requests too close together (unless forced)
// Also expand the assigned_users relation to get user details const timeSinceLastRequest = now - lastRequestTime;
if (timeSinceLastRequest < MIN_REQUEST_INTERVAL && !forceRefresh) {
console.log(`Rate limiting: ${Math.round((MIN_REQUEST_INTERVAL - timeSinceLastRequest) / 1000)}s remaining`);
return getCachedRecords() || [];
}
lastRequestTime = now;
// Create the request promise
currentRequest = performRequest(now);
try {
const result = await currentRequest;
return result;
} finally {
currentRequest = null; // Clear the current request
}
} catch (error) {
currentRequest = null; // Clear the current request on error
console.error('Error fetching maintenance records:', error);
// Return cached data on error if available
return getCachedRecords() || [];
}
};
/**
* Perform the actual API request
*/
const performRequest = async (timestamp: number): Promise<MaintenanceItem[]> => {
console.log('Making API request for maintenance records...');
// Create abort controller for request timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
try {
const result = await pb.collection('maintenance').getList(1, 200, { const result = await pb.collection('maintenance').getList(1, 200, {
sort: '-created', sort: '-created',
requestKey: `maintenance-${timestamp}`, requestKey: `maintenance-${timestamp}`,
$cancelKey: `maintenance-fetch-${timestamp}`, $cancelKey: `maintenance-fetch-${timestamp}`,
expand: 'assigned_users', // Expand the relation to get user details expand: 'assigned_users',
signal: controller.signal,
}); });
clearTimeout(timeoutId);
if (!result || !result.items || result.items.length === 0) { if (!result || !result.items || result.items.length === 0) {
const emptyData: MaintenanceItem[] = []; const emptyData: MaintenanceItem[] = [];
updateCache(emptyData, now); updateCache(emptyData, timestamp);
return emptyData; return emptyData;
} }
// Process and normalize data // Process and normalize data
const normalizedData = result.items.map(item => { const normalizedData = result.items.map(item => normalizeMaintenanceItem(item));
console.log("Processing maintenance item:", item.id);
console.log("Item assigned_users:", item.assigned_users);
console.log("Notification channel:", item.notification_channel_id);
return normalizeMaintenanceItem(item);
});
// Update cache // Update cache
updateCache(normalizedData, now); updateCache(normalizedData, timestamp);
console.log(`Successfully fetched ${normalizedData.length} maintenance records`);
return normalizedData; return normalizedData;
} catch (error) {
// Handle abort errors gracefully without console errors
if ((error as any)?.isAbort) {
console.log("Request aborted:", error);
return getCachedRecords() || [];
}
console.error('Error fetching maintenance records:', error); } catch (error) {
return getCachedRecords() || []; clearTimeout(timeoutId);
throw error;
} }
}; };
@@ -61,12 +61,14 @@ const getCompletedMaintenance = async (): Promise<MaintenanceItem[]> => {
const createMaintenance = async (data: CreateMaintenanceInput): Promise<void> => { const createMaintenance = async (data: CreateMaintenanceInput): Promise<void> => {
await createMaintenanceRecord(data); await createMaintenanceRecord(data);
clearMaintenanceCache(); // Force cache refresh after creation clearMaintenanceCache(); // Force cache refresh after creation
console.log('Maintenance created and cache cleared');
}; };
// Update an existing maintenance record // Update an existing maintenance record
const updateMaintenanceRecord = async (id: string, data: Partial<MaintenanceItem>): Promise<void> => { const updateMaintenanceRecord = async (id: string, data: Partial<MaintenanceItem>): Promise<void> => {
await updateMaintenance(id, data); await updateMaintenance(id, data);
clearMaintenanceCache(); // Force cache refresh after update clearMaintenanceCache(); // Force cache refresh after update
console.log('Maintenance updated and cache cleared');
}; };
// Refresh cache manually // Refresh cache manually