notifications

This commit is contained in:
alma 2025-05-04 11:06:56 +02:00
parent abb3a4ba0e
commit 8f523d65c0
5 changed files with 126 additions and 49 deletions

View File

@ -19,7 +19,10 @@ export async function GET(request: Request) {
const notificationService = NotificationService.getInstance(); const notificationService = NotificationService.getInstance();
const counts = await notificationService.getNotificationCount(userId); const counts = await notificationService.getNotificationCount(userId);
return NextResponse.json(counts); // Add Cache-Control header to help with client-side caching
const response = NextResponse.json(counts);
response.headers.set('Cache-Control', 'private, max-age=10'); // Cache for 10 seconds on client
return response;
} catch (error: any) { } catch (error: any) {
console.error('Error in notification count API:', error); console.error('Error in notification count API:', error);
return NextResponse.json( return NextResponse.json(

View File

@ -38,12 +38,15 @@ export async function GET(request: Request) {
const notificationService = NotificationService.getInstance(); const notificationService = NotificationService.getInstance();
const notifications = await notificationService.getNotifications(userId, page, limit); const notifications = await notificationService.getNotifications(userId, page, limit);
return NextResponse.json({ // Add Cache-Control header to help with client-side caching
const response = NextResponse.json({
notifications, notifications,
page, page,
limit, limit,
total: notifications.length total: notifications.length
}); });
response.headers.set('Cache-Control', 'private, max-age=30'); // Cache for 30 seconds on client
return response;
} catch (error: any) { } catch (error: any) {
console.error('Error in notifications API:', error); console.error('Error in notifications API:', error);
return NextResponse.json( return NextResponse.json(

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { memo } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { Bell } from 'lucide-react'; import { Bell } from 'lucide-react';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
@ -8,7 +8,8 @@ interface NotificationBadgeProps {
className?: string; className?: string;
} }
export function NotificationBadge({ className }: NotificationBadgeProps) { // Use React.memo to prevent unnecessary re-renders
export const NotificationBadge = memo(function NotificationBadge({ className }: NotificationBadgeProps) {
const { notificationCount } = useNotifications(); const { notificationCount } = useNotifications();
const hasUnread = notificationCount.unread > 0; const hasUnread = notificationCount.unread > 0;
@ -32,4 +33,4 @@ export function NotificationBadge({ className }: NotificationBadgeProps) {
</Link> </Link>
</div> </div>
); );
} });

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { Notification, NotificationCount } from '@/lib/types/notification'; import { Notification, NotificationCount } from '@/lib/types/notification';
@ -9,20 +9,47 @@ const defaultNotificationCount: NotificationCount = {
sources: {} sources: {}
}; };
// Debounce function to limit API calls
function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout | null = null;
return function(...args: Parameters<T>) {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
export function useNotifications() { export function useNotifications() {
const { data: session, status } = useSession(); const { data: session, status } = useSession();
const [notifications, setNotifications] = useState<Notification[]>([]); const [notifications, setNotifications] = useState<Notification[]>([]);
const [notificationCount, setNotificationCount] = useState<NotificationCount>(defaultNotificationCount); const [notificationCount, setNotificationCount] = useState<NotificationCount>(defaultNotificationCount);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [pollingInterval, setPollingInterval] = useState<NodeJS.Timeout | null>(null); const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
const lastFetchTimeRef = useRef<number>(0);
const isMountedRef = useRef<boolean>(false);
const isPollingRef = useRef<boolean>(false);
// Fetch notification count // Minimum time between fetches (in milliseconds)
const fetchNotificationCount = useCallback(async () => { const MIN_FETCH_INTERVAL = 5000; // 5 seconds
if (!session?.user) return; const POLLING_INTERVAL = 60000; // 1 minute
// Fetch notification count with rate limiting
const fetchNotificationCount = useCallback(async (force = false) => {
if (!session?.user || !isMountedRef.current) return;
const now = Date.now();
if (!force && now - lastFetchTimeRef.current < MIN_FETCH_INTERVAL) {
console.log('Skipping notification count fetch - too soon');
return;
}
try { try {
setError(null); setError(null);
lastFetchTimeRef.current = now;
const response = await fetch('/api/notifications/count'); const response = await fetch('/api/notifications/count');
@ -34,19 +61,34 @@ export function useNotifications() {
} }
const data = await response.json(); const data = await response.json();
if (isMountedRef.current) {
setNotificationCount(data); setNotificationCount(data);
}
} catch (err) { } catch (err) {
console.error('Error fetching notification count:', err); console.error('Error fetching notification count:', err);
setError('Failed to fetch notification count'); setError('Failed to fetch notification count');
} }
}, [session?.user]); }, [session?.user]);
// Debounced version to prevent rapid successive calls
const debouncedFetchCount = useCallback(
debounce(fetchNotificationCount, 300),
[fetchNotificationCount]
);
// Fetch notifications // Fetch notifications
const fetchNotifications = useCallback(async (page = 1, limit = 20) => { const fetchNotifications = useCallback(async (page = 1, limit = 20) => {
if (!session?.user) return; if (!session?.user || !isMountedRef.current) return;
const now = Date.now();
if (now - lastFetchTimeRef.current < MIN_FETCH_INTERVAL) {
console.log('Skipping notifications fetch - too soon');
return;
}
setLoading(true); setLoading(true);
setError(null); setError(null);
lastFetchTimeRef.current = now;
try { try {
const response = await fetch(`/api/notifications?page=${page}&limit=${limit}`); const response = await fetch(`/api/notifications?page=${page}&limit=${limit}`);
@ -59,18 +101,22 @@ export function useNotifications() {
} }
const data = await response.json(); const data = await response.json();
if (isMountedRef.current) {
setNotifications(data.notifications); setNotifications(data.notifications);
}
} catch (err) { } catch (err) {
console.error('Error fetching notifications:', err); console.error('Error fetching notifications:', err);
setError('Failed to fetch notifications'); setError('Failed to fetch notifications');
} finally { } finally {
if (isMountedRef.current) {
setLoading(false); setLoading(false);
} }
}
}, [session?.user]); }, [session?.user]);
// Mark notification as read // Mark notification as read
const markAsRead = useCallback(async (notificationId: string) => { const markAsRead = useCallback(async (notificationId: string) => {
if (!session?.user) return; if (!session?.user) return false;
try { try {
const response = await fetch(`/api/notifications/${notificationId}/read`, { const response = await fetch(`/api/notifications/${notificationId}/read`, {
@ -96,18 +142,18 @@ export function useNotifications() {
); );
// Refresh notification count // Refresh notification count
fetchNotificationCount(); debouncedFetchCount(true);
return true; return true;
} catch (err) { } catch (err) {
console.error('Error marking notification as read:', err); console.error('Error marking notification as read:', err);
return false; return false;
} }
}, [session?.user, fetchNotificationCount]); }, [session?.user, debouncedFetchCount]);
// Mark all notifications as read // Mark all notifications as read
const markAllAsRead = useCallback(async () => { const markAllAsRead = useCallback(async () => {
if (!session?.user) return; if (!session?.user) return false;
try { try {
const response = await fetch('/api/notifications/read-all', { const response = await fetch('/api/notifications/read-all', {
@ -129,55 +175,62 @@ export function useNotifications() {
); );
// Refresh notification count // Refresh notification count
fetchNotificationCount(); debouncedFetchCount(true);
return true; return true;
} catch (err) { } catch (err) {
console.error('Error marking all notifications as read:', err); console.error('Error marking all notifications as read:', err);
return false; return false;
} }
}, [session?.user, fetchNotificationCount]); }, [session?.user, debouncedFetchCount]);
// Start polling for notification count // Start polling for notification count
const startPolling = useCallback((interval = 30000) => { const startPolling = useCallback(() => {
if (pollingInterval) { if (isPollingRef.current) return;
clearInterval(pollingInterval);
isPollingRef.current = true;
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
} }
const id = setInterval(() => { // Ensure we don't create multiple intervals
fetchNotificationCount(); pollingIntervalRef.current = setInterval(() => {
}, interval); if (isMountedRef.current) {
debouncedFetchCount();
}
}, POLLING_INTERVAL);
setPollingInterval(id); return () => stopPolling();
}, [debouncedFetchCount]);
return () => {
clearInterval(id);
setPollingInterval(null);
};
}, [fetchNotificationCount, pollingInterval]);
// Stop polling // Stop polling
const stopPolling = useCallback(() => { const stopPolling = useCallback(() => {
if (pollingInterval) { if (pollingIntervalRef.current) {
clearInterval(pollingInterval); clearInterval(pollingIntervalRef.current);
setPollingInterval(null); pollingIntervalRef.current = null;
} }
}, [pollingInterval]); isPollingRef.current = false;
}, []);
// Initialize fetching on component mount // Initialize fetching on component mount and cleanup on unmount
useEffect(() => { useEffect(() => {
isMountedRef.current = true;
if (status === 'authenticated' && session?.user) { if (status === 'authenticated' && session?.user) {
fetchNotificationCount(); // Initial fetches
fetchNotificationCount(true);
fetchNotifications(); fetchNotifications();
// Start polling // Start polling
const cleanup = startPolling(); startPolling();
}
return () => { return () => {
cleanup(); isMountedRef.current = false;
stopPolling();
}; };
} }, [status, session?.user, fetchNotificationCount, fetchNotifications, startPolling, stopPolling]);
}, [status, session?.user, fetchNotificationCount, fetchNotifications, startPolling]);
return { return {
notifications, notifications,
@ -185,10 +238,8 @@ export function useNotifications() {
loading, loading,
error, error,
fetchNotifications, fetchNotifications,
fetchNotificationCount, fetchNotificationCount: () => debouncedFetchCount(true),
markAsRead, markAsRead,
markAllAsRead, markAllAsRead
startPolling,
stopPolling
}; };
} }

View File

@ -278,8 +278,27 @@ export class NotificationService {
// Use setTimeout to make this non-blocking // Use setTimeout to make this non-blocking
setTimeout(async () => { setTimeout(async () => {
try { try {
// Check if we've refreshed recently (within the last minute)
// to avoid excessive refreshes from multiple tabs/components
const refreshKey = `notifications:last_refresh:${userId}`;
const lastRefresh = await redis.get(refreshKey);
if (lastRefresh) {
const lastRefreshTime = parseInt(lastRefresh, 10);
const now = Date.now();
// If refreshed in the last minute, skip
if (now - lastRefreshTime < 60000) {
console.log(`[NOTIFICATION_SERVICE] Skipping background refresh for user ${userId} - refreshed recently`);
return;
}
}
console.log(`[NOTIFICATION_SERVICE] Background refresh started for user ${userId}`); console.log(`[NOTIFICATION_SERVICE] Background refresh started for user ${userId}`);
// Set last refresh time
await redis.set(refreshKey, Date.now().toString(), 'EX', 120); // 2 minute TTL
// Refresh counts and notifications (for first page) // Refresh counts and notifications (for first page)
await this.getNotificationCount(userId); await this.getNotificationCount(userId);
await this.getNotifications(userId, 1, 20); await this.getNotifications(userId, 1, 20);