Refactor Notification
This commit is contained in:
parent
0a660bf7f9
commit
7bb9649b6b
307
CRITICAL_FIXES_QUICK_REFERENCE.md
Normal file
307
CRITICAL_FIXES_QUICK_REFERENCE.md
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
# Critical Fixes - Quick Reference Guide
|
||||||
|
|
||||||
|
## 🚨 Top 5 Critical Fixes (Do These First)
|
||||||
|
|
||||||
|
### 1. Fix useNotifications Memory Leak ⚠️ CRITICAL
|
||||||
|
|
||||||
|
**File**: `hooks/use-notifications.ts`
|
||||||
|
**Line**: 239-255
|
||||||
|
|
||||||
|
**Problem**: Cleanup function not properly placed, causing memory leaks
|
||||||
|
|
||||||
|
**Quick Fix**:
|
||||||
|
```typescript
|
||||||
|
useEffect(() => {
|
||||||
|
if (status !== 'authenticated' || !session?.user) return;
|
||||||
|
|
||||||
|
isMountedRef.current = true;
|
||||||
|
|
||||||
|
// Initial fetch
|
||||||
|
fetchNotificationCount(true);
|
||||||
|
fetchNotifications();
|
||||||
|
|
||||||
|
// Start polling with proper cleanup
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
debouncedFetchCount();
|
||||||
|
}
|
||||||
|
}, POLLING_INTERVAL);
|
||||||
|
|
||||||
|
// ✅ Proper cleanup
|
||||||
|
return () => {
|
||||||
|
isMountedRef.current = false;
|
||||||
|
clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, [status, session?.user?.id]); // ✅ Only primitive dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Fix Notification Badge Double Fetching ⚠️ CRITICAL
|
||||||
|
|
||||||
|
**File**: `components/notification-badge.tsx`
|
||||||
|
**Lines**: 65-70, 82-87, 92-99
|
||||||
|
|
||||||
|
**Problem**: Three different places trigger the same fetch simultaneously
|
||||||
|
|
||||||
|
**Quick Fix**:
|
||||||
|
```typescript
|
||||||
|
// Add at top of component
|
||||||
|
const fetchInProgressRef = useRef(false);
|
||||||
|
const lastFetchRef = useRef<number>(0);
|
||||||
|
const FETCH_COOLDOWN = 1000; // 1 second cooldown
|
||||||
|
|
||||||
|
const manualFetch = async () => {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Prevent duplicate fetches
|
||||||
|
if (fetchInProgressRef.current) {
|
||||||
|
console.log('[NOTIFICATION_BADGE] Fetch already in progress');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cooldown check
|
||||||
|
if (now - lastFetchRef.current < FETCH_COOLDOWN) {
|
||||||
|
console.log('[NOTIFICATION_BADGE] Too soon since last fetch');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchInProgressRef.current = true;
|
||||||
|
lastFetchRef.current = now;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetchNotifications(1, 10);
|
||||||
|
} finally {
|
||||||
|
fetchInProgressRef.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove duplicate useEffect hooks, keep only one:
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && status === 'authenticated') {
|
||||||
|
manualFetch();
|
||||||
|
}
|
||||||
|
}, [isOpen, status]); // Only this one
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Fix Redis KEYS Performance Issue ⚠️ CRITICAL
|
||||||
|
|
||||||
|
**File**: `lib/services/notifications/notification-service.ts`
|
||||||
|
**Line**: 293
|
||||||
|
|
||||||
|
**Problem**: `redis.keys()` blocks Redis and is O(N)
|
||||||
|
|
||||||
|
**Quick Fix**:
|
||||||
|
```typescript
|
||||||
|
// BEFORE (Line 293)
|
||||||
|
const listKeys = await redis.keys(listKeysPattern);
|
||||||
|
if (listKeys.length > 0) {
|
||||||
|
await redis.del(...listKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AFTER (Use SCAN)
|
||||||
|
const listKeys: string[] = [];
|
||||||
|
let cursor = '0';
|
||||||
|
do {
|
||||||
|
const [nextCursor, keys] = await redis.scan(
|
||||||
|
cursor,
|
||||||
|
'MATCH',
|
||||||
|
listKeysPattern,
|
||||||
|
'COUNT',
|
||||||
|
100
|
||||||
|
);
|
||||||
|
cursor = nextCursor;
|
||||||
|
if (keys.length > 0) {
|
||||||
|
listKeys.push(...keys);
|
||||||
|
}
|
||||||
|
} while (cursor !== '0');
|
||||||
|
|
||||||
|
if (listKeys.length > 0) {
|
||||||
|
await redis.del(...listKeys);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Fix Widget Interval Cleanup ⚠️ HIGH
|
||||||
|
|
||||||
|
**Files**:
|
||||||
|
- `components/calendar.tsx` (line 70)
|
||||||
|
- `components/parole.tsx` (line 83)
|
||||||
|
- `components/calendar/calendar-widget.tsx` (line 110)
|
||||||
|
|
||||||
|
**Problem**: Intervals may not be cleaned up properly
|
||||||
|
|
||||||
|
**Quick Fix Pattern**:
|
||||||
|
```typescript
|
||||||
|
// BEFORE
|
||||||
|
useEffect(() => {
|
||||||
|
fetchEvents();
|
||||||
|
const intervalId = setInterval(fetchEvents, 300000);
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, []); // ❌ Missing dependencies
|
||||||
|
|
||||||
|
// AFTER
|
||||||
|
useEffect(() => {
|
||||||
|
if (status !== 'authenticated') return;
|
||||||
|
|
||||||
|
const fetchEvents = async () => {
|
||||||
|
// ... fetch logic
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchEvents(); // Initial fetch
|
||||||
|
|
||||||
|
const intervalId = setInterval(fetchEvents, 300000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, [status]); // ✅ Proper dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Fix useEffect Infinite Loop Risk ⚠️ HIGH
|
||||||
|
|
||||||
|
**File**: `hooks/use-notifications.ts`
|
||||||
|
**Line**: 255
|
||||||
|
|
||||||
|
**Problem**: Function dependencies cause infinite re-renders
|
||||||
|
|
||||||
|
**Quick Fix**:
|
||||||
|
```typescript
|
||||||
|
// Remove function dependencies, use refs for stable references
|
||||||
|
const fetchNotificationCountRef = useRef(fetchNotificationCount);
|
||||||
|
const fetchNotificationsRef = useRef(fetchNotifications);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchNotificationCountRef.current = fetchNotificationCount;
|
||||||
|
fetchNotificationsRef.current = fetchNotifications;
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status !== 'authenticated' || !session?.user) return;
|
||||||
|
|
||||||
|
isMountedRef.current = true;
|
||||||
|
|
||||||
|
fetchNotificationCountRef.current(true);
|
||||||
|
fetchNotificationsRef.current();
|
||||||
|
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
fetchNotificationCountRef.current();
|
||||||
|
}
|
||||||
|
}, POLLING_INTERVAL);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMountedRef.current = false;
|
||||||
|
clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, [status, session?.user?.id]); // ✅ Only primitive values
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Additional Quick Wins
|
||||||
|
|
||||||
|
### 6. Add Request Deduplication Utility
|
||||||
|
|
||||||
|
**Create**: `lib/utils/request-deduplication.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const pendingRequests = new Map<string, Promise<any>>();
|
||||||
|
|
||||||
|
export function deduplicateRequest<T>(
|
||||||
|
key: string,
|
||||||
|
requestFn: () => Promise<T>
|
||||||
|
): Promise<T> {
|
||||||
|
if (pendingRequests.has(key)) {
|
||||||
|
return pendingRequests.get(key)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = requestFn().finally(() => {
|
||||||
|
pendingRequests.delete(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
pendingRequests.set(key, promise);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage**:
|
||||||
|
```typescript
|
||||||
|
const data = await deduplicateRequest(
|
||||||
|
`notifications-${userId}`,
|
||||||
|
() => fetch('/api/notifications').then(r => r.json())
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Extract Magic Numbers to Constants
|
||||||
|
|
||||||
|
**Create**: `lib/constants/intervals.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const INTERVALS = {
|
||||||
|
NOTIFICATION_POLLING: 60000, // 1 minute
|
||||||
|
CALENDAR_REFRESH: 300000, // 5 minutes
|
||||||
|
PAROLE_POLLING: 30000, // 30 seconds
|
||||||
|
MIN_FETCH_INTERVAL: 5000, // 5 seconds
|
||||||
|
FETCH_COOLDOWN: 1000, // 1 second
|
||||||
|
} as const;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Add Error Retry Logic
|
||||||
|
|
||||||
|
**Create**: `lib/utils/retry.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export async function retry<T>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
maxAttempts = 3,
|
||||||
|
delay = 1000
|
||||||
|
): Promise<T> {
|
||||||
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt === maxAttempts) throw error;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay * attempt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('Max retry attempts reached');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Testing Checklist
|
||||||
|
|
||||||
|
After applying fixes, test:
|
||||||
|
|
||||||
|
- [ ] No memory leaks (check browser DevTools Memory tab)
|
||||||
|
- [ ] No duplicate API calls (check Network tab)
|
||||||
|
- [ ] Intervals are cleaned up (check console for errors)
|
||||||
|
- [ ] No infinite loops (check React DevTools Profiler)
|
||||||
|
- [ ] Redis performance (check response times)
|
||||||
|
- [ ] Error handling works (test with network offline)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Priority Order
|
||||||
|
|
||||||
|
1. **Fix 1** (Memory Leak) - Do immediately
|
||||||
|
2. **Fix 2** (Double Fetching) - Do immediately
|
||||||
|
3. **Fix 3** (Redis KEYS) - Do immediately
|
||||||
|
4. **Fix 4** (Widget Cleanup) - Do within 24 hours
|
||||||
|
5. **Fix 5** (Infinite Loop) - Do within 24 hours
|
||||||
|
6. **Quick Wins** - Do within 1 week
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last Updated: Critical fixes quick reference*
|
||||||
286
IMPLEMENTATION_CHECKLIST.md
Normal file
286
IMPLEMENTATION_CHECKLIST.md
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
# Implementation Checklist: Unified Refresh System
|
||||||
|
|
||||||
|
## 📋 Step-by-Step Implementation Guide
|
||||||
|
|
||||||
|
### Phase 1: Foundation (Day 1) ⚡ CRITICAL
|
||||||
|
|
||||||
|
#### ✅ Step 1.1: Create Refresh Manager
|
||||||
|
- [ ] Create `lib/services/refresh-manager.ts`
|
||||||
|
- [ ] Test singleton pattern
|
||||||
|
- [ ] Test register/unregister
|
||||||
|
- [ ] Test start/stop
|
||||||
|
- [ ] Test deduplication logic
|
||||||
|
|
||||||
|
**Estimated Time**: 2-3 hours
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ✅ Step 1.2: Create Request Deduplication
|
||||||
|
- [ ] Create `lib/utils/request-deduplication.ts`
|
||||||
|
- [ ] Test deduplication with same key
|
||||||
|
- [ ] Test TTL expiration
|
||||||
|
- [ ] Test cleanup
|
||||||
|
|
||||||
|
**Estimated Time**: 1 hour
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ✅ Step 1.3: Create Constants
|
||||||
|
- [ ] Create `lib/constants/refresh-intervals.ts`
|
||||||
|
- [ ] Define all intervals
|
||||||
|
- [ ] Export helper function
|
||||||
|
|
||||||
|
**Estimated Time**: 30 minutes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ✅ Step 1.4: Create Unified Hook
|
||||||
|
- [ ] Create `hooks/use-unified-refresh.ts`
|
||||||
|
- [ ] Test registration on mount
|
||||||
|
- [ ] Test cleanup on unmount
|
||||||
|
- [ ] Test manual refresh
|
||||||
|
|
||||||
|
**Estimated Time**: 1-2 hours
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: Fix Critical Issues (Day 1-2) 🔴 URGENT
|
||||||
|
|
||||||
|
#### ✅ Step 2.1: Fix Redis KEYS → SCAN
|
||||||
|
- [ ] Update `lib/services/notifications/notification-service.ts` line 293
|
||||||
|
- [ ] Replace `redis.keys()` with `redis.scan()`
|
||||||
|
- [ ] Test with large key sets
|
||||||
|
|
||||||
|
**Estimated Time**: 30 minutes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ✅ Step 2.2: Fix Notification Hook Memory Leak
|
||||||
|
- [ ] Fix `hooks/use-notifications.ts` useEffect cleanup
|
||||||
|
- [ ] Remove function dependencies
|
||||||
|
- [ ] Test cleanup on unmount
|
||||||
|
|
||||||
|
**Estimated Time**: 1 hour
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ✅ Step 2.3: Fix Notification Badge Double Fetch
|
||||||
|
- [ ] Update `components/notification-badge.tsx`
|
||||||
|
- [ ] Remove duplicate useEffect hooks
|
||||||
|
- [ ] Add request deduplication
|
||||||
|
- [ ] Test single fetch per action
|
||||||
|
|
||||||
|
**Estimated Time**: 1 hour
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Refactor Notifications (Day 2) 🟡 HIGH PRIORITY
|
||||||
|
|
||||||
|
#### ✅ Step 3.1: Refactor useNotifications Hook
|
||||||
|
- [ ] Integrate unified refresh
|
||||||
|
- [ ] Add request deduplication
|
||||||
|
- [ ] Remove manual polling
|
||||||
|
- [ ] Test all functionality
|
||||||
|
|
||||||
|
**Estimated Time**: 2-3 hours
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ✅ Step 3.2: Update Notification Badge
|
||||||
|
- [ ] Remove manual fetch logic
|
||||||
|
- [ ] Use hook's refresh function
|
||||||
|
- [ ] Test UI interactions
|
||||||
|
|
||||||
|
**Estimated Time**: 1 hour
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Refactor Widgets (Day 3-4) 🟢 MEDIUM PRIORITY
|
||||||
|
|
||||||
|
#### ✅ Step 4.1: Refactor Calendar Widget
|
||||||
|
- [ ] Update `components/calendar.tsx`
|
||||||
|
- [ ] Use unified refresh hook
|
||||||
|
- [ ] Add request deduplication
|
||||||
|
- [ ] Test refresh functionality
|
||||||
|
|
||||||
|
**Estimated Time**: 1 hour
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ✅ Step 4.2: Refactor Parole Widget
|
||||||
|
- [ ] Update `components/parole.tsx`
|
||||||
|
- [ ] Use unified refresh hook
|
||||||
|
- [ ] Remove manual interval
|
||||||
|
- [ ] Test chat updates
|
||||||
|
|
||||||
|
**Estimated Time**: 1 hour
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ✅ Step 4.3: Refactor News Widget
|
||||||
|
- [ ] Update `components/news.tsx`
|
||||||
|
- [ ] Use unified refresh hook
|
||||||
|
- [ ] Add auto-refresh (was manual only)
|
||||||
|
- [ ] Test news updates
|
||||||
|
|
||||||
|
**Estimated Time**: 1 hour
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ✅ Step 4.4: Refactor Email Widget
|
||||||
|
- [ ] Update `components/email.tsx`
|
||||||
|
- [ ] Use unified refresh hook
|
||||||
|
- [ ] Add auto-refresh (was manual only)
|
||||||
|
- [ ] Test email updates
|
||||||
|
|
||||||
|
**Estimated Time**: 1 hour
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ✅ Step 4.5: Refactor Duties Widget
|
||||||
|
- [ ] Update `components/flow.tsx`
|
||||||
|
- [ ] Use unified refresh hook
|
||||||
|
- [ ] Add auto-refresh (was manual only)
|
||||||
|
- [ ] Test task updates
|
||||||
|
|
||||||
|
**Estimated Time**: 1 hour
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ✅ Step 4.6: Refactor Navigation Bar Time
|
||||||
|
- [ ] Create `components/main-nav-time.tsx`
|
||||||
|
- [ ] Update `components/main-nav.tsx` to use new component
|
||||||
|
- [ ] Use unified refresh hook (1 second interval)
|
||||||
|
- [ ] Test time updates correctly
|
||||||
|
- [ ] Verify cleanup on unmount
|
||||||
|
|
||||||
|
**Estimated Time**: 30 minutes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 5: Testing & Validation (Day 5) ✅ FINAL
|
||||||
|
|
||||||
|
#### ✅ Step 5.1: Memory Leak Testing
|
||||||
|
- [ ] Open DevTools Memory tab
|
||||||
|
- [ ] Monitor memory over 10 minutes
|
||||||
|
- [ ] Verify no memory leaks
|
||||||
|
- [ ] Check interval cleanup
|
||||||
|
|
||||||
|
**Estimated Time**: 1 hour
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ✅ Step 5.2: API Call Reduction Testing
|
||||||
|
- [ ] Open DevTools Network tab
|
||||||
|
- [ ] Monitor API calls for 5 minutes
|
||||||
|
- [ ] Verify deduplication works
|
||||||
|
- [ ] Count total calls (should be ~60% less)
|
||||||
|
|
||||||
|
**Estimated Time**: 1 hour
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ✅ Step 5.3: Performance Testing
|
||||||
|
- [ ] Test page load time
|
||||||
|
- [ ] Test widget refresh times
|
||||||
|
- [ ] Test with multiple tabs open
|
||||||
|
- [ ] Verify no performance degradation
|
||||||
|
|
||||||
|
**Estimated Time**: 1 hour
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ✅ Step 5.4: User Experience Testing
|
||||||
|
- [ ] Test all widgets refresh correctly
|
||||||
|
- [ ] Test manual refresh buttons
|
||||||
|
- [ ] Test notification updates
|
||||||
|
- [ ] Verify smooth UX
|
||||||
|
|
||||||
|
**Estimated Time**: 1 hour
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Daily Progress Tracking
|
||||||
|
|
||||||
|
### Day 1 Target:
|
||||||
|
- [x] Phase 1: Foundation (Steps 1.1-1.4)
|
||||||
|
- [x] Phase 2: Critical Fixes (Steps 2.1-2.3)
|
||||||
|
|
||||||
|
**Status**: ⏳ In Progress
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Day 2 Target:
|
||||||
|
- [ ] Phase 3: Notifications (Steps 3.1-3.2)
|
||||||
|
|
||||||
|
**Status**: ⏸️ Pending
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Day 3 Target:
|
||||||
|
- [ ] Phase 4: Widgets Part 1 (Steps 4.1-4.2)
|
||||||
|
|
||||||
|
**Status**: ⏸️ Pending
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Day 4 Target:
|
||||||
|
- [ ] Phase 4: Widgets Part 2 (Steps 4.3-4.5)
|
||||||
|
|
||||||
|
**Status**: ⏸️ Pending
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Day 5 Target:
|
||||||
|
- [ ] Phase 5: Testing (Steps 5.1-5.4)
|
||||||
|
|
||||||
|
**Status**: ⏸️ Pending
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Known Issues to Watch For
|
||||||
|
|
||||||
|
1. **Race Conditions**: Monitor for duplicate requests
|
||||||
|
2. **Memory Leaks**: Watch for uncleaned intervals
|
||||||
|
3. **Performance**: Monitor API call frequency
|
||||||
|
4. **User Experience**: Ensure smooth refresh transitions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Success Criteria
|
||||||
|
|
||||||
|
### Must Have:
|
||||||
|
- ✅ No memory leaks
|
||||||
|
- ✅ 60%+ reduction in API calls
|
||||||
|
- ✅ All widgets refresh correctly
|
||||||
|
- ✅ No duplicate requests
|
||||||
|
|
||||||
|
### Nice to Have:
|
||||||
|
- ✅ Configurable refresh intervals
|
||||||
|
- ✅ Pause/resume functionality
|
||||||
|
- ✅ Refresh status monitoring
|
||||||
|
- ✅ Error recovery
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Rollback Plan
|
||||||
|
|
||||||
|
If issues arise:
|
||||||
|
|
||||||
|
1. **Keep old code**: Don't delete old implementations immediately
|
||||||
|
2. **Feature flag**: Use environment variable to toggle new/old system
|
||||||
|
3. **Gradual migration**: Migrate one widget at a time
|
||||||
|
4. **Monitor**: Watch for errors in production
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
|
||||||
|
- All new code should be backward compatible
|
||||||
|
- Test each phase before moving to next
|
||||||
|
- Document any deviations from plan
|
||||||
|
- Update this checklist as you progress
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last Updated: Implementation Checklist v1.0*
|
||||||
888
IMPLEMENTATION_PLAN_UNIFIED_REFRESH.md
Normal file
888
IMPLEMENTATION_PLAN_UNIFIED_REFRESH.md
Normal file
@ -0,0 +1,888 @@
|
|||||||
|
# Implementation Plan: Unified Refresh System
|
||||||
|
|
||||||
|
## 🎯 Goals
|
||||||
|
|
||||||
|
1. **Harmonize auto-refresh** across all widgets and notifications
|
||||||
|
2. **Reduce redundancy** and eliminate duplicate API calls
|
||||||
|
3. **Improve API efficiency** with request deduplication and caching
|
||||||
|
4. **Prevent memory leaks** with proper cleanup mechanisms
|
||||||
|
5. **Centralize refresh logic** for easier maintenance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Current State Analysis
|
||||||
|
|
||||||
|
### Current Refresh Intervals:
|
||||||
|
- **Notifications**: 60 seconds (polling)
|
||||||
|
- **Calendar**: 5 minutes (300000ms)
|
||||||
|
- **Parole (Chat)**: 30 seconds (30000ms)
|
||||||
|
- **Navbar Time**: Static (not refreshing - needs fix)
|
||||||
|
- **News**: Manual only
|
||||||
|
- **Email**: Manual only
|
||||||
|
- **Duties (Tasks)**: Manual only
|
||||||
|
|
||||||
|
### Current Problems:
|
||||||
|
1. ❌ No coordination between widgets
|
||||||
|
2. ❌ Duplicate API calls from multiple components
|
||||||
|
3. ❌ Memory leaks from uncleaned intervals
|
||||||
|
4. ❌ No request deduplication
|
||||||
|
5. ❌ Inconsistent refresh patterns
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Architecture: Unified Refresh System
|
||||||
|
|
||||||
|
### Phase 1: Core Infrastructure
|
||||||
|
|
||||||
|
#### 1.1 Create Unified Refresh Manager
|
||||||
|
|
||||||
|
**File**: `lib/services/refresh-manager.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* Unified Refresh Manager
|
||||||
|
* Centralizes all refresh logic, prevents duplicates, manages intervals
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type RefreshableResource =
|
||||||
|
| 'notifications'
|
||||||
|
| 'notifications-count'
|
||||||
|
| 'calendar'
|
||||||
|
| 'news'
|
||||||
|
| 'email'
|
||||||
|
| 'parole'
|
||||||
|
| 'duties';
|
||||||
|
|
||||||
|
export interface RefreshConfig {
|
||||||
|
resource: RefreshableResource;
|
||||||
|
interval: number; // milliseconds
|
||||||
|
enabled: boolean;
|
||||||
|
priority: 'high' | 'medium' | 'low';
|
||||||
|
onRefresh: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RefreshManager {
|
||||||
|
private intervals: Map<RefreshableResource, NodeJS.Timeout> = new Map();
|
||||||
|
private configs: Map<RefreshableResource, RefreshConfig> = new Map();
|
||||||
|
private pendingRequests: Map<string, Promise<any>> = new Map();
|
||||||
|
private lastRefresh: Map<RefreshableResource, number> = new Map();
|
||||||
|
private isActive = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a refreshable resource
|
||||||
|
*/
|
||||||
|
register(config: RefreshConfig): void {
|
||||||
|
this.configs.set(config.resource, config);
|
||||||
|
|
||||||
|
if (config.enabled && this.isActive) {
|
||||||
|
this.startRefresh(config.resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister a resource
|
||||||
|
*/
|
||||||
|
unregister(resource: RefreshableResource): void {
|
||||||
|
this.stopRefresh(resource);
|
||||||
|
this.configs.delete(resource);
|
||||||
|
this.lastRefresh.delete(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start all refresh intervals
|
||||||
|
*/
|
||||||
|
start(): void {
|
||||||
|
if (this.isActive) return;
|
||||||
|
|
||||||
|
this.isActive = true;
|
||||||
|
|
||||||
|
// Start all enabled resources
|
||||||
|
this.configs.forEach((config, resource) => {
|
||||||
|
if (config.enabled) {
|
||||||
|
this.startRefresh(resource);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop all refresh intervals
|
||||||
|
*/
|
||||||
|
stop(): void {
|
||||||
|
this.isActive = false;
|
||||||
|
|
||||||
|
// Clear all intervals
|
||||||
|
this.intervals.forEach((interval) => {
|
||||||
|
clearInterval(interval);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.intervals.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start refresh for a specific resource
|
||||||
|
*/
|
||||||
|
private startRefresh(resource: RefreshableResource): void {
|
||||||
|
// Stop existing interval if any
|
||||||
|
this.stopRefresh(resource);
|
||||||
|
|
||||||
|
const config = this.configs.get(resource);
|
||||||
|
if (!config || !config.enabled) return;
|
||||||
|
|
||||||
|
// Initial refresh
|
||||||
|
this.executeRefresh(resource);
|
||||||
|
|
||||||
|
// Set up interval
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
this.executeRefresh(resource);
|
||||||
|
}, config.interval);
|
||||||
|
|
||||||
|
this.intervals.set(resource, interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop refresh for a specific resource
|
||||||
|
*/
|
||||||
|
private stopRefresh(resource: RefreshableResource): void {
|
||||||
|
const interval = this.intervals.get(resource);
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
this.intervals.delete(resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute refresh with deduplication
|
||||||
|
*/
|
||||||
|
private async executeRefresh(resource: RefreshableResource): Promise<void> {
|
||||||
|
const config = this.configs.get(resource);
|
||||||
|
if (!config) return;
|
||||||
|
|
||||||
|
const requestKey = `${resource}-${Date.now()}`;
|
||||||
|
const now = Date.now();
|
||||||
|
const lastRefreshTime = this.lastRefresh.get(resource) || 0;
|
||||||
|
|
||||||
|
// Prevent too frequent refreshes (minimum 1 second between same resource)
|
||||||
|
if (now - lastRefreshTime < 1000) {
|
||||||
|
console.log(`[RefreshManager] Skipping ${resource} - too soon`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's already a pending request for this resource
|
||||||
|
const pendingKey = `${resource}-pending`;
|
||||||
|
if (this.pendingRequests.has(pendingKey)) {
|
||||||
|
console.log(`[RefreshManager] Deduplicating ${resource} request`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and track the request
|
||||||
|
const refreshPromise = config.onRefresh()
|
||||||
|
.then(() => {
|
||||||
|
this.lastRefresh.set(resource, Date.now());
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(`[RefreshManager] Error refreshing ${resource}:`, error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.pendingRequests.delete(pendingKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pendingRequests.set(pendingKey, refreshPromise);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await refreshPromise;
|
||||||
|
} catch (error) {
|
||||||
|
// Error already logged above
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually trigger refresh for a resource
|
||||||
|
*/
|
||||||
|
async refresh(resource: RefreshableResource, force = false): Promise<void> {
|
||||||
|
const config = this.configs.get(resource);
|
||||||
|
if (!config) {
|
||||||
|
throw new Error(`Resource ${resource} not registered`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
// Force refresh: clear last refresh time
|
||||||
|
this.lastRefresh.delete(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.executeRefresh(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get refresh status
|
||||||
|
*/
|
||||||
|
getStatus(): {
|
||||||
|
active: boolean;
|
||||||
|
resources: Array<{
|
||||||
|
resource: RefreshableResource;
|
||||||
|
enabled: boolean;
|
||||||
|
lastRefresh: number | null;
|
||||||
|
interval: number;
|
||||||
|
}>;
|
||||||
|
} {
|
||||||
|
const resources = Array.from(this.configs.entries()).map(([resource, config]) => ({
|
||||||
|
resource,
|
||||||
|
enabled: config.enabled,
|
||||||
|
lastRefresh: this.lastRefresh.get(resource) || null,
|
||||||
|
interval: config.interval,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
active: this.isActive,
|
||||||
|
resources,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton instance
|
||||||
|
export const refreshManager = new RefreshManager();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 1.2 Create Request Deduplication Utility
|
||||||
|
|
||||||
|
**File**: `lib/utils/request-deduplication.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* Request Deduplication Utility
|
||||||
|
* Prevents duplicate API calls for the same resource
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface PendingRequest<T> {
|
||||||
|
promise: Promise<T>;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RequestDeduplicator {
|
||||||
|
private pendingRequests = new Map<string, PendingRequest<any>>();
|
||||||
|
private readonly DEFAULT_TTL = 5000; // 5 seconds
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a request with deduplication
|
||||||
|
*/
|
||||||
|
async execute<T>(
|
||||||
|
key: string,
|
||||||
|
requestFn: () => Promise<T>,
|
||||||
|
ttl: number = this.DEFAULT_TTL
|
||||||
|
): Promise<T> {
|
||||||
|
// Check if there's a pending request
|
||||||
|
const pending = this.pendingRequests.get(key);
|
||||||
|
|
||||||
|
if (pending) {
|
||||||
|
const age = Date.now() - pending.timestamp;
|
||||||
|
|
||||||
|
// If request is still fresh, reuse it
|
||||||
|
if (age < ttl) {
|
||||||
|
console.log(`[RequestDeduplicator] Reusing pending request: ${key}`);
|
||||||
|
return pending.promise;
|
||||||
|
} else {
|
||||||
|
// Request is stale, remove it
|
||||||
|
this.pendingRequests.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new request
|
||||||
|
const promise = requestFn()
|
||||||
|
.finally(() => {
|
||||||
|
// Clean up after request completes
|
||||||
|
this.pendingRequests.delete(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pendingRequests.set(key, {
|
||||||
|
promise,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel a pending request
|
||||||
|
*/
|
||||||
|
cancel(key: string): void {
|
||||||
|
this.pendingRequests.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all pending requests
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
this.pendingRequests.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pending requests count
|
||||||
|
*/
|
||||||
|
getPendingCount(): number {
|
||||||
|
return this.pendingRequests.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requestDeduplicator = new RequestDeduplicator();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 1.3 Create Unified Refresh Hook
|
||||||
|
|
||||||
|
**File**: `hooks/use-unified-refresh.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* Unified Refresh Hook
|
||||||
|
* Provides consistent refresh functionality for all widgets
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useCallback, useRef } from 'react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
import { refreshManager, RefreshableResource } from '@/lib/services/refresh-manager';
|
||||||
|
|
||||||
|
interface UseUnifiedRefreshOptions {
|
||||||
|
resource: RefreshableResource;
|
||||||
|
interval: number;
|
||||||
|
enabled?: boolean;
|
||||||
|
onRefresh: () => Promise<void>;
|
||||||
|
priority?: 'high' | 'medium' | 'low';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUnifiedRefresh({
|
||||||
|
resource,
|
||||||
|
interval,
|
||||||
|
enabled = true,
|
||||||
|
onRefresh,
|
||||||
|
priority = 'medium',
|
||||||
|
}: UseUnifiedRefreshOptions) {
|
||||||
|
const { status } = useSession();
|
||||||
|
const onRefreshRef = useRef(onRefresh);
|
||||||
|
const isMountedRef = useRef(true);
|
||||||
|
|
||||||
|
// Update callback ref when it changes
|
||||||
|
useEffect(() => {
|
||||||
|
onRefreshRef.current = onRefresh;
|
||||||
|
}, [onRefresh]);
|
||||||
|
|
||||||
|
// Register/unregister with refresh manager
|
||||||
|
useEffect(() => {
|
||||||
|
if (status !== 'authenticated' || !enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isMountedRef.current = true;
|
||||||
|
|
||||||
|
// Register with refresh manager
|
||||||
|
refreshManager.register({
|
||||||
|
resource,
|
||||||
|
interval,
|
||||||
|
enabled: true,
|
||||||
|
priority,
|
||||||
|
onRefresh: async () => {
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
await onRefreshRef.current();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start refresh manager if not already started
|
||||||
|
refreshManager.start();
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
isMountedRef.current = false;
|
||||||
|
refreshManager.unregister(resource);
|
||||||
|
};
|
||||||
|
}, [resource, interval, enabled, priority, status]);
|
||||||
|
|
||||||
|
// Manual refresh function
|
||||||
|
const refresh = useCallback(
|
||||||
|
async (force = false) => {
|
||||||
|
if (status !== 'authenticated') return;
|
||||||
|
await refreshManager.refresh(resource, force);
|
||||||
|
},
|
||||||
|
[resource, status]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
refresh,
|
||||||
|
isActive: refreshManager.getStatus().active,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: Harmonized Refresh Intervals
|
||||||
|
|
||||||
|
#### 2.1 Define Standard Intervals
|
||||||
|
|
||||||
|
**File**: `lib/constants/refresh-intervals.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* Standard Refresh Intervals
|
||||||
|
* All intervals in milliseconds
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const REFRESH_INTERVALS = {
|
||||||
|
// High priority - real-time updates
|
||||||
|
NOTIFICATIONS: 30000, // 30 seconds (was 60s)
|
||||||
|
NOTIFICATIONS_COUNT: 30000, // 30 seconds (same as notifications)
|
||||||
|
PAROLE: 30000, // 30 seconds (unchanged)
|
||||||
|
NAVBAR_TIME: 1000, // 1 second (navigation bar time - real-time)
|
||||||
|
|
||||||
|
// Medium priority - frequent but not real-time
|
||||||
|
EMAIL: 60000, // 1 minute (was manual only)
|
||||||
|
DUTIES: 120000, // 2 minutes (was manual only)
|
||||||
|
|
||||||
|
// Low priority - less frequent updates
|
||||||
|
CALENDAR: 300000, // 5 minutes (unchanged)
|
||||||
|
NEWS: 600000, // 10 minutes (was manual only)
|
||||||
|
|
||||||
|
// Minimum interval between refreshes (prevents spam)
|
||||||
|
MIN_INTERVAL: 1000, // 1 second
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get refresh interval for a resource
|
||||||
|
*/
|
||||||
|
export function getRefreshInterval(resource: string): number {
|
||||||
|
switch (resource) {
|
||||||
|
case 'notifications':
|
||||||
|
return REFRESH_INTERVALS.NOTIFICATIONS;
|
||||||
|
case 'notifications-count':
|
||||||
|
return REFRESH_INTERVALS.NOTIFICATIONS_COUNT;
|
||||||
|
case 'parole':
|
||||||
|
return REFRESH_INTERVALS.PAROLE;
|
||||||
|
case 'email':
|
||||||
|
return REFRESH_INTERVALS.EMAIL;
|
||||||
|
case 'duties':
|
||||||
|
return REFRESH_INTERVALS.DUTIES;
|
||||||
|
case 'calendar':
|
||||||
|
return REFRESH_INTERVALS.CALENDAR;
|
||||||
|
case 'news':
|
||||||
|
return REFRESH_INTERVALS.NEWS;
|
||||||
|
default:
|
||||||
|
return 60000; // Default: 1 minute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Refactor Widgets
|
||||||
|
|
||||||
|
#### 3.1 Refactor Notification Hook
|
||||||
|
|
||||||
|
**File**: `hooks/use-notifications.ts` (Refactored)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
import { Notification, NotificationCount } from '@/lib/types/notification';
|
||||||
|
import { useUnifiedRefresh } from './use-unified-refresh';
|
||||||
|
import { REFRESH_INTERVALS } from '@/lib/constants/refresh-intervals';
|
||||||
|
import { requestDeduplicator } from '@/lib/utils/request-deduplication';
|
||||||
|
|
||||||
|
const defaultNotificationCount: NotificationCount = {
|
||||||
|
total: 0,
|
||||||
|
unread: 0,
|
||||||
|
sources: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
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 isMountedRef = useRef(true);
|
||||||
|
|
||||||
|
// Fetch notification count
|
||||||
|
const fetchNotificationCount = useCallback(async () => {
|
||||||
|
if (!session?.user || !isMountedRef.current) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const data = await requestDeduplicator.execute(
|
||||||
|
`notifications-count-${session.user.id}`,
|
||||||
|
async () => {
|
||||||
|
const response = await fetch('/api/notifications/count', {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch notification count');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
setNotificationCount(data);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching notification count:', err);
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
setError('Failed to fetch notification count');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [session?.user]);
|
||||||
|
|
||||||
|
// Fetch notifications
|
||||||
|
const fetchNotifications = useCallback(async (page = 1, limit = 20) => {
|
||||||
|
if (!session?.user || !isMountedRef.current) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await requestDeduplicator.execute(
|
||||||
|
`notifications-${session.user.id}-${page}-${limit}`,
|
||||||
|
async () => {
|
||||||
|
const response = await fetch(`/api/notifications?page=${page}&limit=${limit}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch notifications');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
setNotifications(data.notifications);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching notifications:', err);
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
setError('Failed to fetch notifications');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [session?.user]);
|
||||||
|
|
||||||
|
// Use unified refresh for notification count
|
||||||
|
useUnifiedRefresh({
|
||||||
|
resource: 'notifications-count',
|
||||||
|
interval: REFRESH_INTERVALS.NOTIFICATIONS_COUNT,
|
||||||
|
enabled: status === 'authenticated',
|
||||||
|
onRefresh: fetchNotificationCount,
|
||||||
|
priority: 'high',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial fetch
|
||||||
|
useEffect(() => {
|
||||||
|
isMountedRef.current = true;
|
||||||
|
|
||||||
|
if (status === 'authenticated' && session?.user) {
|
||||||
|
fetchNotificationCount();
|
||||||
|
fetchNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMountedRef.current = false;
|
||||||
|
};
|
||||||
|
}, [status, session?.user, fetchNotificationCount, fetchNotifications]);
|
||||||
|
|
||||||
|
// Mark as read
|
||||||
|
const markAsRead = useCallback(async (notificationId: string) => {
|
||||||
|
if (!session?.user) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/notifications/${notificationId}/read`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) return false;
|
||||||
|
|
||||||
|
setNotifications(prev =>
|
||||||
|
prev.map(n => n.id === notificationId ? { ...n, isRead: true } : n)
|
||||||
|
);
|
||||||
|
|
||||||
|
await fetchNotificationCount();
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error marking notification as read:', err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [session?.user, fetchNotificationCount]);
|
||||||
|
|
||||||
|
// Mark all as read
|
||||||
|
const markAllAsRead = useCallback(async () => {
|
||||||
|
if (!session?.user) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/notifications/read-all', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) return false;
|
||||||
|
|
||||||
|
setNotifications(prev => prev.map(n => ({ ...n, isRead: true })));
|
||||||
|
await fetchNotificationCount();
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error marking all notifications as read:', err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [session?.user, fetchNotificationCount]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
notifications,
|
||||||
|
notificationCount,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
fetchNotifications,
|
||||||
|
fetchNotificationCount,
|
||||||
|
markAsRead,
|
||||||
|
markAllAsRead,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3.2 Refactor Widget Components
|
||||||
|
|
||||||
|
**Example: Calendar Widget**
|
||||||
|
|
||||||
|
**File**: `components/calendar.tsx` (Refactored)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { RefreshCw, Calendar as CalendarIcon } from "lucide-react";
|
||||||
|
import { useUnifiedRefresh } from '@/hooks/use-unified-refresh';
|
||||||
|
import { REFRESH_INTERVALS } from '@/lib/constants/refresh-intervals';
|
||||||
|
import { requestDeduplicator } from '@/lib/utils/request-deduplication';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
|
interface Event {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
allDay: boolean;
|
||||||
|
calendar: string;
|
||||||
|
calendarColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Calendar() {
|
||||||
|
const { status } = useSession();
|
||||||
|
const [events, setEvents] = useState<Event[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchEvents = async () => {
|
||||||
|
if (status !== 'authenticated') return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const calendarsData = await requestDeduplicator.execute(
|
||||||
|
'calendar-events',
|
||||||
|
async () => {
|
||||||
|
const response = await fetch('/api/calendars?refresh=true');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch events');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
now.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const allEvents = calendarsData.flatMap((calendar: any) =>
|
||||||
|
(calendar.events || []).map((event: any) => ({
|
||||||
|
id: event.id,
|
||||||
|
title: event.title,
|
||||||
|
start: event.start,
|
||||||
|
end: event.end,
|
||||||
|
allDay: event.isAllDay,
|
||||||
|
calendar: calendar.name,
|
||||||
|
calendarColor: calendar.color
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const upcomingEvents = allEvents
|
||||||
|
.filter((event: any) => new Date(event.start) >= now)
|
||||||
|
.sort((a: any, b: any) => new Date(a.start).getTime() - new Date(b.start).getTime())
|
||||||
|
.slice(0, 7);
|
||||||
|
|
||||||
|
setEvents(upcomingEvents);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching events:', err);
|
||||||
|
setError('Failed to load events');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use unified refresh
|
||||||
|
const { refresh } = useUnifiedRefresh({
|
||||||
|
resource: 'calendar',
|
||||||
|
interval: REFRESH_INTERVALS.CALENDAR,
|
||||||
|
enabled: status === 'authenticated',
|
||||||
|
onRefresh: fetchEvents,
|
||||||
|
priority: 'low',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial fetch
|
||||||
|
useEffect(() => {
|
||||||
|
if (status === 'authenticated') {
|
||||||
|
fetchEvents();
|
||||||
|
}
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
|
// ... rest of component (formatDate, formatTime, render)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="...">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Agenda</CardTitle>
|
||||||
|
<Button onClick={() => refresh(true)}>
|
||||||
|
<RefreshCw className="..." />
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
{/* ... */}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Implementation Steps
|
||||||
|
|
||||||
|
#### Step 1: Create Core Infrastructure (Day 1)
|
||||||
|
|
||||||
|
1. ✅ Create `lib/services/refresh-manager.ts`
|
||||||
|
2. ✅ Create `lib/utils/request-deduplication.ts`
|
||||||
|
3. ✅ Create `lib/constants/refresh-intervals.ts`
|
||||||
|
4. ✅ Create `hooks/use-unified-refresh.ts`
|
||||||
|
|
||||||
|
**Testing**: Unit tests for each module
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 2: Fix Memory Leaks (Day 1-2)
|
||||||
|
|
||||||
|
1. ✅ Fix `useNotifications` hook cleanup
|
||||||
|
2. ✅ Fix notification badge double fetching
|
||||||
|
3. ✅ Fix widget interval cleanup
|
||||||
|
4. ✅ Fix Redis KEYS → SCAN
|
||||||
|
|
||||||
|
**Testing**: Memory leak detection in DevTools
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 3: Refactor Notifications (Day 2)
|
||||||
|
|
||||||
|
1. ✅ Refactor `hooks/use-notifications.ts`
|
||||||
|
2. ✅ Update `components/notification-badge.tsx`
|
||||||
|
3. ✅ Remove duplicate fetch logic
|
||||||
|
|
||||||
|
**Testing**: Verify no duplicate API calls
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 4: Refactor Widgets (Day 3-4)
|
||||||
|
|
||||||
|
1. ✅ Refactor `components/calendar.tsx`
|
||||||
|
2. ✅ Refactor `components/parole.tsx`
|
||||||
|
3. ✅ Refactor `components/news.tsx`
|
||||||
|
4. ✅ Refactor `components/email.tsx`
|
||||||
|
5. ✅ Refactor `components/flow.tsx` (Duties)
|
||||||
|
6. ✅ Refactor `components/main-nav.tsx` (Time display)
|
||||||
|
|
||||||
|
**Testing**: Verify all widgets refresh correctly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Step 5: Testing & Optimization (Day 5)
|
||||||
|
|
||||||
|
1. ✅ Performance testing
|
||||||
|
2. ✅ Memory leak verification
|
||||||
|
3. ✅ API call reduction verification
|
||||||
|
4. ✅ User experience testing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Expected Improvements
|
||||||
|
|
||||||
|
### Before:
|
||||||
|
- **API Calls**: ~120-150 calls/minute (with duplicates)
|
||||||
|
- **Memory Leaks**: Yes (intervals not cleaned up)
|
||||||
|
- **Refresh Coordination**: None
|
||||||
|
- **Request Deduplication**: None
|
||||||
|
|
||||||
|
### After:
|
||||||
|
- **API Calls**: ~40-50 calls/minute (60-70% reduction)
|
||||||
|
- **Memory Leaks**: None (proper cleanup)
|
||||||
|
- **Refresh Coordination**: Centralized
|
||||||
|
- **Request Deduplication**: Full coverage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Success Metrics
|
||||||
|
|
||||||
|
1. **API Call Reduction**: 60%+ reduction in duplicate calls
|
||||||
|
2. **Memory Usage**: No memory leaks detected
|
||||||
|
3. **Performance**: Faster page loads, smoother UX
|
||||||
|
4. **Maintainability**: Single source of truth for refresh logic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start Implementation
|
||||||
|
|
||||||
|
### Priority Order:
|
||||||
|
|
||||||
|
1. **Critical** (Do First):
|
||||||
|
- Fix memory leaks
|
||||||
|
- Create refresh manager
|
||||||
|
- Create request deduplication
|
||||||
|
|
||||||
|
2. **High** (Do Second):
|
||||||
|
- Refactor notifications
|
||||||
|
- Refactor high-frequency widgets (parole, notifications)
|
||||||
|
|
||||||
|
3. **Medium** (Do Third):
|
||||||
|
- Refactor medium-frequency widgets (email, duties)
|
||||||
|
|
||||||
|
4. **Low** (Do Last):
|
||||||
|
- Refactor low-frequency widgets (calendar, news)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
|
||||||
|
- All intervals are configurable via constants
|
||||||
|
- Refresh manager can be paused/resumed globally
|
||||||
|
- Request deduplication prevents duplicate calls within 5 seconds
|
||||||
|
- All cleanup is handled automatically
|
||||||
|
- Compatible with existing code (gradual migration)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Implementation Plan v1.0*
|
||||||
129
NAVBAR_TIME_INTEGRATION.md
Normal file
129
NAVBAR_TIME_INTEGRATION.md
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# Navigation Bar Time Integration
|
||||||
|
|
||||||
|
## 🎯 Overview
|
||||||
|
|
||||||
|
The navigation bar (`components/main-nav.tsx`) currently displays a static time that doesn't refresh. This document outlines how to integrate it into the unified refresh system.
|
||||||
|
|
||||||
|
## 🔍 Current Issue
|
||||||
|
|
||||||
|
**File**: `components/main-nav.tsx` (lines 228-231)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Current code - STATIC (doesn't refresh)
|
||||||
|
const now = new Date();
|
||||||
|
const formattedDate = format(now, "d MMMM yyyy", { locale: fr });
|
||||||
|
const formattedTime = format(now, "HH:mm");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem**: Time is calculated once when component renders and never updates.
|
||||||
|
|
||||||
|
## ✅ Solution
|
||||||
|
|
||||||
|
### Step 1: Create Time Component
|
||||||
|
|
||||||
|
**File**: `components/main-nav-time.tsx` (✅ Already created)
|
||||||
|
|
||||||
|
This component:
|
||||||
|
- Uses `useState` to track current time
|
||||||
|
- Uses `useUnifiedRefresh` hook for 1-second updates
|
||||||
|
- Properly cleans up on unmount
|
||||||
|
- No API calls needed (client-side only)
|
||||||
|
|
||||||
|
### Step 2: Update MainNav Component
|
||||||
|
|
||||||
|
**File**: `components/main-nav.tsx`
|
||||||
|
|
||||||
|
**Changes needed**:
|
||||||
|
|
||||||
|
1. **Import the new component**:
|
||||||
|
```typescript
|
||||||
|
import { MainNavTime } from './main-nav-time';
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Remove static time code** (lines 228-231):
|
||||||
|
```typescript
|
||||||
|
// DELETE THESE LINES:
|
||||||
|
// Format current date and time
|
||||||
|
const now = new Date();
|
||||||
|
const formattedDate = format(now, "d MMMM yyyy", { locale: fr });
|
||||||
|
const formattedTime = format(now, "HH:mm");
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Replace time display** (lines 294-298):
|
||||||
|
```typescript
|
||||||
|
// BEFORE:
|
||||||
|
{/* Center - Date and Time */}
|
||||||
|
<div className="hidden md:flex flex-col items-center">
|
||||||
|
<div className="text-white/80 text-xs">{formattedDate}</div>
|
||||||
|
<div className="text-white text-sm font-medium">{formattedTime}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// AFTER:
|
||||||
|
{/* Center - Date and Time */}
|
||||||
|
<MainNavTime />
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Verify Integration
|
||||||
|
|
||||||
|
After changes:
|
||||||
|
- ✅ Time updates every second
|
||||||
|
- ✅ Uses unified refresh system
|
||||||
|
- ✅ Proper cleanup on unmount
|
||||||
|
- ✅ No memory leaks
|
||||||
|
- ✅ Consistent with other widgets
|
||||||
|
|
||||||
|
## 📊 Benefits
|
||||||
|
|
||||||
|
1. **Real-time clock**: Time updates every second
|
||||||
|
2. **Unified system**: Uses same refresh manager as widgets
|
||||||
|
3. **Memory safe**: Proper cleanup prevents leaks
|
||||||
|
4. **Consistent**: Same pattern as other components
|
||||||
|
5. **Maintainable**: Centralized refresh logic
|
||||||
|
|
||||||
|
## 🔧 Technical Details
|
||||||
|
|
||||||
|
### Refresh Configuration
|
||||||
|
|
||||||
|
- **Resource**: `navbar-time`
|
||||||
|
- **Interval**: 1000ms (1 second)
|
||||||
|
- **Priority**: `high` (real-time display)
|
||||||
|
- **API Calls**: None (client-side only)
|
||||||
|
- **Cleanup**: Automatic via `useUnifiedRefresh`
|
||||||
|
|
||||||
|
### Integration with Refresh Manager
|
||||||
|
|
||||||
|
The time component registers with the refresh manager:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
useUnifiedRefresh({
|
||||||
|
resource: 'navbar-time',
|
||||||
|
interval: REFRESH_INTERVALS.NAVBAR_TIME, // 1000ms
|
||||||
|
enabled: true, // Always enabled
|
||||||
|
onRefresh: async () => {
|
||||||
|
setCurrentTime(new Date());
|
||||||
|
},
|
||||||
|
priority: 'high',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Implementation Checklist
|
||||||
|
|
||||||
|
- [x] Create `components/main-nav-time.tsx`
|
||||||
|
- [x] Add `NAVBAR_TIME` to refresh intervals
|
||||||
|
- [x] Add `navbar-time` to refreshable resources
|
||||||
|
- [ ] Update `components/main-nav.tsx` to use new component
|
||||||
|
- [ ] Test time updates correctly
|
||||||
|
- [ ] Verify cleanup on unmount
|
||||||
|
- [ ] Test with multiple tabs
|
||||||
|
|
||||||
|
## 🎯 Expected Result
|
||||||
|
|
||||||
|
After implementation:
|
||||||
|
- Time updates smoothly every second
|
||||||
|
- No performance impact
|
||||||
|
- No memory leaks
|
||||||
|
- Consistent with unified refresh system
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last Updated: Navbar Time Integration Guide*
|
||||||
548
NOTIFICATION_AND_WIDGET_ANALYSIS.md
Normal file
548
NOTIFICATION_AND_WIDGET_ANALYSIS.md
Normal file
@ -0,0 +1,548 @@
|
|||||||
|
# Notification and Widget Update System - Complete File & Route Analysis
|
||||||
|
|
||||||
|
## 📋 Table of Contents
|
||||||
|
1. [Notification System](#notification-system)
|
||||||
|
2. [Widget Update System](#widget-update-system)
|
||||||
|
3. [API Routes](#api-routes)
|
||||||
|
4. [Components](#components)
|
||||||
|
5. [Services & Libraries](#services--libraries)
|
||||||
|
6. [Hooks](#hooks)
|
||||||
|
7. [Types](#types)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔔 Notification System
|
||||||
|
|
||||||
|
### API Routes
|
||||||
|
|
||||||
|
#### 1. **GET `/api/notifications`**
|
||||||
|
- **File**: `app/api/notifications/route.ts`
|
||||||
|
- **Purpose**: Fetch paginated notifications for authenticated user
|
||||||
|
- **Query Parameters**:
|
||||||
|
- `page` (default: 1)
|
||||||
|
- `limit` (default: 20, max: 100)
|
||||||
|
- **Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"notifications": Notification[],
|
||||||
|
"page": number,
|
||||||
|
"limit": number,
|
||||||
|
"total": number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Cache**: 30 seconds client-side cache
|
||||||
|
- **Authentication**: Required (session-based)
|
||||||
|
|
||||||
|
#### 2. **GET `/api/notifications/count`**
|
||||||
|
- **File**: `app/api/notifications/count/route.ts`
|
||||||
|
- **Purpose**: Get notification count (total and unread) for authenticated user
|
||||||
|
- **Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"total": number,
|
||||||
|
"unread": number,
|
||||||
|
"sources": {
|
||||||
|
[source]: {
|
||||||
|
"total": number,
|
||||||
|
"unread": number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Cache**: 10 seconds client-side cache
|
||||||
|
- **Authentication**: Required
|
||||||
|
|
||||||
|
#### 3. **POST `/api/notifications/[id]/read`**
|
||||||
|
- **File**: `app/api/notifications/[id]/read/route.ts`
|
||||||
|
- **Purpose**: Mark a specific notification as read
|
||||||
|
- **Parameters**:
|
||||||
|
- `id` (path parameter): Notification ID (format: `source-sourceId`)
|
||||||
|
- **Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": boolean
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Authentication**: Required
|
||||||
|
|
||||||
|
#### 4. **POST `/api/notifications/read-all`**
|
||||||
|
- **File**: `app/api/notifications/read-all/route.ts`
|
||||||
|
- **Purpose**: Mark all notifications as read for authenticated user
|
||||||
|
- **Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": boolean
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Authentication**: Required
|
||||||
|
|
||||||
|
#### 5. **GET `/api/debug/notifications`**
|
||||||
|
- **File**: `app/api/debug/notifications/route.ts`
|
||||||
|
- **Purpose**: Debug endpoint to test notification system
|
||||||
|
- **Response**: Detailed debug information including:
|
||||||
|
- Environment variables status
|
||||||
|
- User information
|
||||||
|
- Notification service test results
|
||||||
|
- Performance metrics
|
||||||
|
- **Authentication**: Required
|
||||||
|
|
||||||
|
### Services
|
||||||
|
|
||||||
|
#### 1. **NotificationService** (Singleton)
|
||||||
|
- **File**: `lib/services/notifications/notification-service.ts`
|
||||||
|
- **Purpose**: Core notification aggregation service
|
||||||
|
- **Features**:
|
||||||
|
- Multi-source notification aggregation (adapter pattern)
|
||||||
|
- Redis caching (30s for counts, 5min for lists)
|
||||||
|
- Background refresh scheduling
|
||||||
|
- Cache invalidation on read operations
|
||||||
|
- Lock mechanism to prevent concurrent refreshes
|
||||||
|
- **Methods**:
|
||||||
|
- `getInstance()`: Get singleton instance
|
||||||
|
- `getNotifications(userId, page, limit)`: Fetch notifications
|
||||||
|
- `getNotificationCount(userId)`: Get notification counts
|
||||||
|
- `markAsRead(userId, notificationId)`: Mark notification as read
|
||||||
|
- `markAllAsRead(userId)`: Mark all as read
|
||||||
|
- `invalidateCache(userId)`: Invalidate user caches
|
||||||
|
- `scheduleBackgroundRefresh(userId)`: Schedule background refresh
|
||||||
|
|
||||||
|
#### 2. **NotificationAdapter Interface**
|
||||||
|
- **File**: `lib/services/notifications/notification-adapter.interface.ts`
|
||||||
|
- **Purpose**: Interface for notification source adapters
|
||||||
|
- **Methods**:
|
||||||
|
- `getNotifications(userId, page?, limit?)`: Fetch notifications
|
||||||
|
- `getNotificationCount(userId)`: Get counts
|
||||||
|
- `markAsRead(userId, notificationId)`: Mark as read
|
||||||
|
- `markAllAsRead(userId)`: Mark all as read
|
||||||
|
- `isConfigured()`: Check if adapter is configured
|
||||||
|
|
||||||
|
#### 3. **LeantimeAdapter** (Implementation)
|
||||||
|
- **File**: `lib/services/notifications/leantime-adapter.ts`
|
||||||
|
- **Purpose**: Leantime notification source adapter
|
||||||
|
- **Features**:
|
||||||
|
- Fetches notifications from Leantime API via JSON-RPC
|
||||||
|
- Maps Leantime user IDs by email
|
||||||
|
- Transforms Leantime notifications to unified format
|
||||||
|
- Supports marking notifications as read
|
||||||
|
- **Configuration**:
|
||||||
|
- `LEANTIME_API_URL` environment variable
|
||||||
|
- `LEANTIME_TOKEN` environment variable
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
#### 1. **NotificationBadge**
|
||||||
|
- **File**: `components/notification-badge.tsx`
|
||||||
|
- **Purpose**: Notification bell icon with badge and dropdown
|
||||||
|
- **Features**:
|
||||||
|
- Displays unread count badge
|
||||||
|
- Dropdown menu with recent notifications
|
||||||
|
- Manual refresh button
|
||||||
|
- Mark as read functionality
|
||||||
|
- Mark all as read functionality
|
||||||
|
- Source badges (e.g., "Agilité" for Leantime)
|
||||||
|
- Links to source systems
|
||||||
|
- Error handling and retry
|
||||||
|
- **Used in**: `components/main-nav.tsx`
|
||||||
|
|
||||||
|
#### 2. **MainNav** (Notification Integration)
|
||||||
|
- **File**: `components/main-nav.tsx`
|
||||||
|
- **Purpose**: Main navigation bar with notification badge
|
||||||
|
- **Notification Features**:
|
||||||
|
- Includes `<NotificationBadge />` component
|
||||||
|
- Browser notification permission handling
|
||||||
|
- User status-based notification management
|
||||||
|
|
||||||
|
### Hooks
|
||||||
|
|
||||||
|
#### 1. **useNotifications**
|
||||||
|
- **File**: `hooks/use-notifications.ts`
|
||||||
|
- **Purpose**: React hook for notification management
|
||||||
|
- **Features**:
|
||||||
|
- Automatic polling (60 seconds interval)
|
||||||
|
- Rate limiting (5 seconds minimum between fetches)
|
||||||
|
- Debounced count fetching (300ms)
|
||||||
|
- Manual refresh support
|
||||||
|
- Mount/unmount lifecycle management
|
||||||
|
- Error handling
|
||||||
|
- **Returns**:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
notifications: Notification[],
|
||||||
|
notificationCount: NotificationCount,
|
||||||
|
loading: boolean,
|
||||||
|
error: string | null,
|
||||||
|
fetchNotifications: (page?, limit?) => Promise<void>,
|
||||||
|
fetchNotificationCount: () => Promise<void>,
|
||||||
|
markAsRead: (notificationId: string) => Promise<boolean>,
|
||||||
|
markAllAsRead: () => Promise<boolean>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Types
|
||||||
|
|
||||||
|
#### 1. **Notification Types**
|
||||||
|
- **File**: `lib/types/notification.ts`
|
||||||
|
- **Interfaces**:
|
||||||
|
- `Notification`: Main notification interface
|
||||||
|
- `id`: string (format: `source-sourceId`)
|
||||||
|
- `source`: 'leantime' | 'nextcloud' | 'gitea' | 'dolibarr' | 'moodle'
|
||||||
|
- `sourceId`: string
|
||||||
|
- `type`: string
|
||||||
|
- `title`: string
|
||||||
|
- `message`: string
|
||||||
|
- `link?`: string
|
||||||
|
- `isRead`: boolean
|
||||||
|
- `timestamp`: Date
|
||||||
|
- `priority`: 'low' | 'normal' | 'high'
|
||||||
|
- `user`: { id: string, name?: string }
|
||||||
|
- `metadata?`: Record<string, any>
|
||||||
|
- `NotificationCount`: Count interface
|
||||||
|
- `total`: number
|
||||||
|
- `unread`: number
|
||||||
|
- `sources`: Record<string, { total: number, unread: number }>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Widget Update System
|
||||||
|
|
||||||
|
### Dashboard Widgets
|
||||||
|
|
||||||
|
The main dashboard (`app/page.tsx`) contains the following widgets:
|
||||||
|
|
||||||
|
1. **QuoteCard** - Daily quote widget
|
||||||
|
2. **Calendar** - Upcoming events widget
|
||||||
|
3. **News** - News articles widget
|
||||||
|
4. **Duties** - Tasks/Devoirs widget (Leantime)
|
||||||
|
5. **Email** - Email inbox widget
|
||||||
|
6. **Parole** - Chat messages widget (Rocket.Chat)
|
||||||
|
|
||||||
|
### Widget Components & Update Mechanisms
|
||||||
|
|
||||||
|
#### 1. **Calendar Widget**
|
||||||
|
- **Files**:
|
||||||
|
- `components/calendar.tsx` (Main dashboard widget)
|
||||||
|
- `components/calendar-widget.tsx` (Alternative implementation)
|
||||||
|
- `components/calendar/calendar-widget.tsx` (Calendar-specific widget)
|
||||||
|
- **Update Mechanism**:
|
||||||
|
- **Manual Refresh**: Refresh button in header
|
||||||
|
- **Auto Refresh**: Every 5 minutes (300000ms interval)
|
||||||
|
- **API Endpoint**: `/api/calendars?refresh=true`
|
||||||
|
- **Features**:
|
||||||
|
- Fetches calendars with events
|
||||||
|
- Filters upcoming events (today and future)
|
||||||
|
- Sorts by date (oldest first)
|
||||||
|
- Shows up to 7 events
|
||||||
|
- Displays calendar color coding
|
||||||
|
- **State Management**:
|
||||||
|
- `useState` for events, loading, error
|
||||||
|
- `useEffect` for initial fetch and interval setup
|
||||||
|
|
||||||
|
#### 2. **News Widget**
|
||||||
|
- **File**: `components/news.tsx`
|
||||||
|
- **Update Mechanism**:
|
||||||
|
- **Manual Refresh**: Refresh button in header
|
||||||
|
- **Initial Load**: On component mount when authenticated
|
||||||
|
- **API Endpoint**: `/api/news?limit=100` or `/api/news?refresh=true&limit=100`
|
||||||
|
- **Features**:
|
||||||
|
- Fetches up to 100 news articles
|
||||||
|
- Displays article count
|
||||||
|
- Click to open in new tab
|
||||||
|
- Scrollable list (max-height: 400px)
|
||||||
|
- **State Management**:
|
||||||
|
- `useState` for news, loading, error, refreshing
|
||||||
|
- `useEffect` for initial fetch on authentication
|
||||||
|
|
||||||
|
#### 3. **Duties Widget (Tasks)**
|
||||||
|
- **File**: `components/flow.tsx`
|
||||||
|
- **Update Mechanism**:
|
||||||
|
- **Manual Refresh**: Refresh button in header
|
||||||
|
- **Initial Load**: On component mount
|
||||||
|
- **API Endpoint**: `/api/leantime/tasks?refresh=true`
|
||||||
|
- **Features**:
|
||||||
|
- Fetches tasks from Leantime
|
||||||
|
- Filters out completed tasks (status 5)
|
||||||
|
- Sorts by due date (oldest first)
|
||||||
|
- Shows up to 7 tasks
|
||||||
|
- Displays task status badges
|
||||||
|
- Links to Leantime ticket view
|
||||||
|
- **State Management**:
|
||||||
|
- `useState` for tasks, loading, error, refreshing
|
||||||
|
- `useEffect` for initial fetch
|
||||||
|
|
||||||
|
#### 4. **Email Widget**
|
||||||
|
- **File**: `components/email.tsx`
|
||||||
|
- **Update Mechanism**:
|
||||||
|
- **Manual Refresh**: Refresh button in header
|
||||||
|
- **Initial Load**: On component mount
|
||||||
|
- **API Endpoint**: `/api/courrier?folder=INBOX&page=1&perPage=5` (+ `&refresh=true` for refresh)
|
||||||
|
- **Features**:
|
||||||
|
- Fetches 5 most recent emails from INBOX
|
||||||
|
- Sorts by date (most recent first)
|
||||||
|
- Shows read/unread status
|
||||||
|
- Displays sender, subject, date
|
||||||
|
- Link to full email view (`/courrier`)
|
||||||
|
- **State Management**:
|
||||||
|
- `useState` for emails, loading, error, mailUrl
|
||||||
|
- `useEffect` for initial fetch
|
||||||
|
|
||||||
|
#### 5. **Parole Widget (Chat Messages)**
|
||||||
|
- **File**: `components/parole.tsx`
|
||||||
|
- **Update Mechanism**:
|
||||||
|
- **Manual Refresh**: Refresh button in header
|
||||||
|
- **Auto Polling**: Every 30 seconds (30000ms interval)
|
||||||
|
- **Initial Load**: On authentication
|
||||||
|
- **API Endpoint**: `/api/rocket-chat/messages` (+ `?refresh=true` for refresh)
|
||||||
|
- **Features**:
|
||||||
|
- Fetches recent chat messages from Rocket.Chat
|
||||||
|
- Displays sender avatar, name, message
|
||||||
|
- Shows room/channel information
|
||||||
|
- Click to navigate to full chat (`/parole`)
|
||||||
|
- Authentication check with sign-in prompt
|
||||||
|
- **State Management**:
|
||||||
|
- `useState` for messages, loading, error, refreshing
|
||||||
|
- `useEffect` for initial fetch and polling setup
|
||||||
|
- Session status checking
|
||||||
|
|
||||||
|
#### 6. **QuoteCard Widget**
|
||||||
|
- **File**: `components/quote-card.tsx`
|
||||||
|
- **Update Mechanism**: (To be verified - likely static or daily update)
|
||||||
|
|
||||||
|
### Widget Update Patterns
|
||||||
|
|
||||||
|
#### Common Update Mechanisms:
|
||||||
|
|
||||||
|
1. **Manual Refresh**:
|
||||||
|
- All widgets have a refresh button in their header
|
||||||
|
- Triggers API call with `refresh=true` parameter
|
||||||
|
- Shows loading/spinning state during refresh
|
||||||
|
|
||||||
|
2. **Auto Refresh/Polling**:
|
||||||
|
- **Calendar**: 5 minutes interval
|
||||||
|
- **Parole**: 30 seconds interval
|
||||||
|
- Others: On component mount only
|
||||||
|
|
||||||
|
3. **Session-Based Loading**:
|
||||||
|
- Widgets check authentication status
|
||||||
|
- Only fetch data when `status === 'authenticated'`
|
||||||
|
- Show loading state during authentication check
|
||||||
|
|
||||||
|
4. **Error Handling**:
|
||||||
|
- All widgets display error messages
|
||||||
|
- Retry buttons available
|
||||||
|
- Graceful degradation (empty states)
|
||||||
|
|
||||||
|
5. **State Management**:
|
||||||
|
- All widgets use React `useState` hooks
|
||||||
|
- Loading states managed locally
|
||||||
|
- Error states managed locally
|
||||||
|
|
||||||
|
### Related API Routes for Widgets
|
||||||
|
|
||||||
|
#### Calendar
|
||||||
|
- **GET `/api/calendars`**: Fetch calendars with events
|
||||||
|
- **GET `/api/calendars/[id]/events`**: Fetch events for specific calendar
|
||||||
|
- **GET `/api/calendars/[id]`**: Get calendar details
|
||||||
|
|
||||||
|
#### News
|
||||||
|
- **GET `/api/news`**: Fetch news articles
|
||||||
|
- Query params: `limit`, `refresh`
|
||||||
|
|
||||||
|
#### Tasks (Leantime)
|
||||||
|
- **GET `/api/leantime/tasks`**: Fetch tasks
|
||||||
|
- Query params: `refresh`
|
||||||
|
|
||||||
|
#### Email (Courrier)
|
||||||
|
- **GET `/api/courrier`**: Fetch emails
|
||||||
|
- Query params: `folder`, `page`, `perPage`, `refresh`
|
||||||
|
- **POST `/api/courrier/refresh`**: Force refresh email cache
|
||||||
|
|
||||||
|
#### Chat (Rocket.Chat)
|
||||||
|
- **GET `/api/rocket-chat/messages`**: Fetch messages
|
||||||
|
- Query params: `refresh`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Complete File Structure
|
||||||
|
|
||||||
|
### Notification Files
|
||||||
|
|
||||||
|
```
|
||||||
|
app/api/notifications/
|
||||||
|
├── route.ts # GET /api/notifications
|
||||||
|
├── count/
|
||||||
|
│ └── route.ts # GET /api/notifications/count
|
||||||
|
├── read-all/
|
||||||
|
│ └── route.ts # POST /api/notifications/read-all
|
||||||
|
└── [id]/
|
||||||
|
└── read/
|
||||||
|
└── route.ts # POST /api/notifications/[id]/read
|
||||||
|
|
||||||
|
app/api/debug/
|
||||||
|
└── notifications/
|
||||||
|
└── route.ts # GET /api/debug/notifications
|
||||||
|
|
||||||
|
lib/services/notifications/
|
||||||
|
├── notification-service.ts # Core notification service
|
||||||
|
├── notification-adapter.interface.ts # Adapter interface
|
||||||
|
└── leantime-adapter.ts # Leantime adapter implementation
|
||||||
|
|
||||||
|
lib/types/
|
||||||
|
└── notification.ts # Notification type definitions
|
||||||
|
|
||||||
|
hooks/
|
||||||
|
└── use-notifications.ts # React hook for notifications
|
||||||
|
|
||||||
|
components/
|
||||||
|
├── notification-badge.tsx # Notification UI component
|
||||||
|
└── main-nav.tsx # Navigation with notification badge
|
||||||
|
```
|
||||||
|
|
||||||
|
### Widget Files
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
└── page.tsx # Main dashboard with widgets
|
||||||
|
|
||||||
|
components/
|
||||||
|
├── calendar.tsx # Calendar widget
|
||||||
|
├── calendar-widget.tsx # Alternative calendar widget
|
||||||
|
├── calendar/
|
||||||
|
│ └── calendar-widget.tsx # Calendar-specific widget
|
||||||
|
├── news.tsx # News widget
|
||||||
|
├── flow.tsx # Duties/Tasks widget
|
||||||
|
├── email.tsx # Email widget
|
||||||
|
├── parole.tsx # Chat messages widget
|
||||||
|
└── quote-card.tsx # Quote widget
|
||||||
|
|
||||||
|
app/api/
|
||||||
|
├── calendars/
|
||||||
|
│ ├── route.ts # GET /api/calendars
|
||||||
|
│ └── [id]/
|
||||||
|
│ └── events/
|
||||||
|
│ └── route.ts # GET /api/calendars/[id]/events
|
||||||
|
├── news/
|
||||||
|
│ └── route.ts # GET /api/news
|
||||||
|
├── leantime/
|
||||||
|
│ └── tasks/
|
||||||
|
│ └── route.ts # GET /api/leantime/tasks
|
||||||
|
├── courrier/
|
||||||
|
│ ├── route.ts # GET /api/courrier
|
||||||
|
│ └── refresh/
|
||||||
|
│ └── route.ts # POST /api/courrier/refresh
|
||||||
|
└── rocket-chat/
|
||||||
|
└── messages/
|
||||||
|
└── route.ts # GET /api/rocket-chat/messages
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Update Flow Diagrams
|
||||||
|
|
||||||
|
### Notification Update Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
User Action / Polling
|
||||||
|
↓
|
||||||
|
useNotifications Hook
|
||||||
|
↓
|
||||||
|
API Route (/api/notifications or /api/notifications/count)
|
||||||
|
↓
|
||||||
|
NotificationService.getInstance()
|
||||||
|
↓
|
||||||
|
Check Redis Cache
|
||||||
|
├─ Cache Hit → Return cached data
|
||||||
|
└─ Cache Miss → Fetch from Adapters
|
||||||
|
↓
|
||||||
|
LeantimeAdapter (and other adapters)
|
||||||
|
↓
|
||||||
|
Transform & Aggregate
|
||||||
|
↓
|
||||||
|
Store in Redis Cache
|
||||||
|
↓
|
||||||
|
Return to API
|
||||||
|
↓
|
||||||
|
Return to Hook
|
||||||
|
↓
|
||||||
|
Update Component State
|
||||||
|
```
|
||||||
|
|
||||||
|
### Widget Update Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Component Mount / User Click Refresh
|
||||||
|
↓
|
||||||
|
useEffect / onClick Handler
|
||||||
|
↓
|
||||||
|
fetch() API Call
|
||||||
|
├─ With refresh=true (manual)
|
||||||
|
└─ Without refresh (initial)
|
||||||
|
↓
|
||||||
|
API Route Handler
|
||||||
|
├─ Check Cache (if applicable)
|
||||||
|
├─ Fetch from External Service
|
||||||
|
└─ Return Data
|
||||||
|
↓
|
||||||
|
Update Component State
|
||||||
|
├─ setLoading(false)
|
||||||
|
├─ setData(response)
|
||||||
|
└─ setError(null)
|
||||||
|
↓
|
||||||
|
Re-render Component
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Key Features Summary
|
||||||
|
|
||||||
|
### Notification System
|
||||||
|
- ✅ Multi-source aggregation (adapter pattern)
|
||||||
|
- ✅ Redis caching with TTL
|
||||||
|
- ✅ Background refresh scheduling
|
||||||
|
- ✅ Polling mechanism (60s interval)
|
||||||
|
- ✅ Rate limiting (5s minimum)
|
||||||
|
- ✅ Mark as read / Mark all as read
|
||||||
|
- ✅ Cache invalidation on updates
|
||||||
|
- ✅ Error handling and retry
|
||||||
|
- ✅ Source badges and links
|
||||||
|
|
||||||
|
### Widget System
|
||||||
|
- ✅ Manual refresh buttons
|
||||||
|
- ✅ Auto-refresh/polling (widget-specific intervals)
|
||||||
|
- ✅ Session-based loading
|
||||||
|
- ✅ Error handling
|
||||||
|
- ✅ Loading states
|
||||||
|
- ✅ Empty states
|
||||||
|
- ✅ Responsive design
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
|
||||||
|
1. **Notification Sources**: Currently only Leantime adapter is implemented. Other adapters (Nextcloud, Gitea, Dolibarr, Moodle) are commented out in the service.
|
||||||
|
|
||||||
|
2. **Cache Strategy**:
|
||||||
|
- Notification counts: 30 seconds TTL
|
||||||
|
- Notification lists: 5 minutes TTL
|
||||||
|
- Widget data: Varies by widget (some use API-level caching)
|
||||||
|
|
||||||
|
3. **Polling Intervals**:
|
||||||
|
- Notifications: 60 seconds
|
||||||
|
- Calendar widget: 5 minutes
|
||||||
|
- Parole widget: 30 seconds
|
||||||
|
- Other widgets: On mount only
|
||||||
|
|
||||||
|
4. **Authentication**: All notification and widget APIs require authentication via NextAuth session.
|
||||||
|
|
||||||
|
5. **Error Handling**: All components implement error states with retry mechanisms.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Debugging
|
||||||
|
|
||||||
|
- Use `/api/debug/notifications` to test notification system
|
||||||
|
- Check browser console for detailed logs (all components log extensively)
|
||||||
|
- Check Redis cache keys: `notifications:count:{userId}`, `notifications:list:{userId}:{page}:{limit}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last Updated: Generated from codebase analysis*
|
||||||
540
STACK_QUALITY_AND_FLOW_ANALYSIS.md
Normal file
540
STACK_QUALITY_AND_FLOW_ANALYSIS.md
Normal file
@ -0,0 +1,540 @@
|
|||||||
|
# Stack Quality & Flow Analysis Report
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
This document provides a comprehensive analysis of the codebase quality, architecture patterns, and identifies critical issues in the notification and widget update flows.
|
||||||
|
|
||||||
|
**Overall Assessment**: ⚠️ **Moderate Quality** - Good foundation with several critical issues that need attention.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 Critical Issues
|
||||||
|
|
||||||
|
### 1. **Memory Leak: Multiple Polling Intervals**
|
||||||
|
|
||||||
|
**Location**: `hooks/use-notifications.ts`, `components/parole.tsx`, `components/calendar/calendar-widget.tsx`
|
||||||
|
|
||||||
|
**Problem**:
|
||||||
|
- `useNotifications` hook creates polling intervals that may not be properly cleaned up
|
||||||
|
- Multiple components using the hook can create duplicate intervals
|
||||||
|
- `startPolling()` returns a cleanup function but it's not properly used in the useEffect
|
||||||
|
|
||||||
|
**Code Issue**:
|
||||||
|
```typescript
|
||||||
|
// Line 226 in use-notifications.ts
|
||||||
|
return () => stopPolling(); // This return is inside startPolling, not useEffect!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: Memory leaks, excessive API calls, degraded performance
|
||||||
|
|
||||||
|
**Fix Required**:
|
||||||
|
```typescript
|
||||||
|
useEffect(() => {
|
||||||
|
isMountedRef.current = true;
|
||||||
|
|
||||||
|
if (status === 'authenticated' && session?.user) {
|
||||||
|
fetchNotificationCount(true);
|
||||||
|
fetchNotifications();
|
||||||
|
startPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMountedRef.current = false;
|
||||||
|
stopPolling(); // ✅ Correct placement
|
||||||
|
};
|
||||||
|
}, [status, session?.user, fetchNotificationCount, fetchNotifications, startPolling, stopPolling]);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. **Race Condition: Notification Badge Double Fetching**
|
||||||
|
|
||||||
|
**Location**: `components/notification-badge.tsx`
|
||||||
|
|
||||||
|
**Problem**:
|
||||||
|
- Multiple `useEffect` hooks trigger `manualFetch()` simultaneously
|
||||||
|
- Lines 65-70, 82-87, and 92-99 all trigger fetches
|
||||||
|
- No debouncing or request deduplication
|
||||||
|
|
||||||
|
**Code Issue**:
|
||||||
|
```typescript
|
||||||
|
// Line 65-70: Fetch on dropdown open
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && status === 'authenticated') {
|
||||||
|
manualFetch();
|
||||||
|
}
|
||||||
|
}, [isOpen, status]);
|
||||||
|
|
||||||
|
// Line 82-87: Fetch on mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (status === 'authenticated') {
|
||||||
|
manualFetch();
|
||||||
|
}
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
|
// Line 92-99: Fetch on handleOpenChange
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
setIsOpen(open);
|
||||||
|
if (open && status === 'authenticated') {
|
||||||
|
manualFetch(); // Duplicate fetch!
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: Unnecessary API calls, potential race conditions, poor UX
|
||||||
|
|
||||||
|
**Fix Required**: Consolidate fetch logic, add request deduplication
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. **Redis KEYS Command Performance Issue**
|
||||||
|
|
||||||
|
**Location**: `lib/services/notifications/notification-service.ts` (line 293)
|
||||||
|
|
||||||
|
**Problem**:
|
||||||
|
- Using `redis.keys()` which is O(N) and blocks Redis
|
||||||
|
- Can cause performance degradation in production
|
||||||
|
|
||||||
|
**Code Issue**:
|
||||||
|
```typescript
|
||||||
|
// Line 293 - BAD
|
||||||
|
const listKeys = await redis.keys(listKeysPattern);
|
||||||
|
if (listKeys.length > 0) {
|
||||||
|
await redis.del(...listKeys);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: Redis blocking, slow response times, potential timeouts
|
||||||
|
|
||||||
|
**Fix Required**: Use `SCAN` instead of `KEYS`:
|
||||||
|
```typescript
|
||||||
|
// GOOD - Use SCAN
|
||||||
|
let cursor = '0';
|
||||||
|
do {
|
||||||
|
const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', listKeysPattern, 'COUNT', 100);
|
||||||
|
cursor = nextCursor;
|
||||||
|
if (keys.length > 0) {
|
||||||
|
await redis.del(...keys);
|
||||||
|
}
|
||||||
|
} while (cursor !== '0');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. **Infinite Loop Risk: useEffect Dependencies**
|
||||||
|
|
||||||
|
**Location**: `hooks/use-notifications.ts` (line 255)
|
||||||
|
|
||||||
|
**Problem**:
|
||||||
|
- `useEffect` includes functions in dependencies that are recreated on every render
|
||||||
|
- `fetchNotificationCount`, `fetchNotifications`, `startPolling`, `stopPolling` are in deps
|
||||||
|
- These functions depend on `session?.user` which changes, causing re-renders
|
||||||
|
|
||||||
|
**Code Issue**:
|
||||||
|
```typescript
|
||||||
|
useEffect(() => {
|
||||||
|
// ...
|
||||||
|
}, [status, session?.user, fetchNotificationCount, fetchNotifications, startPolling, stopPolling]);
|
||||||
|
// ❌ Functions are recreated, causing infinite loops
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: Infinite re-renders, excessive API calls, browser freezing
|
||||||
|
|
||||||
|
**Fix Required**: Remove function dependencies or use `useCallback` properly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. **Background Refresh Memory Leak**
|
||||||
|
|
||||||
|
**Location**: `lib/services/notifications/notification-service.ts` (line 326)
|
||||||
|
|
||||||
|
**Problem**:
|
||||||
|
- `setTimeout` in `scheduleBackgroundRefresh` creates closures that may not be cleaned up
|
||||||
|
- No way to cancel pending background refreshes
|
||||||
|
- Can accumulate in serverless environments
|
||||||
|
|
||||||
|
**Code Issue**:
|
||||||
|
```typescript
|
||||||
|
setTimeout(async () => {
|
||||||
|
// This closure holds references and may not be garbage collected
|
||||||
|
await this.getNotificationCount(userId);
|
||||||
|
await this.getNotifications(userId, 1, 20);
|
||||||
|
}, 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: Memory leaks, especially in serverless/edge environments
|
||||||
|
|
||||||
|
**Fix Required**: Use proper cleanup mechanism or job queue
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ High Priority Issues
|
||||||
|
|
||||||
|
### 6. **Widget Update Race Conditions**
|
||||||
|
|
||||||
|
**Location**: Multiple widget components
|
||||||
|
|
||||||
|
**Problem**:
|
||||||
|
- Widgets don't coordinate updates
|
||||||
|
- Multiple widgets can trigger simultaneous API calls
|
||||||
|
- No request deduplication
|
||||||
|
|
||||||
|
**Affected Widgets**:
|
||||||
|
- `components/calendar.tsx` - Auto-refresh every 5 minutes
|
||||||
|
- `components/parole.tsx` - Auto-polling every 30 seconds
|
||||||
|
- `components/news.tsx` - Manual refresh only
|
||||||
|
- `components/flow.tsx` - Manual refresh only
|
||||||
|
- `components/email.tsx` - Manual refresh only
|
||||||
|
|
||||||
|
**Impact**: Unnecessary load on backend, potential rate limiting
|
||||||
|
|
||||||
|
**Fix Required**: Implement request deduplication layer or use React Query/SWR
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. **Redis Connection Singleton Issues**
|
||||||
|
|
||||||
|
**Location**: `lib/redis.ts`
|
||||||
|
|
||||||
|
**Problem**:
|
||||||
|
- Singleton pattern but no proper connection pooling
|
||||||
|
- In serverless environments, connections may not be reused
|
||||||
|
- No connection health monitoring
|
||||||
|
- Race condition in `getRedisClient()` when `isConnecting` is true
|
||||||
|
|
||||||
|
**Code Issue**:
|
||||||
|
```typescript
|
||||||
|
if (isConnecting) {
|
||||||
|
if (redisClient) return redisClient;
|
||||||
|
// ⚠️ What if redisClient is null but isConnecting is true?
|
||||||
|
console.warn('Redis connection in progress, creating temporary client');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: Connection leaks, connection pool exhaustion, degraded performance
|
||||||
|
|
||||||
|
**Fix Required**: Implement proper connection pool or use Redis connection manager
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. **Error Handling Gaps**
|
||||||
|
|
||||||
|
**Location**: Multiple files
|
||||||
|
|
||||||
|
**Problems**:
|
||||||
|
- Errors are logged but not always handled gracefully
|
||||||
|
- No retry logic for transient failures
|
||||||
|
- No circuit breaker pattern
|
||||||
|
- Widgets show errors but don't recover automatically
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
- `components/notification-badge.tsx` - Shows error but no auto-retry
|
||||||
|
- `lib/services/notifications/notification-service.ts` - Errors return empty arrays silently
|
||||||
|
- Widget components - Errors stop updates, no recovery
|
||||||
|
|
||||||
|
**Impact**: Poor UX, silent failures, degraded functionality
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. **Cache Invalidation Issues**
|
||||||
|
|
||||||
|
**Location**: `lib/services/notifications/notification-service.ts`
|
||||||
|
|
||||||
|
**Problem**:
|
||||||
|
- Cache invalidation uses `KEYS` command (blocking)
|
||||||
|
- No partial cache invalidation
|
||||||
|
- Background refresh may not invalidate properly
|
||||||
|
- Race condition: cache can be invalidated while being refreshed
|
||||||
|
|
||||||
|
**Impact**: Stale data, inconsistent state
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. **Excessive Logging**
|
||||||
|
|
||||||
|
**Location**: Throughout codebase
|
||||||
|
|
||||||
|
**Problem**:
|
||||||
|
- Console.log statements everywhere
|
||||||
|
- No log levels
|
||||||
|
- Production code has debug logs
|
||||||
|
- Performance impact from string concatenation
|
||||||
|
|
||||||
|
**Impact**: Performance degradation, log storage costs, security concerns
|
||||||
|
|
||||||
|
**Fix Required**: Use proper logging library with levels (e.g., Winston, Pino)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Architecture Quality Assessment
|
||||||
|
|
||||||
|
### Strengths ✅
|
||||||
|
|
||||||
|
1. **Adapter Pattern**: Well-implemented notification adapter pattern
|
||||||
|
2. **Separation of Concerns**: Clear separation between services, hooks, and components
|
||||||
|
3. **Type Safety**: Good TypeScript usage
|
||||||
|
4. **Caching Strategy**: Redis caching implemented
|
||||||
|
5. **Error Boundaries**: Some error handling present
|
||||||
|
|
||||||
|
### Weaknesses ❌
|
||||||
|
|
||||||
|
1. **No State Management**: Using local state instead of global state management
|
||||||
|
2. **No Request Deduplication**: Multiple components can trigger same API calls
|
||||||
|
3. **No Request Cancellation**: No way to cancel in-flight requests
|
||||||
|
4. **No Optimistic Updates**: UI doesn't update optimistically
|
||||||
|
5. **No Offline Support**: No handling for offline scenarios
|
||||||
|
6. **No Request Queue**: No queuing mechanism for API calls
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Flow Analysis
|
||||||
|
|
||||||
|
### Notification Flow Issues
|
||||||
|
|
||||||
|
#### Flow Diagram (Current - Problematic):
|
||||||
|
```
|
||||||
|
User Action / Polling
|
||||||
|
↓
|
||||||
|
useNotifications Hook (multiple instances)
|
||||||
|
↓
|
||||||
|
Multiple API Calls (no deduplication)
|
||||||
|
↓
|
||||||
|
NotificationService (Redis cache check)
|
||||||
|
↓
|
||||||
|
Adapter Calls (parallel, but no error aggregation)
|
||||||
|
↓
|
||||||
|
Response (may be stale due to race conditions)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Issues:
|
||||||
|
1. **Multiple Hook Instances**: `NotificationBadge` and potentially other components use `useNotifications`, creating multiple polling intervals
|
||||||
|
2. **No Request Deduplication**: Same request can be made multiple times simultaneously
|
||||||
|
3. **Cache Race Conditions**: Background refresh can conflict with user requests
|
||||||
|
4. **No Request Cancellation**: Old requests aren't cancelled when new ones start
|
||||||
|
|
||||||
|
### Widget Update Flow Issues
|
||||||
|
|
||||||
|
#### Flow Diagram (Current - Problematic):
|
||||||
|
```
|
||||||
|
Component Mount
|
||||||
|
↓
|
||||||
|
useEffect triggers fetch
|
||||||
|
↓
|
||||||
|
API Call (no coordination with other widgets)
|
||||||
|
↓
|
||||||
|
State Update (may cause unnecessary re-renders)
|
||||||
|
↓
|
||||||
|
Auto-refresh interval (no cleanup guarantee)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Issues:
|
||||||
|
1. **No Coordination**: Widgets don't know about each other's updates
|
||||||
|
2. **Duplicate Requests**: Same data fetched multiple times
|
||||||
|
3. **Cleanup Issues**: Intervals may not be cleaned up properly
|
||||||
|
4. **No Stale-While-Revalidate**: No background updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Recommendations
|
||||||
|
|
||||||
|
### Immediate Actions (Critical)
|
||||||
|
|
||||||
|
1. **Fix Memory Leaks**
|
||||||
|
- Fix `useNotifications` cleanup
|
||||||
|
- Ensure all intervals are cleared
|
||||||
|
- Add cleanup in all widget components
|
||||||
|
|
||||||
|
2. **Fix Race Conditions**
|
||||||
|
- Implement request deduplication
|
||||||
|
- Fix notification badge double fetching
|
||||||
|
- Add request cancellation
|
||||||
|
|
||||||
|
3. **Fix Redis Performance**
|
||||||
|
- Replace `KEYS` with `SCAN`
|
||||||
|
- Implement proper connection pooling
|
||||||
|
- Add connection health checks
|
||||||
|
|
||||||
|
### Short-term Improvements (High Priority)
|
||||||
|
|
||||||
|
1. **Implement Request Management**
|
||||||
|
- Use React Query or SWR for request deduplication
|
||||||
|
- Implement request cancellation
|
||||||
|
- Add request queuing
|
||||||
|
|
||||||
|
2. **Improve Error Handling**
|
||||||
|
- Add retry logic with exponential backoff
|
||||||
|
- Implement circuit breaker pattern
|
||||||
|
- Add error boundaries
|
||||||
|
|
||||||
|
3. **Optimize Caching**
|
||||||
|
- Implement stale-while-revalidate pattern
|
||||||
|
- Add cache versioning
|
||||||
|
- Improve cache invalidation strategy
|
||||||
|
|
||||||
|
### Long-term Improvements (Medium Priority)
|
||||||
|
|
||||||
|
1. **State Management**
|
||||||
|
- Consider Zustand or Redux for global state
|
||||||
|
- Centralize notification state
|
||||||
|
- Implement optimistic updates
|
||||||
|
|
||||||
|
2. **Monitoring & Observability**
|
||||||
|
- Add proper logging (Winston/Pino)
|
||||||
|
- Implement metrics collection
|
||||||
|
- Add performance monitoring
|
||||||
|
|
||||||
|
3. **Testing**
|
||||||
|
- Add unit tests for hooks
|
||||||
|
- Add integration tests for flows
|
||||||
|
- Add E2E tests for critical paths
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Performance Metrics (Estimated)
|
||||||
|
|
||||||
|
### Current Performance Issues:
|
||||||
|
|
||||||
|
1. **API Calls**:
|
||||||
|
- Estimated 2-3x more calls than necessary due to race conditions
|
||||||
|
- No request deduplication
|
||||||
|
|
||||||
|
2. **Memory Usage**:
|
||||||
|
- Potential memory leaks from uncleaned intervals
|
||||||
|
- Closures holding references
|
||||||
|
|
||||||
|
3. **Redis Performance**:
|
||||||
|
- `KEYS` command can block for seconds with many keys
|
||||||
|
- No connection pooling
|
||||||
|
|
||||||
|
4. **Bundle Size**:
|
||||||
|
- Excessive logging increases bundle size
|
||||||
|
- No code splitting for widgets
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Code Quality Metrics
|
||||||
|
|
||||||
|
### Code Smells Found:
|
||||||
|
|
||||||
|
1. **Long Functions**: Some functions exceed 50 lines
|
||||||
|
2. **High Cyclomatic Complexity**: `useNotifications` hook has high complexity
|
||||||
|
3. **Duplicate Code**: Similar fetch patterns across widgets
|
||||||
|
4. **Magic Numbers**: Hardcoded intervals (300000, 60000, etc.)
|
||||||
|
5. **Inconsistent Error Handling**: Different error handling patterns
|
||||||
|
|
||||||
|
### Technical Debt:
|
||||||
|
|
||||||
|
- **Estimated**: Medium-High
|
||||||
|
- **Areas**:
|
||||||
|
- Memory management
|
||||||
|
- Request management
|
||||||
|
- Error handling
|
||||||
|
- Caching strategy
|
||||||
|
- Logging infrastructure
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Specific Code Fixes Needed
|
||||||
|
|
||||||
|
### Fix 1: useNotifications Hook Cleanup
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BEFORE (Current - Problematic)
|
||||||
|
useEffect(() => {
|
||||||
|
isMountedRef.current = true;
|
||||||
|
|
||||||
|
if (status === 'authenticated' && session?.user) {
|
||||||
|
fetchNotificationCount(true);
|
||||||
|
fetchNotifications();
|
||||||
|
startPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMountedRef.current = false;
|
||||||
|
stopPolling();
|
||||||
|
};
|
||||||
|
}, [status, session?.user, fetchNotificationCount, fetchNotifications, startPolling, stopPolling]);
|
||||||
|
|
||||||
|
// AFTER (Fixed)
|
||||||
|
useEffect(() => {
|
||||||
|
if (status !== 'authenticated' || !session?.user) return;
|
||||||
|
|
||||||
|
isMountedRef.current = true;
|
||||||
|
|
||||||
|
// Initial fetch
|
||||||
|
fetchNotificationCount(true);
|
||||||
|
fetchNotifications();
|
||||||
|
|
||||||
|
// Start polling
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
debouncedFetchCount();
|
||||||
|
}
|
||||||
|
}, POLLING_INTERVAL);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
isMountedRef.current = false;
|
||||||
|
clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, [status, session?.user?.id]); // Only depend on primitive values
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fix 2: Notification Badge Deduplication
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Add request deduplication
|
||||||
|
const fetchInProgressRef = useRef(false);
|
||||||
|
|
||||||
|
const manualFetch = async () => {
|
||||||
|
if (fetchInProgressRef.current) {
|
||||||
|
console.log('[NOTIFICATION_BADGE] Fetch already in progress, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchInProgressRef.current = true;
|
||||||
|
try {
|
||||||
|
await fetchNotifications(1, 10);
|
||||||
|
} finally {
|
||||||
|
fetchInProgressRef.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fix 3: Redis SCAN Instead of KEYS
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BEFORE
|
||||||
|
const listKeys = await redis.keys(listKeysPattern);
|
||||||
|
|
||||||
|
// AFTER
|
||||||
|
const listKeys: string[] = [];
|
||||||
|
let cursor = '0';
|
||||||
|
do {
|
||||||
|
const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', listKeysPattern, 'COUNT', 100);
|
||||||
|
cursor = nextCursor;
|
||||||
|
listKeys.push(...keys);
|
||||||
|
} while (cursor !== '0');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Conclusion
|
||||||
|
|
||||||
|
The codebase has a solid foundation with good architectural patterns (adapter pattern, separation of concerns), but suffers from several critical issues:
|
||||||
|
|
||||||
|
1. **Memory leaks** from improper cleanup
|
||||||
|
2. **Race conditions** from lack of request coordination
|
||||||
|
3. **Performance issues** from blocking Redis operations
|
||||||
|
4. **Error handling gaps** that degrade UX
|
||||||
|
|
||||||
|
**Priority**: Fix critical issues immediately, then implement improvements incrementally.
|
||||||
|
|
||||||
|
**Estimated Effort**:
|
||||||
|
- Critical fixes: 2-3 days
|
||||||
|
- High priority improvements: 1-2 weeks
|
||||||
|
- Long-term improvements: 1-2 months
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Generated: Comprehensive codebase analysis*
|
||||||
302
UNIFIED_REFRESH_SUMMARY.md
Normal file
302
UNIFIED_REFRESH_SUMMARY.md
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
# Unified Refresh System - Implementation Summary
|
||||||
|
|
||||||
|
## ✅ What Has Been Created
|
||||||
|
|
||||||
|
### Core Infrastructure Files
|
||||||
|
|
||||||
|
1. **`lib/constants/refresh-intervals.ts`**
|
||||||
|
- Standardized refresh intervals for all resources
|
||||||
|
- Helper functions for interval management
|
||||||
|
- All intervals harmonized and documented
|
||||||
|
|
||||||
|
2. **`lib/utils/request-deduplication.ts`**
|
||||||
|
- Request deduplication utility
|
||||||
|
- Prevents duplicate API calls within 5 seconds
|
||||||
|
- Automatic cleanup of stale requests
|
||||||
|
|
||||||
|
3. **`lib/services/refresh-manager.ts`**
|
||||||
|
- Centralized refresh management
|
||||||
|
- Handles all refresh intervals
|
||||||
|
- Provides pause/resume functionality
|
||||||
|
- Prevents duplicate refreshes
|
||||||
|
|
||||||
|
4. **`hooks/use-unified-refresh.ts`**
|
||||||
|
- React hook for easy integration
|
||||||
|
- Automatic registration/cleanup
|
||||||
|
- Manual refresh support
|
||||||
|
|
||||||
|
### Documentation Files
|
||||||
|
|
||||||
|
1. **`IMPLEMENTATION_PLAN_UNIFIED_REFRESH.md`**
|
||||||
|
- Complete architecture overview
|
||||||
|
- Detailed implementation guide
|
||||||
|
- Code examples for all widgets
|
||||||
|
|
||||||
|
2. **`IMPLEMENTATION_CHECKLIST.md`**
|
||||||
|
- Step-by-step checklist
|
||||||
|
- Daily progress tracking
|
||||||
|
- Success criteria
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Next Steps
|
||||||
|
|
||||||
|
### Immediate Actions (Start Here)
|
||||||
|
|
||||||
|
#### 1. Fix Critical Memory Leaks (30 minutes)
|
||||||
|
|
||||||
|
**File**: `lib/services/notifications/notification-service.ts`
|
||||||
|
|
||||||
|
Replace `redis.keys()` with `redis.scan()`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Line 293 - BEFORE
|
||||||
|
const listKeys = await redis.keys(listKeysPattern);
|
||||||
|
|
||||||
|
// AFTER
|
||||||
|
const listKeys: string[] = [];
|
||||||
|
let cursor = '0';
|
||||||
|
do {
|
||||||
|
const [nextCursor, keys] = await redis.scan(
|
||||||
|
cursor,
|
||||||
|
'MATCH',
|
||||||
|
listKeysPattern,
|
||||||
|
'COUNT',
|
||||||
|
100
|
||||||
|
);
|
||||||
|
cursor = nextCursor;
|
||||||
|
if (keys.length > 0) {
|
||||||
|
listKeys.push(...keys);
|
||||||
|
}
|
||||||
|
} while (cursor !== '0');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. Test Core Infrastructure (1 hour)
|
||||||
|
|
||||||
|
Create a test file to verify everything works:
|
||||||
|
|
||||||
|
**File**: `lib/services/__tests__/refresh-manager.test.ts` (optional)
|
||||||
|
|
||||||
|
Or test manually:
|
||||||
|
1. Import refresh manager in a component
|
||||||
|
2. Register a test resource
|
||||||
|
3. Verify it refreshes at correct interval
|
||||||
|
4. Verify cleanup on unmount
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3. Refactor Notifications (2-3 hours)
|
||||||
|
|
||||||
|
**File**: `hooks/use-notifications.ts`
|
||||||
|
|
||||||
|
Key changes:
|
||||||
|
- Remove manual polling logic
|
||||||
|
- Use `useUnifiedRefresh` hook
|
||||||
|
- Add `requestDeduplicator` for API calls
|
||||||
|
- Fix useEffect dependencies
|
||||||
|
|
||||||
|
See `IMPLEMENTATION_PLAN_UNIFIED_REFRESH.md` Section 3.1 for full code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4. Refactor Notification Badge (1 hour)
|
||||||
|
|
||||||
|
**File**: `components/notification-badge.tsx`
|
||||||
|
|
||||||
|
Key changes:
|
||||||
|
- Remove duplicate `useEffect` hooks
|
||||||
|
- Use hook's `refresh` function for manual refresh
|
||||||
|
- Remove manual fetch logic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 5. Refactor Navigation Bar Time (30 minutes)
|
||||||
|
|
||||||
|
**File**: `components/main-nav.tsx` + `components/main-nav-time.tsx` (new)
|
||||||
|
|
||||||
|
Key changes:
|
||||||
|
- Extract time display to separate component
|
||||||
|
- Use `useUnifiedRefresh` hook (1 second interval)
|
||||||
|
- Fix static time issue
|
||||||
|
|
||||||
|
See `IMPLEMENTATION_PLAN_UNIFIED_REFRESH.md` Section 3.7 for full code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 6. Refactor Widgets (1 hour each)
|
||||||
|
|
||||||
|
Start with high-frequency widgets:
|
||||||
|
1. **Parole** (`components/parole.tsx`) - 30s interval
|
||||||
|
2. **Calendar** (`components/calendar.tsx`) - 5min interval
|
||||||
|
3. **News** (`components/news.tsx`) - 10min interval
|
||||||
|
4. **Email** (`components/email.tsx`) - 1min interval
|
||||||
|
5. **Duties** (`components/flow.tsx`) - 2min interval
|
||||||
|
|
||||||
|
See `IMPLEMENTATION_PLAN_UNIFIED_REFRESH.md` Section 3.2 for example code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Expected Results
|
||||||
|
|
||||||
|
### Before Implementation:
|
||||||
|
- ❌ 120-150 API calls/minute
|
||||||
|
- ❌ Memory leaks from uncleaned intervals
|
||||||
|
- ❌ Duplicate requests
|
||||||
|
- ❌ No coordination between widgets
|
||||||
|
|
||||||
|
### After Implementation:
|
||||||
|
- ✅ 40-50 API calls/minute (60-70% reduction)
|
||||||
|
- ✅ No memory leaks
|
||||||
|
- ✅ Request deduplication working
|
||||||
|
- ✅ Centralized refresh coordination
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Testing Checklist
|
||||||
|
|
||||||
|
After each phase, verify:
|
||||||
|
|
||||||
|
- [ ] No console errors
|
||||||
|
- [ ] Widgets refresh at correct intervals
|
||||||
|
- [ ] Manual refresh buttons work
|
||||||
|
- [ ] No duplicate API calls (check Network tab)
|
||||||
|
- [ ] No memory leaks (check Memory tab)
|
||||||
|
- [ ] Cleanup on component unmount
|
||||||
|
- [ ] Multiple tabs don't cause issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 Important Notes
|
||||||
|
|
||||||
|
### Backward Compatibility
|
||||||
|
|
||||||
|
All new code is designed to be:
|
||||||
|
- ✅ Non-breaking (old code still works)
|
||||||
|
- ✅ Gradual migration (one widget at a time)
|
||||||
|
- ✅ Easy rollback (keep old implementations)
|
||||||
|
|
||||||
|
### Migration Strategy
|
||||||
|
|
||||||
|
1. **Phase 1**: Core infrastructure (DONE ✅)
|
||||||
|
2. **Phase 2**: Fix critical issues
|
||||||
|
3. **Phase 3**: Migrate notifications
|
||||||
|
4. **Phase 4**: Migrate widgets one by one
|
||||||
|
5. **Phase 5**: Remove old code
|
||||||
|
|
||||||
|
### Feature Flags (Optional)
|
||||||
|
|
||||||
|
If you want to toggle the new system:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In refresh manager
|
||||||
|
const USE_UNIFIED_REFRESH = process.env.NEXT_PUBLIC_USE_UNIFIED_REFRESH !== 'false';
|
||||||
|
|
||||||
|
if (USE_UNIFIED_REFRESH) {
|
||||||
|
// Use new system
|
||||||
|
} else {
|
||||||
|
// Use old system
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Performance Monitoring
|
||||||
|
|
||||||
|
### Metrics to Track
|
||||||
|
|
||||||
|
1. **API Call Count**
|
||||||
|
- Before: ~120-150/min
|
||||||
|
- Target: ~40-50/min
|
||||||
|
- Monitor in Network tab
|
||||||
|
|
||||||
|
2. **Memory Usage**
|
||||||
|
- Before: Growing over time
|
||||||
|
- Target: Stable
|
||||||
|
- Monitor in Memory tab
|
||||||
|
|
||||||
|
3. **Refresh Accuracy**
|
||||||
|
- Verify intervals are correct
|
||||||
|
- Check last refresh times
|
||||||
|
- Monitor refresh manager status
|
||||||
|
|
||||||
|
### Debug Tools
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get refresh manager status
|
||||||
|
const status = refreshManager.getStatus();
|
||||||
|
console.log('Refresh Manager Status:', status);
|
||||||
|
|
||||||
|
// Get pending requests
|
||||||
|
const pendingCount = requestDeduplicator.getPendingCount();
|
||||||
|
console.log('Pending Requests:', pendingCount);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎓 Learning Resources
|
||||||
|
|
||||||
|
### Key Concepts
|
||||||
|
|
||||||
|
1. **Singleton Pattern**: Refresh manager uses singleton
|
||||||
|
2. **Request Deduplication**: Prevents duplicate calls
|
||||||
|
3. **React Hooks**: Proper cleanup with useEffect
|
||||||
|
4. **Memory Management**: Clearing intervals and refs
|
||||||
|
|
||||||
|
### Code Patterns
|
||||||
|
|
||||||
|
- **useRef for callbacks**: Prevents dependency issues
|
||||||
|
- **Map for tracking**: Efficient resource management
|
||||||
|
- **Promise tracking**: Prevents duplicate requests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Issue: Widgets not refreshing
|
||||||
|
|
||||||
|
**Check**:
|
||||||
|
1. Is refresh manager started? (`refreshManager.start()`)
|
||||||
|
2. Is resource registered? (`refreshManager.getStatus()`)
|
||||||
|
3. Is user authenticated? (`status === 'authenticated'`)
|
||||||
|
|
||||||
|
### Issue: Duplicate API calls
|
||||||
|
|
||||||
|
**Check**:
|
||||||
|
1. Is request deduplication working? (`requestDeduplicator.getPendingCount()`)
|
||||||
|
2. Are multiple components using the same resource?
|
||||||
|
3. Is TTL too short?
|
||||||
|
|
||||||
|
### Issue: Memory leaks
|
||||||
|
|
||||||
|
**Check**:
|
||||||
|
1. Are intervals cleaned up? (check cleanup functions)
|
||||||
|
2. Are refs cleared? (`isMountedRef.current = false`)
|
||||||
|
3. Are pending requests cleared? (check cleanup)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Next Session Goals
|
||||||
|
|
||||||
|
1. ✅ Core infrastructure created
|
||||||
|
2. ⏭️ Fix Redis KEYS → SCAN
|
||||||
|
3. ⏭️ Refactor notifications hook
|
||||||
|
4. ⏭️ Refactor notification badge
|
||||||
|
5. ⏭️ Refactor first widget (Parole)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 Success!
|
||||||
|
|
||||||
|
Once all widgets are migrated:
|
||||||
|
|
||||||
|
- ✅ Unified refresh system
|
||||||
|
- ✅ 60%+ reduction in API calls
|
||||||
|
- ✅ No memory leaks
|
||||||
|
- ✅ Better user experience
|
||||||
|
- ✅ Easier maintenance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last Updated: Implementation Summary v1.0*
|
||||||
39
components/main-nav-time.tsx
Normal file
39
components/main-nav-time.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Navigation Bar Time Display Component
|
||||||
|
*
|
||||||
|
* Displays current date and time in the navigation bar.
|
||||||
|
* Uses unified refresh system for consistent time updates.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { fr } from 'date-fns/locale';
|
||||||
|
import { useUnifiedRefresh } from '@/hooks/use-unified-refresh';
|
||||||
|
import { REFRESH_INTERVALS } from '@/lib/constants/refresh-intervals';
|
||||||
|
|
||||||
|
export function MainNavTime() {
|
||||||
|
const [currentTime, setCurrentTime] = useState(new Date());
|
||||||
|
|
||||||
|
// Update time using unified refresh system
|
||||||
|
useUnifiedRefresh({
|
||||||
|
resource: 'navbar-time',
|
||||||
|
interval: REFRESH_INTERVALS.NAVBAR_TIME,
|
||||||
|
enabled: true, // Always enabled (no auth required for time display)
|
||||||
|
onRefresh: async () => {
|
||||||
|
setCurrentTime(new Date());
|
||||||
|
},
|
||||||
|
priority: 'high', // High priority for real-time clock
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedDate = format(currentTime, "d MMMM yyyy", { locale: fr });
|
||||||
|
const formattedTime = format(currentTime, "HH:mm");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="hidden md:flex flex-col items-center">
|
||||||
|
<div className="text-white/80 text-xs">{formattedDate}</div>
|
||||||
|
<div className="text-white text-sm font-medium">{formattedTime}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -37,11 +37,10 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { format } from 'date-fns';
|
import { NotificationBadge } from "./notification-badge";
|
||||||
import { fr } from 'date-fns/locale';
|
import { NotesDialog } from "./notes-dialog";
|
||||||
import { NotificationBadge } from './notification-badge';
|
|
||||||
import { NotesDialog } from './notes-dialog';
|
|
||||||
import { WindowControls } from "@/components/electron/WindowControls";
|
import { WindowControls } from "@/components/electron/WindowControls";
|
||||||
|
import { MainNavTime } from "./main-nav-time";
|
||||||
|
|
||||||
const requestNotificationPermission = async () => {
|
const requestNotificationPermission = async () => {
|
||||||
try {
|
try {
|
||||||
@ -222,14 +221,9 @@ export function MainNav() {
|
|||||||
// Get visible menu items based on user roles
|
// Get visible menu items based on user roles
|
||||||
const visibleMenuItems = [
|
const visibleMenuItems = [
|
||||||
...baseMenuItems,
|
...baseMenuItems,
|
||||||
...roleSpecificItems.filter(item => hasRole(item.requiredRoles))
|
...roleSpecificItems.filter((item) => hasRole(item.requiredRoles))
|
||||||
];
|
];
|
||||||
|
|
||||||
// Format current date and time
|
|
||||||
const now = new Date();
|
|
||||||
const formattedDate = format(now, "d MMMM yyyy", { locale: fr });
|
|
||||||
const formattedTime = format(now, "HH:mm");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="fixed top-0 left-0 right-0 z-50 bg-black">
|
<div className="fixed top-0 left-0 right-0 z-50 bg-black">
|
||||||
@ -292,10 +286,7 @@ export function MainNav() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Center - Date and Time */}
|
{/* Center - Date and Time */}
|
||||||
<div className="hidden md:flex flex-col items-center">
|
<MainNavTime />
|
||||||
<div className="text-white/80 text-xs">{formattedDate}</div>
|
|
||||||
<div className="text-white text-sm font-medium">{formattedTime}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right side */}
|
{/* Right side */}
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
|||||||
107
hooks/use-unified-refresh.ts
Normal file
107
hooks/use-unified-refresh.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* Unified Refresh Hook
|
||||||
|
*
|
||||||
|
* Provides consistent refresh functionality for all widgets and notifications.
|
||||||
|
* Handles registration, cleanup, and manual refresh triggers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useCallback, useRef } from 'react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
import { refreshManager, RefreshableResource } from '@/lib/services/refresh-manager';
|
||||||
|
|
||||||
|
interface UseUnifiedRefreshOptions {
|
||||||
|
resource: RefreshableResource;
|
||||||
|
interval: number;
|
||||||
|
enabled?: boolean;
|
||||||
|
onRefresh: () => Promise<void>;
|
||||||
|
priority?: 'high' | 'medium' | 'low';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for unified refresh management
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* const { refresh } = useUnifiedRefresh({
|
||||||
|
* resource: 'calendar',
|
||||||
|
* interval: 300000, // 5 minutes
|
||||||
|
* enabled: status === 'authenticated',
|
||||||
|
* onRefresh: fetchEvents,
|
||||||
|
* priority: 'low',
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function useUnifiedRefresh({
|
||||||
|
resource,
|
||||||
|
interval,
|
||||||
|
enabled = true,
|
||||||
|
onRefresh,
|
||||||
|
priority = 'medium',
|
||||||
|
}: UseUnifiedRefreshOptions) {
|
||||||
|
const { status } = useSession();
|
||||||
|
const onRefreshRef = useRef(onRefresh);
|
||||||
|
const isMountedRef = useRef(true);
|
||||||
|
|
||||||
|
// Update callback ref when it changes
|
||||||
|
useEffect(() => {
|
||||||
|
onRefreshRef.current = onRefresh;
|
||||||
|
}, [onRefresh]);
|
||||||
|
|
||||||
|
// Register/unregister with refresh manager
|
||||||
|
useEffect(() => {
|
||||||
|
const isAuthenticated = status === 'authenticated';
|
||||||
|
const shouldEnable = enabled && isAuthenticated;
|
||||||
|
|
||||||
|
if (!shouldEnable) {
|
||||||
|
// Unregister if disabled or not authenticated
|
||||||
|
refreshManager.unregister(resource);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isMountedRef.current = true;
|
||||||
|
|
||||||
|
console.log(`[useUnifiedRefresh] Registering ${resource} (interval: ${interval}ms, priority: ${priority})`);
|
||||||
|
|
||||||
|
// Register with refresh manager
|
||||||
|
refreshManager.register({
|
||||||
|
resource,
|
||||||
|
interval,
|
||||||
|
enabled: true,
|
||||||
|
priority,
|
||||||
|
onRefresh: async () => {
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
await onRefreshRef.current();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start refresh manager if not already started
|
||||||
|
refreshManager.start();
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
console.log(`[useUnifiedRefresh] Cleaning up ${resource}`);
|
||||||
|
isMountedRef.current = false;
|
||||||
|
refreshManager.unregister(resource);
|
||||||
|
};
|
||||||
|
}, [resource, interval, enabled, priority, status]);
|
||||||
|
|
||||||
|
// Manual refresh function
|
||||||
|
const refresh = useCallback(
|
||||||
|
async (force = false) => {
|
||||||
|
if (status !== 'authenticated') {
|
||||||
|
console.warn(`[useUnifiedRefresh] Cannot refresh ${resource}: not authenticated`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[useUnifiedRefresh] Manual refresh triggered for ${resource} (force: ${force})`);
|
||||||
|
await refreshManager.refresh(resource, force);
|
||||||
|
},
|
||||||
|
[resource, status]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
refresh,
|
||||||
|
isActive: refreshManager.getStatus().active,
|
||||||
|
};
|
||||||
|
}
|
||||||
65
lib/constants/refresh-intervals.ts
Normal file
65
lib/constants/refresh-intervals.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* Standard Refresh Intervals
|
||||||
|
* All intervals in milliseconds
|
||||||
|
*
|
||||||
|
* These intervals are harmonized across all widgets and notifications
|
||||||
|
* to prevent redundant API calls and ensure consistent user experience.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const REFRESH_INTERVALS = {
|
||||||
|
// High priority - real-time updates
|
||||||
|
NOTIFICATIONS: 30000, // 30 seconds (reduced from 60s for better UX)
|
||||||
|
NOTIFICATIONS_COUNT: 30000, // 30 seconds (synchronized with notifications)
|
||||||
|
PAROLE: 30000, // 30 seconds (chat messages - unchanged)
|
||||||
|
NAVBAR_TIME: 1000, // 1 second (navigation bar time display - real-time)
|
||||||
|
|
||||||
|
// Medium priority - frequent but not real-time
|
||||||
|
EMAIL: 60000, // 1 minute (was manual only - now auto-refresh)
|
||||||
|
DUTIES: 120000, // 2 minutes (was manual only - now auto-refresh)
|
||||||
|
|
||||||
|
// Low priority - less frequent updates
|
||||||
|
CALENDAR: 300000, // 5 minutes (unchanged - calendar events don't change often)
|
||||||
|
NEWS: 600000, // 10 minutes (was manual only - now auto-refresh)
|
||||||
|
|
||||||
|
// Minimum interval between refreshes (prevents spam)
|
||||||
|
MIN_INTERVAL: 1000, // 1 second - minimum time between same resource refreshes
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get refresh interval for a resource
|
||||||
|
*
|
||||||
|
* @param resource - The resource identifier
|
||||||
|
* @returns The refresh interval in milliseconds
|
||||||
|
*/
|
||||||
|
export function getRefreshInterval(resource: string): number {
|
||||||
|
switch (resource) {
|
||||||
|
case 'notifications':
|
||||||
|
return REFRESH_INTERVALS.NOTIFICATIONS;
|
||||||
|
case 'notifications-count':
|
||||||
|
return REFRESH_INTERVALS.NOTIFICATIONS_COUNT;
|
||||||
|
case 'parole':
|
||||||
|
return REFRESH_INTERVALS.PAROLE;
|
||||||
|
case 'navbar-time':
|
||||||
|
return REFRESH_INTERVALS.NAVBAR_TIME;
|
||||||
|
case 'email':
|
||||||
|
return REFRESH_INTERVALS.EMAIL;
|
||||||
|
case 'duties':
|
||||||
|
return REFRESH_INTERVALS.DUTIES;
|
||||||
|
case 'calendar':
|
||||||
|
return REFRESH_INTERVALS.CALENDAR;
|
||||||
|
case 'news':
|
||||||
|
return REFRESH_INTERVALS.NEWS;
|
||||||
|
default:
|
||||||
|
return 60000; // Default: 1 minute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert interval to human-readable format
|
||||||
|
*/
|
||||||
|
export function formatInterval(ms: number): string {
|
||||||
|
if (ms < 1000) return `${ms}ms`;
|
||||||
|
if (ms < 60000) return `${Math.floor(ms / 1000)}s`;
|
||||||
|
if (ms < 3600000) return `${Math.floor(ms / 60000)}m`;
|
||||||
|
return `${Math.floor(ms / 3600000)}h`;
|
||||||
|
}
|
||||||
@ -289,11 +289,22 @@ export class NotificationService {
|
|||||||
// Delete count cache
|
// Delete count cache
|
||||||
await redis.del(countKey);
|
await redis.del(countKey);
|
||||||
|
|
||||||
// Find and delete list caches
|
// Find and delete list caches using SCAN to avoid blocking Redis
|
||||||
const listKeys = await redis.keys(listKeysPattern);
|
let cursor = "0";
|
||||||
if (listKeys.length > 0) {
|
do {
|
||||||
await redis.del(...listKeys);
|
const [nextCursor, keys] = await redis.scan(
|
||||||
}
|
cursor,
|
||||||
|
"MATCH",
|
||||||
|
listKeysPattern,
|
||||||
|
"COUNT",
|
||||||
|
100
|
||||||
|
);
|
||||||
|
cursor = nextCursor;
|
||||||
|
|
||||||
|
if (keys.length > 0) {
|
||||||
|
await redis.del(...keys);
|
||||||
|
}
|
||||||
|
} while (cursor !== "0");
|
||||||
|
|
||||||
console.log(`[NOTIFICATION_SERVICE] Invalidated notification caches for user ${userId}`);
|
console.log(`[NOTIFICATION_SERVICE] Invalidated notification caches for user ${userId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
260
lib/services/refresh-manager.ts
Normal file
260
lib/services/refresh-manager.ts
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
/**
|
||||||
|
* Unified Refresh Manager
|
||||||
|
*
|
||||||
|
* Centralizes all refresh logic across widgets and notifications.
|
||||||
|
* Prevents duplicate refreshes, manages intervals, and provides
|
||||||
|
* a single source of truth for refresh coordination.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type RefreshableResource =
|
||||||
|
| 'notifications'
|
||||||
|
| 'notifications-count'
|
||||||
|
| 'calendar'
|
||||||
|
| 'news'
|
||||||
|
| 'email'
|
||||||
|
| 'parole'
|
||||||
|
| 'duties'
|
||||||
|
| 'navbar-time';
|
||||||
|
|
||||||
|
export interface RefreshConfig {
|
||||||
|
resource: RefreshableResource;
|
||||||
|
interval: number; // milliseconds
|
||||||
|
enabled: boolean;
|
||||||
|
priority: 'high' | 'medium' | 'low';
|
||||||
|
onRefresh: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RefreshManager {
|
||||||
|
private intervals: Map<RefreshableResource, NodeJS.Timeout> = new Map();
|
||||||
|
private configs: Map<RefreshableResource, RefreshConfig> = new Map();
|
||||||
|
private pendingRequests: Map<string, Promise<any>> = new Map();
|
||||||
|
private lastRefresh: Map<RefreshableResource, number> = new Map();
|
||||||
|
private isActive = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a refreshable resource
|
||||||
|
*/
|
||||||
|
register(config: RefreshConfig): void {
|
||||||
|
console.log(`[RefreshManager] Registering resource: ${config.resource} (interval: ${config.interval}ms)`);
|
||||||
|
|
||||||
|
this.configs.set(config.resource, config);
|
||||||
|
|
||||||
|
if (config.enabled && this.isActive) {
|
||||||
|
this.startRefresh(config.resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister a resource
|
||||||
|
*/
|
||||||
|
unregister(resource: RefreshableResource): void {
|
||||||
|
console.log(`[RefreshManager] Unregistering resource: ${resource}`);
|
||||||
|
|
||||||
|
this.stopRefresh(resource);
|
||||||
|
this.configs.delete(resource);
|
||||||
|
this.lastRefresh.delete(resource);
|
||||||
|
|
||||||
|
// Clean up pending request
|
||||||
|
const pendingKey = `${resource}-pending`;
|
||||||
|
this.pendingRequests.delete(pendingKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start all refresh intervals
|
||||||
|
*/
|
||||||
|
start(): void {
|
||||||
|
if (this.isActive) {
|
||||||
|
console.log('[RefreshManager] Already active');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[RefreshManager] Starting refresh manager');
|
||||||
|
this.isActive = true;
|
||||||
|
|
||||||
|
// Start all enabled resources
|
||||||
|
this.configs.forEach((config, resource) => {
|
||||||
|
if (config.enabled) {
|
||||||
|
this.startRefresh(resource);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop all refresh intervals
|
||||||
|
*/
|
||||||
|
stop(): void {
|
||||||
|
if (!this.isActive) {
|
||||||
|
console.log('[RefreshManager] Already stopped');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[RefreshManager] Stopping refresh manager');
|
||||||
|
this.isActive = false;
|
||||||
|
|
||||||
|
// Clear all intervals
|
||||||
|
this.intervals.forEach((interval, resource) => {
|
||||||
|
console.log(`[RefreshManager] Stopping refresh for: ${resource}`);
|
||||||
|
clearInterval(interval);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.intervals.clear();
|
||||||
|
|
||||||
|
// Clear pending requests
|
||||||
|
this.pendingRequests.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start refresh for a specific resource
|
||||||
|
*/
|
||||||
|
private startRefresh(resource: RefreshableResource): void {
|
||||||
|
// Stop existing interval if any
|
||||||
|
this.stopRefresh(resource);
|
||||||
|
|
||||||
|
const config = this.configs.get(resource);
|
||||||
|
if (!config || !config.enabled) {
|
||||||
|
console.log(`[RefreshManager] Cannot start refresh for ${resource}: not configured or disabled`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[RefreshManager] Starting refresh for ${resource} (interval: ${config.interval}ms)`);
|
||||||
|
|
||||||
|
// Initial refresh
|
||||||
|
this.executeRefresh(resource);
|
||||||
|
|
||||||
|
// Set up interval
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
this.executeRefresh(resource);
|
||||||
|
}, config.interval);
|
||||||
|
|
||||||
|
this.intervals.set(resource, interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop refresh for a specific resource
|
||||||
|
*/
|
||||||
|
private stopRefresh(resource: RefreshableResource): void {
|
||||||
|
const interval = this.intervals.get(resource);
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
this.intervals.delete(resource);
|
||||||
|
console.log(`[RefreshManager] Stopped refresh for: ${resource}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute refresh with deduplication
|
||||||
|
*/
|
||||||
|
private async executeRefresh(resource: RefreshableResource): Promise<void> {
|
||||||
|
const config = this.configs.get(resource);
|
||||||
|
if (!config) {
|
||||||
|
console.warn(`[RefreshManager] No config found for resource: ${resource}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const lastRefreshTime = this.lastRefresh.get(resource) || 0;
|
||||||
|
|
||||||
|
// Prevent too frequent refreshes (minimum 1 second between same resource)
|
||||||
|
if (now - lastRefreshTime < 1000) {
|
||||||
|
console.log(`[RefreshManager] Skipping ${resource} - too soon (${now - lastRefreshTime}ms ago)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's already a pending request for this resource
|
||||||
|
const pendingKey = `${resource}-pending`;
|
||||||
|
if (this.pendingRequests.has(pendingKey)) {
|
||||||
|
console.log(`[RefreshManager] Deduplicating ${resource} request - already pending`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and track the request
|
||||||
|
console.log(`[RefreshManager] Executing refresh for: ${resource}`);
|
||||||
|
const refreshPromise = config.onRefresh()
|
||||||
|
.then(() => {
|
||||||
|
this.lastRefresh.set(resource, Date.now());
|
||||||
|
console.log(`[RefreshManager] Successfully refreshed: ${resource}`);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(`[RefreshManager] Error refreshing ${resource}:`, error);
|
||||||
|
// Don't update lastRefresh on error to allow retry
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.pendingRequests.delete(pendingKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pendingRequests.set(pendingKey, refreshPromise);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await refreshPromise;
|
||||||
|
} catch (error) {
|
||||||
|
// Error already logged above
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually trigger refresh for a resource
|
||||||
|
*/
|
||||||
|
async refresh(resource: RefreshableResource, force = false): Promise<void> {
|
||||||
|
const config = this.configs.get(resource);
|
||||||
|
if (!config) {
|
||||||
|
throw new Error(`Resource ${resource} not registered`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[RefreshManager] Manual refresh requested for: ${resource} (force: ${force})`);
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
// Force refresh: clear last refresh time and pending request
|
||||||
|
this.lastRefresh.delete(resource);
|
||||||
|
const pendingKey = `${resource}-pending`;
|
||||||
|
this.pendingRequests.delete(pendingKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.executeRefresh(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get refresh status
|
||||||
|
*/
|
||||||
|
getStatus(): {
|
||||||
|
active: boolean;
|
||||||
|
resources: Array<{
|
||||||
|
resource: RefreshableResource;
|
||||||
|
enabled: boolean;
|
||||||
|
lastRefresh: number | null;
|
||||||
|
interval: number;
|
||||||
|
isRunning: boolean;
|
||||||
|
}>;
|
||||||
|
} {
|
||||||
|
const resources = Array.from(this.configs.entries()).map(([resource, config]) => ({
|
||||||
|
resource,
|
||||||
|
enabled: config.enabled,
|
||||||
|
lastRefresh: this.lastRefresh.get(resource) || null,
|
||||||
|
interval: config.interval,
|
||||||
|
isRunning: this.intervals.has(resource),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
active: this.isActive,
|
||||||
|
resources,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause all refreshes (temporary stop)
|
||||||
|
*/
|
||||||
|
pause(): void {
|
||||||
|
console.log('[RefreshManager] Pausing all refreshes');
|
||||||
|
this.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resume all refreshes
|
||||||
|
*/
|
||||||
|
resume(): void {
|
||||||
|
console.log('[RefreshManager] Resuming all refreshes');
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton instance
|
||||||
|
export const refreshManager = new RefreshManager();
|
||||||
104
lib/utils/request-deduplication.ts
Normal file
104
lib/utils/request-deduplication.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
/**
|
||||||
|
* Request Deduplication Utility
|
||||||
|
*
|
||||||
|
* Prevents duplicate API calls for the same resource within a time window.
|
||||||
|
* This significantly reduces server load and improves performance.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface PendingRequest<T> {
|
||||||
|
promise: Promise<T>;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RequestDeduplicator {
|
||||||
|
private pendingRequests = new Map<string, PendingRequest<any>>();
|
||||||
|
private readonly DEFAULT_TTL = 5000; // 5 seconds default TTL
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a request with deduplication
|
||||||
|
*
|
||||||
|
* If a request with the same key is already pending and within TTL,
|
||||||
|
* the existing promise is returned instead of making a new request.
|
||||||
|
*
|
||||||
|
* @param key - Unique identifier for the request
|
||||||
|
* @param requestFn - Function that returns a promise for the request
|
||||||
|
* @param ttl - Time-to-live in milliseconds (default: 5000ms)
|
||||||
|
* @returns Promise that resolves with the request result
|
||||||
|
*/
|
||||||
|
async execute<T>(
|
||||||
|
key: string,
|
||||||
|
requestFn: () => Promise<T>,
|
||||||
|
ttl: number = this.DEFAULT_TTL
|
||||||
|
): Promise<T> {
|
||||||
|
// Check if there's a pending request
|
||||||
|
const pending = this.pendingRequests.get(key);
|
||||||
|
|
||||||
|
if (pending) {
|
||||||
|
const age = Date.now() - pending.timestamp;
|
||||||
|
|
||||||
|
// If request is still fresh, reuse it
|
||||||
|
if (age < ttl) {
|
||||||
|
console.log(`[RequestDeduplicator] Reusing pending request: ${key} (age: ${age}ms)`);
|
||||||
|
return pending.promise;
|
||||||
|
} else {
|
||||||
|
// Request is stale, remove it
|
||||||
|
console.log(`[RequestDeduplicator] Stale request removed: ${key} (age: ${age}ms)`);
|
||||||
|
this.pendingRequests.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new request
|
||||||
|
console.log(`[RequestDeduplicator] Creating new request: ${key}`);
|
||||||
|
const promise = requestFn()
|
||||||
|
.finally(() => {
|
||||||
|
// Clean up after request completes
|
||||||
|
this.pendingRequests.delete(key);
|
||||||
|
console.log(`[RequestDeduplicator] Request completed and cleaned up: ${key}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pendingRequests.set(key, {
|
||||||
|
promise,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel a pending request
|
||||||
|
*
|
||||||
|
* @param key - The request key to cancel
|
||||||
|
*/
|
||||||
|
cancel(key: string): void {
|
||||||
|
if (this.pendingRequests.has(key)) {
|
||||||
|
this.pendingRequests.delete(key);
|
||||||
|
console.log(`[RequestDeduplicator] Request cancelled: ${key}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all pending requests
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
const count = this.pendingRequests.size;
|
||||||
|
this.pendingRequests.clear();
|
||||||
|
console.log(`[RequestDeduplicator] Cleared ${count} pending requests`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get count of pending requests
|
||||||
|
*/
|
||||||
|
getPendingCount(): number {
|
||||||
|
return this.pendingRequests.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all pending request keys (for debugging)
|
||||||
|
*/
|
||||||
|
getPendingKeys(): string[] {
|
||||||
|
return Array.from(this.pendingRequests.keys());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton instance
|
||||||
|
export const requestDeduplicator = new RequestDeduplicator();
|
||||||
Loading…
Reference in New Issue
Block a user