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:
@@ -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;
|
||||||
}
|
}
|
||||||
};
|
})();
|
||||||
|
|
||||||
|
currentRequestRef.current = requestPromise;
|
||||||
|
await requestPromise;
|
||||||
|
}, [t, toast, error, initialized]);
|
||||||
|
|
||||||
|
// Initial fetch on mount - NO AUTOMATIC POLLING
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("useMaintenanceData hook mounted, fetching initial data");
|
||||||
|
mountedRef.current = true;
|
||||||
|
|
||||||
fetchData();
|
// Only fetch initial data, no polling
|
||||||
|
fetchMaintenanceData(true);
|
||||||
// Set up polling with longer interval (5 minutes instead of 3)
|
|
||||||
const intervalId = setInterval(() => {
|
|
||||||
if (isMounted) fetchMaintenanceData(false);
|
|
||||||
}, 300000); // 5 minutes
|
|
||||||
|
|
||||||
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]);
|
||||||
|
|
||||||
@@ -170,4 +184,4 @@ export const useMaintenanceData = ({ refreshTrigger = 0 }: UseMaintenanceDataPro
|
|||||||
error,
|
error,
|
||||||
initialized,
|
initialized,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -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
|
||||||
@@ -89,4 +91,4 @@ export const maintenanceService = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Re-export types for convenience
|
// Re-export types for convenience
|
||||||
export type { MaintenanceItem, CreateMaintenanceInput };
|
export type { MaintenanceItem, CreateMaintenanceInput };
|
||||||
Reference in New Issue
Block a user