mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 20:11:12 -05:00
Implement cache invalidation improvements
This commit is contained in:
@@ -1,11 +1,17 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient, QueryCache } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
interface InvalidationEvent {
|
||||||
|
timestamp: number;
|
||||||
|
queryKey: readonly unknown[];
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CacheMonitor Component (Dev Only)
|
* CacheMonitor Component (Dev Only)
|
||||||
*
|
*
|
||||||
* Real-time cache performance monitoring for development.
|
* 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.
|
* Only renders in development mode.
|
||||||
*/
|
*/
|
||||||
export function CacheMonitor() {
|
export function CacheMonitor() {
|
||||||
@@ -16,7 +22,10 @@ export function CacheMonitor() {
|
|||||||
fetchingQueries: 0,
|
fetchingQueries: 0,
|
||||||
cacheSize: 0,
|
cacheSize: 0,
|
||||||
});
|
});
|
||||||
|
const [recentInvalidations, setRecentInvalidations] = useState<InvalidationEvent[]>([]);
|
||||||
|
const invalidationsRef = useRef<InvalidationEvent[]>([]);
|
||||||
|
|
||||||
|
// Monitor cache stats
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
const cache = queryClient.getQueryCache();
|
const cache = queryClient.getQueryCache();
|
||||||
@@ -28,22 +37,85 @@ export function CacheMonitor() {
|
|||||||
fetchingQueries: queries.filter(q => q.state.fetchStatus === 'fetching').length,
|
fetchingQueries: queries.filter(q => q.state.fetchStatus === 'fetching').length,
|
||||||
cacheSize: JSON.stringify(queries).length,
|
cacheSize: JSON.stringify(queries).length,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update invalidations display
|
||||||
|
setRecentInvalidations([...invalidationsRef.current]);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [queryClient]);
|
}, [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;
|
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 (
|
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">
|
<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-2 text-primary">Cache Monitor</h3>
|
<h3 className="font-bold mb-3 text-primary border-b border-primary/30 pb-2">Cache Monitor</h3>
|
||||||
<div className="space-y-1">
|
|
||||||
<div>Total Queries: <span className="text-green-400">{stats.totalQueries}</span></div>
|
<div className="space-y-1 mb-3 pb-3 border-b border-white/10">
|
||||||
<div>Stale: <span className="text-yellow-400">{stats.staleQueries}</span></div>
|
<div className="flex justify-between">
|
||||||
<div>Fetching: <span className="text-blue-400">{stats.fetchingQueries}</span></div>
|
<span>Total Queries:</span>
|
||||||
<div>Size: <span className="text-purple-400">{(stats.cacheSize / 1024).toFixed(1)} KB</span></div>
|
<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>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import { StarRating } from './StarRating';
|
|||||||
import { toDateOnly } from '@/lib/dateUtils';
|
import { toDateOnly } from '@/lib/dateUtils';
|
||||||
import { getErrorMessage } from '@/lib/errorHandler';
|
import { getErrorMessage } from '@/lib/errorHandler';
|
||||||
import { logger } from '@/lib/logger';
|
import { logger } from '@/lib/logger';
|
||||||
|
import { useQueryInvalidation } from '@/lib/queryInvalidation';
|
||||||
|
|
||||||
const reviewSchema = z.object({
|
const reviewSchema = z.object({
|
||||||
rating: z.number().min(0.5).max(5).multipleOf(0.5),
|
rating: z.number().min(0.5).max(5).multipleOf(0.5),
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
@@ -41,6 +43,7 @@ export function ReviewForm({
|
|||||||
const {
|
const {
|
||||||
user
|
user
|
||||||
} = useAuth();
|
} = useAuth();
|
||||||
|
const { invalidateEntityReviews } = useQueryInvalidation();
|
||||||
const [rating, setRating] = useState(0);
|
const [rating, setRating] = useState(0);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [photos, setPhotos] = useState<string[]>([]);
|
const [photos, setPhotos] = useState<string[]>([]);
|
||||||
@@ -118,6 +121,10 @@ export function ReviewForm({
|
|||||||
title: "Review Submitted!",
|
title: "Review Submitted!",
|
||||||
description: "Thank you for your review. It will be published after moderation."
|
description: "Thank you for your review. It will be published after moderation."
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Invalidate review cache for instant UI update
|
||||||
|
invalidateEntityReviews(entityType, entityId);
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
setRating(0);
|
setRating(0);
|
||||||
setPhotos([]);
|
setPhotos([]);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
import { handleError, handleSuccess } from '@/lib/errorHandler';
|
import { handleError, handleSuccess } from '@/lib/errorHandler';
|
||||||
|
import { useQueryInvalidation } from '@/lib/queryInvalidation';
|
||||||
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
|
|
||||||
export type AvatarUploadState = {
|
export type AvatarUploadState = {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -13,6 +15,8 @@ export const useAvatarUpload = (
|
|||||||
initialImageId: string = '',
|
initialImageId: string = '',
|
||||||
username: string
|
username: string
|
||||||
) => {
|
) => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const { invalidateUserProfile } = useQueryInvalidation();
|
||||||
const [state, setState] = useState<AvatarUploadState>({
|
const [state, setState] = useState<AvatarUploadState>({
|
||||||
url: initialUrl,
|
url: initialUrl,
|
||||||
imageId: initialImageId,
|
imageId: initialImageId,
|
||||||
@@ -48,6 +52,11 @@ export const useAvatarUpload = (
|
|||||||
setState(prev => ({ ...prev, isUploading: false }));
|
setState(prev => ({ ...prev, isUploading: false }));
|
||||||
handleSuccess('Avatar updated', 'Your avatar has been successfully updated.');
|
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 };
|
return { success: true };
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
// Rollback on error
|
// Rollback on error
|
||||||
@@ -64,7 +73,7 @@ export const useAvatarUpload = (
|
|||||||
|
|
||||||
return { success: false, error };
|
return { success: false, error };
|
||||||
}
|
}
|
||||||
}, [username, initialUrl, initialImageId]);
|
}, [username, initialUrl, initialImageId, user?.id, invalidateUserProfile]);
|
||||||
|
|
||||||
const resetAvatar = useCallback(() => {
|
const resetAvatar = useCallback(() => {
|
||||||
setState({
|
setState({
|
||||||
|
|||||||
@@ -324,5 +324,14 @@ export function useQueryInvalidation() {
|
|||||||
queryKey: queryKeys.entities.name(entityType, entityId)
|
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 { Footer } from '@/components/layout/Footer';
|
||||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||||
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
||||||
|
import { useQueryInvalidation } from '@/lib/queryInvalidation';
|
||||||
|
|
||||||
export default function BlogPost() {
|
export default function BlogPost() {
|
||||||
const { slug } = useParams<{ slug: string }>();
|
const { slug } = useParams<{ slug: string }>();
|
||||||
|
const { invalidateBlogPost } = useQueryInvalidation();
|
||||||
|
|
||||||
const { data: post, isLoading } = useBlogPost(slug);
|
const { data: post, isLoading } = useBlogPost(slug);
|
||||||
|
|
||||||
@@ -34,9 +36,12 @@ export default function BlogPost() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (slug) {
|
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user