notifications
This commit is contained in:
parent
abb3a4ba0e
commit
8f523d65c0
@ -19,7 +19,10 @@ export async function GET(request: Request) {
|
||||
const notificationService = NotificationService.getInstance();
|
||||
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) {
|
||||
console.error('Error in notification count API:', error);
|
||||
return NextResponse.json(
|
||||
|
||||
@ -38,12 +38,15 @@ export async function GET(request: Request) {
|
||||
const notificationService = NotificationService.getInstance();
|
||||
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,
|
||||
page,
|
||||
limit,
|
||||
total: notifications.length
|
||||
});
|
||||
response.headers.set('Cache-Control', 'private, max-age=30'); // Cache for 30 seconds on client
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
console.error('Error in notifications API:', error);
|
||||
return NextResponse.json(
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Bell } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@ -8,7 +8,8 @@ interface NotificationBadgeProps {
|
||||
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 hasUnread = notificationCount.unread > 0;
|
||||
|
||||
@ -32,4 +33,4 @@ export function NotificationBadge({ className }: NotificationBadgeProps) {
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { Notification, NotificationCount } from '@/lib/types/notification';
|
||||
|
||||
@ -9,20 +9,47 @@ const defaultNotificationCount: NotificationCount = {
|
||||
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() {
|
||||
const { data: session, status } = useSession();
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
const [notificationCount, setNotificationCount] = useState<NotificationCount>(defaultNotificationCount);
|
||||
const [loading, setLoading] = useState(false);
|
||||
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);
|
||||
|
||||
// Minimum time between fetches (in milliseconds)
|
||||
const MIN_FETCH_INTERVAL = 5000; // 5 seconds
|
||||
const POLLING_INTERVAL = 60000; // 1 minute
|
||||
|
||||
// Fetch notification count
|
||||
const fetchNotificationCount = useCallback(async () => {
|
||||
if (!session?.user) return;
|
||||
// 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 {
|
||||
setError(null);
|
||||
lastFetchTimeRef.current = now;
|
||||
|
||||
const response = await fetch('/api/notifications/count');
|
||||
|
||||
@ -34,19 +61,34 @@ export function useNotifications() {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setNotificationCount(data);
|
||||
if (isMountedRef.current) {
|
||||
setNotificationCount(data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching notification count:', err);
|
||||
setError('Failed to fetch notification count');
|
||||
}
|
||||
}, [session?.user]);
|
||||
|
||||
// Debounced version to prevent rapid successive calls
|
||||
const debouncedFetchCount = useCallback(
|
||||
debounce(fetchNotificationCount, 300),
|
||||
[fetchNotificationCount]
|
||||
);
|
||||
|
||||
// Fetch notifications
|
||||
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);
|
||||
setError(null);
|
||||
lastFetchTimeRef.current = now;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/notifications?page=${page}&limit=${limit}`);
|
||||
@ -59,18 +101,22 @@ export function useNotifications() {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setNotifications(data.notifications);
|
||||
if (isMountedRef.current) {
|
||||
setNotifications(data.notifications);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching notifications:', err);
|
||||
setError('Failed to fetch notifications');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (isMountedRef.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [session?.user]);
|
||||
|
||||
// Mark notification as read
|
||||
const markAsRead = useCallback(async (notificationId: string) => {
|
||||
if (!session?.user) return;
|
||||
if (!session?.user) return false;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/notifications/${notificationId}/read`, {
|
||||
@ -96,18 +142,18 @@ export function useNotifications() {
|
||||
);
|
||||
|
||||
// Refresh notification count
|
||||
fetchNotificationCount();
|
||||
debouncedFetchCount(true);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error marking notification as read:', err);
|
||||
return false;
|
||||
}
|
||||
}, [session?.user, fetchNotificationCount]);
|
||||
}, [session?.user, debouncedFetchCount]);
|
||||
|
||||
// Mark all notifications as read
|
||||
const markAllAsRead = useCallback(async () => {
|
||||
if (!session?.user) return;
|
||||
if (!session?.user) return false;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/notifications/read-all', {
|
||||
@ -129,55 +175,62 @@ export function useNotifications() {
|
||||
);
|
||||
|
||||
// Refresh notification count
|
||||
fetchNotificationCount();
|
||||
debouncedFetchCount(true);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error marking all notifications as read:', err);
|
||||
return false;
|
||||
}
|
||||
}, [session?.user, fetchNotificationCount]);
|
||||
}, [session?.user, debouncedFetchCount]);
|
||||
|
||||
// Start polling for notification count
|
||||
const startPolling = useCallback((interval = 30000) => {
|
||||
if (pollingInterval) {
|
||||
clearInterval(pollingInterval);
|
||||
const startPolling = useCallback(() => {
|
||||
if (isPollingRef.current) return;
|
||||
|
||||
isPollingRef.current = true;
|
||||
|
||||
if (pollingIntervalRef.current) {
|
||||
clearInterval(pollingIntervalRef.current);
|
||||
}
|
||||
|
||||
const id = setInterval(() => {
|
||||
fetchNotificationCount();
|
||||
}, interval);
|
||||
// Ensure we don't create multiple intervals
|
||||
pollingIntervalRef.current = setInterval(() => {
|
||||
if (isMountedRef.current) {
|
||||
debouncedFetchCount();
|
||||
}
|
||||
}, POLLING_INTERVAL);
|
||||
|
||||
setPollingInterval(id);
|
||||
|
||||
return () => {
|
||||
clearInterval(id);
|
||||
setPollingInterval(null);
|
||||
};
|
||||
}, [fetchNotificationCount, pollingInterval]);
|
||||
return () => stopPolling();
|
||||
}, [debouncedFetchCount]);
|
||||
|
||||
// Stop polling
|
||||
const stopPolling = useCallback(() => {
|
||||
if (pollingInterval) {
|
||||
clearInterval(pollingInterval);
|
||||
setPollingInterval(null);
|
||||
if (pollingIntervalRef.current) {
|
||||
clearInterval(pollingIntervalRef.current);
|
||||
pollingIntervalRef.current = null;
|
||||
}
|
||||
}, [pollingInterval]);
|
||||
isPollingRef.current = false;
|
||||
}, []);
|
||||
|
||||
// Initialize fetching on component mount
|
||||
// Initialize fetching on component mount and cleanup on unmount
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
|
||||
if (status === 'authenticated' && session?.user) {
|
||||
fetchNotificationCount();
|
||||
// Initial fetches
|
||||
fetchNotificationCount(true);
|
||||
fetchNotifications();
|
||||
|
||||
// Start polling
|
||||
const cleanup = startPolling();
|
||||
|
||||
return () => {
|
||||
cleanup();
|
||||
};
|
||||
startPolling();
|
||||
}
|
||||
}, [status, session?.user, fetchNotificationCount, fetchNotifications, startPolling]);
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
stopPolling();
|
||||
};
|
||||
}, [status, session?.user, fetchNotificationCount, fetchNotifications, startPolling, stopPolling]);
|
||||
|
||||
return {
|
||||
notifications,
|
||||
@ -185,10 +238,8 @@ export function useNotifications() {
|
||||
loading,
|
||||
error,
|
||||
fetchNotifications,
|
||||
fetchNotificationCount,
|
||||
fetchNotificationCount: () => debouncedFetchCount(true),
|
||||
markAsRead,
|
||||
markAllAsRead,
|
||||
startPolling,
|
||||
stopPolling
|
||||
markAllAsRead
|
||||
};
|
||||
}
|
||||
@ -278,8 +278,27 @@ export class NotificationService {
|
||||
// Use setTimeout to make this non-blocking
|
||||
setTimeout(async () => {
|
||||
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}`);
|
||||
|
||||
// Set last refresh time
|
||||
await redis.set(refreshKey, Date.now().toString(), 'EX', 120); // 2 minute TTL
|
||||
|
||||
// Refresh counts and notifications (for first page)
|
||||
await this.getNotificationCount(userId);
|
||||
await this.getNotifications(userId, 1, 20);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user