Files
srbhr--resume-matcher/apps/frontend/lib/context/status-cache.tsx
T
wehub-resource-sync 5bdf4cc89a
Publish Docker Image / publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:36 +08:00

252 lines
6.3 KiB
TypeScript

'use client';
import React, { createContext, useContext, useEffect, useState, useCallback, useRef } from 'react';
import { fetchSystemStatus, type SystemStatus } from '@/lib/api/config';
// Cache duration constants
const LLM_HEALTH_CHECK_INTERVAL = 30 * 60 * 1000; // 30 minutes
const STATUS_STALE_THRESHOLD = 5 * 60 * 1000; // 5 minutes for DB stats
interface CachedStatus {
status: SystemStatus | null;
lastFetched: number | null;
lastLlmCheck: number | null;
isLoading: boolean;
error: string | null;
}
interface StatusCacheContextValue {
// Cached data
status: SystemStatus | null;
isLoading: boolean;
error: string | null;
lastFetched: Date | null;
// Actions
refreshStatus: () => Promise<void>;
refreshLlmHealth: () => Promise<void>;
// Increment counters (for optimistic updates)
incrementResumes: () => void;
decrementResumes: () => void;
incrementJobs: () => void;
incrementImprovements: () => void;
setHasMasterResume: (value: boolean) => void;
}
const StatusCacheContext = createContext<StatusCacheContextValue | null>(null);
export function StatusCacheProvider({ children }: { children: React.ReactNode }) {
const [cache, setCache] = useState<CachedStatus>({
status: null,
lastFetched: null,
lastLlmCheck: null,
isLoading: true,
error: null,
});
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const mountedRef = useRef(true);
// Fetch full status from backend
const refreshStatus = useCallback(async () => {
setCache((prev) => ({ ...prev, isLoading: true, error: null }));
try {
const status = await fetchSystemStatus();
if (!mountedRef.current) return;
const now = Date.now();
setCache({
status,
lastFetched: now,
lastLlmCheck: now, // Full status includes LLM health
isLoading: false,
error: null,
});
} catch (err) {
if (!mountedRef.current) return;
setCache((prev) => ({
...prev,
isLoading: false,
error: (err as Error).message || 'Failed to fetch status',
}));
}
}, []);
// Refresh just LLM health (called periodically)
const refreshLlmHealth = useCallback(async () => {
try {
const status = await fetchSystemStatus();
if (!mountedRef.current) return;
const now = Date.now();
setCache((prev) => ({
...prev,
status: status,
lastFetched: now,
lastLlmCheck: now,
}));
} catch (err) {
// Silent fail for background refresh - keep existing data
console.error('Background LLM health check failed:', err);
}
}, []);
// Counter update methods (optimistic updates)
const incrementResumes = useCallback(() => {
setCache((prev) => {
if (!prev.status) return prev;
return {
...prev,
status: {
...prev.status,
database_stats: {
...prev.status.database_stats,
total_resumes: prev.status.database_stats.total_resumes + 1,
},
},
};
});
}, []);
const decrementResumes = useCallback(() => {
setCache((prev) => {
if (!prev.status) return prev;
return {
...prev,
status: {
...prev.status,
database_stats: {
...prev.status.database_stats,
total_resumes: Math.max(0, prev.status.database_stats.total_resumes - 1),
},
},
};
});
}, []);
const incrementJobs = useCallback(() => {
setCache((prev) => {
if (!prev.status) return prev;
return {
...prev,
status: {
...prev.status,
database_stats: {
...prev.status.database_stats,
total_jobs: prev.status.database_stats.total_jobs + 1,
},
},
};
});
}, []);
const incrementImprovements = useCallback(() => {
setCache((prev) => {
if (!prev.status) return prev;
return {
...prev,
status: {
...prev.status,
database_stats: {
...prev.status.database_stats,
total_improvements: prev.status.database_stats.total_improvements + 1,
},
},
};
});
}, []);
const setHasMasterResume = useCallback((value: boolean) => {
setCache((prev) => {
if (!prev.status) return prev;
return {
...prev,
status: {
...prev.status,
has_master_resume: value,
database_stats: {
...prev.status.database_stats,
has_master_resume: value,
},
},
};
});
}, []);
// Initial fetch on mount
useEffect(() => {
mountedRef.current = true;
refreshStatus();
return () => {
mountedRef.current = false;
};
}, [refreshStatus]);
// Set up periodic LLM health check (every 30 minutes)
useEffect(() => {
intervalRef.current = setInterval(() => {
refreshLlmHealth();
}, LLM_HEALTH_CHECK_INTERVAL);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [refreshLlmHealth]);
const value: StatusCacheContextValue = {
status: cache.status,
isLoading: cache.isLoading,
error: cache.error,
lastFetched: cache.lastFetched ? new Date(cache.lastFetched) : null,
refreshStatus,
refreshLlmHealth,
incrementResumes,
decrementResumes,
incrementJobs,
incrementImprovements,
setHasMasterResume,
};
return <StatusCacheContext.Provider value={value}>{children}</StatusCacheContext.Provider>;
}
export function useStatusCache() {
const context = useContext(StatusCacheContext);
if (!context) {
throw new Error('useStatusCache must be used within a StatusCacheProvider');
}
return context;
}
/**
* Hook to check if status data is stale (older than threshold)
*/
export function useIsStatusStale(thresholdMs: number = STATUS_STALE_THRESHOLD): boolean {
const { lastFetched } = useStatusCache();
const [isStale, setIsStale] = useState(false);
useEffect(() => {
if (!lastFetched) {
setIsStale(true);
return;
}
const checkStale = () => {
const elapsed = Date.now() - lastFetched.getTime();
setIsStale(elapsed > thresholdMs);
};
checkStale();
const interval = setInterval(checkStale, 60000); // Check every minute
return () => clearInterval(interval);
}, [lastFetched, thresholdMs]);
return isStale;
}