Refactor: Implement documentation plan

This commit is contained in:
gpt-engineer-app[bot]
2025-10-31 12:53:45 +00:00
parent c70c5a4150
commit 4f24eaf204
7 changed files with 1867 additions and 1 deletions

387
src/lib/cacheMonitoring.ts Normal file
View File

@@ -0,0 +1,387 @@
/**
* Cache Performance Monitoring Utilities
*
* Provides tools to monitor React Query cache performance in production.
* Use sparingly - only enable when debugging performance issues.
*
* Features:
* - Cache hit/miss tracking
* - Query duration monitoring
* - Slow query detection
* - Invalidation frequency tracking
*
* @example
* import { cacheMonitor } from '@/lib/cacheMonitoring';
*
* // Start monitoring (development only)
* if (process.env.NODE_ENV === 'development') {
* cacheMonitor.start();
* }
*
* // Get metrics
* const metrics = cacheMonitor.getMetrics();
* console.log('Cache hit rate:', metrics.hitRate);
*/
import { QueryClient } from '@tanstack/react-query';
import { logger } from '@/lib/logger';
interface CacheMetrics {
hits: number;
misses: number;
hitRate: number;
totalQueries: number;
avgQueryTime: number;
slowQueries: number;
invalidations: number;
lastReset: Date;
}
interface QueryTiming {
queryKey: string;
startTime: number;
endTime?: number;
duration?: number;
status: 'pending' | 'success' | 'error';
}
class CacheMonitor {
private metrics: CacheMetrics;
private queryTimings: Map<string, QueryTiming>;
private slowQueryThreshold: number = 500; // ms
private enabled: boolean = false;
private listeners: {
onSlowQuery?: (queryKey: string, duration: number) => void;
onCacheMiss?: (queryKey: string) => void;
onInvalidation?: (queryKey: string) => void;
} = {};
constructor() {
this.metrics = this.resetMetrics();
this.queryTimings = new Map();
}
/**
* Start monitoring cache performance
* Should only be used in development or for debugging
*/
start(queryClient?: QueryClient) {
if (this.enabled) {
logger.warn('Cache monitor already started');
return;
}
this.enabled = true;
this.metrics = this.resetMetrics();
logger.info('Cache monitor started', {
slowQueryThreshold: this.slowQueryThreshold
});
// If queryClient provided, set up automatic tracking
if (queryClient) {
this.setupQueryClientTracking(queryClient);
}
}
/**
* Stop monitoring
*/
stop() {
this.enabled = false;
this.queryTimings.clear();
logger.info('Cache monitor stopped');
}
/**
* Reset all metrics
*/
reset() {
this.metrics = this.resetMetrics();
this.queryTimings.clear();
logger.info('Cache metrics reset');
}
/**
* Record a cache hit (data served from cache)
*/
recordHit(queryKey: string) {
if (!this.enabled) return;
this.metrics.hits++;
this.metrics.totalQueries++;
this.updateHitRate();
logger.debug('Cache hit', { queryKey });
}
/**
* Record a cache miss (data fetched from server)
*/
recordMiss(queryKey: string) {
if (!this.enabled) return;
this.metrics.misses++;
this.metrics.totalQueries++;
this.updateHitRate();
logger.debug('Cache miss', { queryKey });
if (this.listeners.onCacheMiss) {
this.listeners.onCacheMiss(queryKey);
}
}
/**
* Start timing a query
*/
startQuery(queryKey: string) {
if (!this.enabled) return;
const key = this.normalizeQueryKey(queryKey);
this.queryTimings.set(key, {
queryKey: key,
startTime: performance.now(),
status: 'pending'
});
}
/**
* End timing a query
*/
endQuery(queryKey: string, status: 'success' | 'error') {
if (!this.enabled) return;
const key = this.normalizeQueryKey(queryKey);
const timing = this.queryTimings.get(key);
if (!timing) {
logger.warn('Query timing not found', { queryKey: key });
return;
}
const endTime = performance.now();
const duration = endTime - timing.startTime;
timing.endTime = endTime;
timing.duration = duration;
timing.status = status;
// Update average query time
const totalTime = this.metrics.avgQueryTime * (this.metrics.totalQueries - 1) + duration;
this.metrics.avgQueryTime = totalTime / this.metrics.totalQueries;
// Check for slow query
if (duration > this.slowQueryThreshold) {
this.metrics.slowQueries++;
logger.warn('Slow query detected', {
queryKey: key,
duration: Math.round(duration),
threshold: this.slowQueryThreshold
});
if (this.listeners.onSlowQuery) {
this.listeners.onSlowQuery(key, duration);
}
}
// Clean up
this.queryTimings.delete(key);
}
/**
* Record a cache invalidation
*/
recordInvalidation(queryKey: string) {
if (!this.enabled) return;
this.metrics.invalidations++;
logger.debug('Cache invalidated', { queryKey });
if (this.listeners.onInvalidation) {
this.listeners.onInvalidation(queryKey);
}
}
/**
* Get current metrics
*/
getMetrics(): Readonly<CacheMetrics> {
return { ...this.metrics };
}
/**
* Get metrics as formatted string
*/
getMetricsReport(): string {
const m = this.metrics;
const uptimeMinutes = Math.round((Date.now() - m.lastReset.getTime()) / 1000 / 60);
return `
Cache Performance Report
========================
Uptime: ${uptimeMinutes} minutes
Total Queries: ${m.totalQueries}
Cache Hits: ${m.hits} (${(m.hitRate * 100).toFixed(1)}%)
Cache Misses: ${m.misses}
Avg Query Time: ${Math.round(m.avgQueryTime)}ms
Slow Queries: ${m.slowQueries}
Invalidations: ${m.invalidations}
`.trim();
}
/**
* Log current metrics to console
*/
logMetrics() {
console.log(this.getMetricsReport());
}
/**
* Set slow query threshold (in milliseconds)
*/
setSlowQueryThreshold(ms: number) {
this.slowQueryThreshold = ms;
logger.info('Slow query threshold updated', { threshold: ms });
}
/**
* Register event listeners
*/
on(event: 'slowQuery', callback: (queryKey: string, duration: number) => void): void;
on(event: 'cacheMiss', callback: (queryKey: string) => void): void;
on(event: 'invalidation', callback: (queryKey: string) => void): void;
on(event: string, callback: (...args: any[]) => void): void {
if (event === 'slowQuery') {
this.listeners.onSlowQuery = callback;
} else if (event === 'cacheMiss') {
this.listeners.onCacheMiss = callback;
} else if (event === 'invalidation') {
this.listeners.onInvalidation = callback;
}
}
/**
* Setup automatic tracking with QueryClient
* @private
*/
private setupQueryClientTracking(queryClient: QueryClient) {
const cache = queryClient.getQueryCache();
// Subscribe to cache updates
const unsubscribe = cache.subscribe((event) => {
if (!this.enabled) return;
const queryKey = this.normalizeQueryKey(event.query.queryKey);
if (event.type === 'updated') {
const query = event.query;
// Check if this is a cache hit or miss
if (query.state.dataUpdatedAt > 0) {
const isCacheHit = query.state.fetchStatus !== 'fetching';
if (isCacheHit) {
this.recordHit(queryKey);
} else {
this.recordMiss(queryKey);
this.startQuery(queryKey);
}
}
// Record when fetch completes
if (query.state.status === 'success' || query.state.status === 'error') {
this.endQuery(queryKey, query.state.status);
}
}
});
// Store unsubscribe function
(this as any)._unsubscribe = unsubscribe;
}
/**
* Normalize query key to string for tracking
* @private
*/
private normalizeQueryKey(queryKey: string | readonly unknown[]): string {
if (typeof queryKey === 'string') {
return queryKey;
}
return JSON.stringify(queryKey);
}
/**
* Update hit rate percentage
* @private
*/
private updateHitRate() {
if (this.metrics.totalQueries === 0) {
this.metrics.hitRate = 0;
} else {
this.metrics.hitRate = this.metrics.hits / this.metrics.totalQueries;
}
}
/**
* Reset metrics to initial state
* @private
*/
private resetMetrics(): CacheMetrics {
return {
hits: 0,
misses: 0,
hitRate: 0,
totalQueries: 0,
avgQueryTime: 0,
slowQueries: 0,
invalidations: 0,
lastReset: new Date()
};
}
}
// Singleton instance
export const cacheMonitor = new CacheMonitor();
/**
* Hook to use cache monitoring in React components
* Only use for debugging - do not leave in production code
*
* @example
* function DebugPanel() {
* const metrics = useCacheMonitoring();
*
* return (
* <div>
* <h3>Cache Stats</h3>
* <p>Hit Rate: {(metrics.hitRate * 100).toFixed(1)}%</p>
* <p>Avg Query Time: {Math.round(metrics.avgQueryTime)}ms</p>
* </div>
* );
* }
*/
export function useCacheMonitoring() {
// Re-render when metrics change (simple polling)
const [metrics, setMetrics] = React.useState(cacheMonitor.getMetrics());
React.useEffect(() => {
const interval = setInterval(() => {
setMetrics(cacheMonitor.getMetrics());
}, 1000);
return () => clearInterval(interval);
}, []);
return metrics;
}
// Only import React if using the hook
let React: any;
try {
React = require('react');
} catch {
// React not available, hook won't work but main exports still functional
}