mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 18:11:12 -05:00
Implement cache invalidation improvements
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useQueryClient, QueryCache } from '@tanstack/react-query';
|
||||
|
||||
interface InvalidationEvent {
|
||||
timestamp: number;
|
||||
queryKey: readonly unknown[];
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* CacheMonitor Component (Dev Only)
|
||||
*
|
||||
* Real-time cache performance monitoring for development.
|
||||
* Displays total queries, stale queries, fetching queries, and cache size.
|
||||
* Displays total queries, stale queries, fetching queries, cache size, and invalidations.
|
||||
* Only renders in development mode.
|
||||
*/
|
||||
export function CacheMonitor() {
|
||||
@@ -16,7 +22,10 @@ export function CacheMonitor() {
|
||||
fetchingQueries: 0,
|
||||
cacheSize: 0,
|
||||
});
|
||||
const [recentInvalidations, setRecentInvalidations] = useState<InvalidationEvent[]>([]);
|
||||
const invalidationsRef = useRef<InvalidationEvent[]>([]);
|
||||
|
||||
// Monitor cache stats
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
const cache = queryClient.getQueryCache();
|
||||
@@ -28,22 +37,85 @@ export function CacheMonitor() {
|
||||
fetchingQueries: queries.filter(q => q.state.fetchStatus === 'fetching').length,
|
||||
cacheSize: JSON.stringify(queries).length,
|
||||
});
|
||||
|
||||
// Update invalidations display
|
||||
setRecentInvalidations([...invalidationsRef.current]);
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [queryClient]);
|
||||
|
||||
// Track invalidations
|
||||
useEffect(() => {
|
||||
const cache = queryClient.getQueryCache();
|
||||
|
||||
const unsubscribe = cache.subscribe((event) => {
|
||||
if (event?.type === 'removed' || event?.type === 'updated') {
|
||||
const query = event.query;
|
||||
if (query && query.state.fetchStatus === 'idle' && query.isStale()) {
|
||||
const invalidation: InvalidationEvent = {
|
||||
timestamp: Date.now(),
|
||||
queryKey: query.queryKey,
|
||||
reason: event.type,
|
||||
};
|
||||
|
||||
invalidationsRef.current = [invalidation, ...invalidationsRef.current].slice(0, 5);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, [queryClient]);
|
||||
|
||||
if (!import.meta.env.DEV) return null;
|
||||
|
||||
const formatQueryKey = (key: readonly unknown[]): string => {
|
||||
return JSON.stringify(key).slice(0, 40) + '...';
|
||||
};
|
||||
|
||||
const formatTime = (timestamp: number): string => {
|
||||
const diff = Date.now() - timestamp;
|
||||
if (diff < 1000) return 'just now';
|
||||
if (diff < 60000) return `${Math.floor(diff / 1000)}s ago`;
|
||||
return `${Math.floor(diff / 60000)}m ago`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 bg-black/80 text-white p-4 rounded-lg text-xs font-mono z-50 shadow-xl">
|
||||
<h3 className="font-bold mb-2 text-primary">Cache Monitor</h3>
|
||||
<div className="space-y-1">
|
||||
<div>Total Queries: <span className="text-green-400">{stats.totalQueries}</span></div>
|
||||
<div>Stale: <span className="text-yellow-400">{stats.staleQueries}</span></div>
|
||||
<div>Fetching: <span className="text-blue-400">{stats.fetchingQueries}</span></div>
|
||||
<div>Size: <span className="text-purple-400">{(stats.cacheSize / 1024).toFixed(1)} KB</span></div>
|
||||
<div className="fixed bottom-4 right-4 bg-black/90 text-white p-4 rounded-lg text-xs font-mono z-50 shadow-xl max-w-sm">
|
||||
<h3 className="font-bold mb-3 text-primary border-b border-primary/30 pb-2">Cache Monitor</h3>
|
||||
|
||||
<div className="space-y-1 mb-3 pb-3 border-b border-white/10">
|
||||
<div className="flex justify-between">
|
||||
<span>Total Queries:</span>
|
||||
<span className="text-green-400 font-bold">{stats.totalQueries}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Stale:</span>
|
||||
<span className="text-yellow-400 font-bold">{stats.staleQueries}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Fetching:</span>
|
||||
<span className="text-blue-400 font-bold">{stats.fetchingQueries}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Size:</span>
|
||||
<span className="text-purple-400 font-bold">{(stats.cacheSize / 1024).toFixed(1)} KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{recentInvalidations.length > 0 && (
|
||||
<div>
|
||||
<h4 className="font-bold mb-2 text-accent">Recent Invalidations</h4>
|
||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||
{recentInvalidations.map((inv, i) => (
|
||||
<div key={i} className="text-[10px] opacity-80 border-l-2 border-accent/50 pl-2">
|
||||
<div className="text-muted-foreground">{formatTime(inv.timestamp)}</div>
|
||||
<div className="truncate">{formatQueryKey(inv.queryKey)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import { StarRating } from './StarRating';
|
||||
import { toDateOnly } from '@/lib/dateUtils';
|
||||
import { getErrorMessage } from '@/lib/errorHandler';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { useQueryInvalidation } from '@/lib/queryInvalidation';
|
||||
|
||||
const reviewSchema = z.object({
|
||||
rating: z.number().min(0.5).max(5).multipleOf(0.5),
|
||||
title: z.string().optional(),
|
||||
@@ -41,6 +43,7 @@ export function ReviewForm({
|
||||
const {
|
||||
user
|
||||
} = useAuth();
|
||||
const { invalidateEntityReviews } = useQueryInvalidation();
|
||||
const [rating, setRating] = useState(0);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [photos, setPhotos] = useState<string[]>([]);
|
||||
@@ -118,6 +121,10 @@ export function ReviewForm({
|
||||
title: "Review Submitted!",
|
||||
description: "Thank you for your review. It will be published after moderation."
|
||||
});
|
||||
|
||||
// Invalidate review cache for instant UI update
|
||||
invalidateEntityReviews(entityType, entityId);
|
||||
|
||||
reset();
|
||||
setRating(0);
|
||||
setPhotos([]);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { handleError, handleSuccess } from '@/lib/errorHandler';
|
||||
import { useQueryInvalidation } from '@/lib/queryInvalidation';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
|
||||
export type AvatarUploadState = {
|
||||
url: string;
|
||||
@@ -13,6 +15,8 @@ export const useAvatarUpload = (
|
||||
initialImageId: string = '',
|
||||
username: string
|
||||
) => {
|
||||
const { user } = useAuth();
|
||||
const { invalidateUserProfile } = useQueryInvalidation();
|
||||
const [state, setState] = useState<AvatarUploadState>({
|
||||
url: initialUrl,
|
||||
imageId: initialImageId,
|
||||
@@ -48,6 +52,11 @@ export const useAvatarUpload = (
|
||||
setState(prev => ({ ...prev, isUploading: false }));
|
||||
handleSuccess('Avatar updated', 'Your avatar has been successfully updated.');
|
||||
|
||||
// Invalidate user profile cache for instant UI update
|
||||
if (user?.id) {
|
||||
invalidateUserProfile(user.id);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error: unknown) {
|
||||
// Rollback on error
|
||||
@@ -64,7 +73,7 @@ export const useAvatarUpload = (
|
||||
|
||||
return { success: false, error };
|
||||
}
|
||||
}, [username, initialUrl, initialImageId]);
|
||||
}, [username, initialUrl, initialImageId, user?.id, invalidateUserProfile]);
|
||||
|
||||
const resetAvatar = useCallback(() => {
|
||||
setState({
|
||||
|
||||
@@ -324,5 +324,14 @@ export function useQueryInvalidation() {
|
||||
queryKey: queryKeys.entities.name(entityType, entityId)
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Invalidate user profile cache
|
||||
*/
|
||||
invalidateUserProfile: (userId: string) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['profiles', userId]
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,9 +13,11 @@ import { Header } from '@/components/layout/Header';
|
||||
import { Footer } from '@/components/layout/Footer';
|
||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
||||
import { useQueryInvalidation } from '@/lib/queryInvalidation';
|
||||
|
||||
export default function BlogPost() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const { invalidateBlogPost } = useQueryInvalidation();
|
||||
|
||||
const { data: post, isLoading } = useBlogPost(slug);
|
||||
|
||||
@@ -34,9 +36,12 @@ export default function BlogPost() {
|
||||
|
||||
useEffect(() => {
|
||||
if (slug) {
|
||||
supabase.rpc('increment_blog_view_count', { post_slug: slug });
|
||||
supabase.rpc('increment_blog_view_count', { post_slug: slug }).then(() => {
|
||||
// Invalidate blog post cache to update view count
|
||||
invalidateBlogPost(slug);
|
||||
});
|
||||
}
|
||||
}, [slug]);
|
||||
}, [slug, invalidateBlogPost]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user