Refactor Notification
This commit is contained in:
parent
53a4164561
commit
ddacd8d6e3
140
MARK_ALL_READ_CACHE_ISSUE.md
Normal file
140
MARK_ALL_READ_CACHE_ISSUE.md
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
# Mark All As Read - Cache Issue Analysis
|
||||||
|
|
||||||
|
**Date**: 2026-01-01
|
||||||
|
**Issue**: After marking all as read, list is empty but count still shows 66
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Problem Analysis
|
||||||
|
|
||||||
|
### Current Flow
|
||||||
|
|
||||||
|
1. **User clicks "Mark all as read"**
|
||||||
|
2. **`markAllAsRead()` is called**
|
||||||
|
3. **Fetches notifications**: `this.getNotifications(userId, 1, 1000)`
|
||||||
|
- ⚠️ **PROBLEM**: This goes through `NotificationService.getNotifications()`
|
||||||
|
- ⚠️ **PROBLEM**: Which uses **CACHED** data if available
|
||||||
|
- ⚠️ **PROBLEM**: Cached notifications still have `isRead: false`
|
||||||
|
4. **Filters unread**: Gets 66 unread from cached data
|
||||||
|
5. **Marks each as read**: Calls Leantime API for each
|
||||||
|
6. **Invalidates cache**: After marking completes
|
||||||
|
7. **Count is fetched**: But might use stale cache or be fetched before invalidation
|
||||||
|
|
||||||
|
### The Issue
|
||||||
|
|
||||||
|
**Cache Race Condition**:
|
||||||
|
- `markAllAsRead` uses cached notifications (which are stale)
|
||||||
|
- Marks them as read in Leantime
|
||||||
|
- Invalidates cache
|
||||||
|
- But count might be fetched from cache **before** invalidation completes
|
||||||
|
- Or count cache might not be properly invalidated
|
||||||
|
|
||||||
|
**Why List is Empty**:
|
||||||
|
- After marking, all notifications are read
|
||||||
|
- List might filter to show only unread
|
||||||
|
- So list is empty (correct behavior)
|
||||||
|
- But count still shows 66 (stale cache)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Root Causes
|
||||||
|
|
||||||
|
### 1. Using Cached Data in `markAllAsRead`
|
||||||
|
|
||||||
|
**Current Code**:
|
||||||
|
```typescript
|
||||||
|
// In markAllAsRead
|
||||||
|
const allNotifications = await this.getNotifications(userId, 1, 1000);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem**: `getNotifications()` uses cache, so we're working with stale data.
|
||||||
|
|
||||||
|
**Solution**: Fetch directly from Leantime API, bypassing cache.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Cache Invalidation Timing
|
||||||
|
|
||||||
|
**Current Flow**:
|
||||||
|
1. Mark all as read (uses cached data)
|
||||||
|
2. Invalidate cache
|
||||||
|
3. Count is fetched (might use stale cache if fetched too soon)
|
||||||
|
|
||||||
|
**Problem**: Race condition between invalidation and count fetch.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Invalidate cache **before** marking (or fetch fresh data)
|
||||||
|
- Force immediate count refresh after marking
|
||||||
|
- Add delay before count fetch to ensure cache is cleared
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Count Cache Not Properly Invalidated
|
||||||
|
|
||||||
|
**Current Code**:
|
||||||
|
```typescript
|
||||||
|
if (success) {
|
||||||
|
await this.invalidateCache(userId);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem**: If `markAllAsRead` fails partially, cache might not be invalidated.
|
||||||
|
|
||||||
|
**Solution**: Always invalidate cache, even on partial success.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Recommended Fixes
|
||||||
|
|
||||||
|
### Fix 1: Bypass Cache in `markAllAsRead`
|
||||||
|
|
||||||
|
**Change**: Fetch notifications directly from Leantime API, not through cached service.
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
- Add a method to fetch notifications directly from adapter (bypassing cache)
|
||||||
|
- Or add a `forceRefresh` parameter to `getNotifications`
|
||||||
|
- Or fetch directly in `markAllAsRead` using Leantime API
|
||||||
|
|
||||||
|
### Fix 2: Always Invalidate Cache
|
||||||
|
|
||||||
|
**Change**: Invalidate cache even if some notifications fail to mark.
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
- Invalidate cache if **any** notifications were successfully marked
|
||||||
|
- Not just if **all** succeeded
|
||||||
|
|
||||||
|
### Fix 3: Force Fresh Count After Marking
|
||||||
|
|
||||||
|
**Change**: After marking, force an immediate fresh count fetch.
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
- After `markAllAsRead` completes, immediately call `getNotificationCount()` with cache bypass
|
||||||
|
- Or add a delay before count fetch to ensure cache is cleared
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Expected Behavior After Fixes
|
||||||
|
|
||||||
|
### After Mark All As Read
|
||||||
|
|
||||||
|
**Before**:
|
||||||
|
- List: Empty (all read) ✅
|
||||||
|
- Count: 66 (stale cache) ❌
|
||||||
|
|
||||||
|
**After**:
|
||||||
|
- List: Empty (all read) ✅
|
||||||
|
- Count: 0 (fresh data) ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Next Steps
|
||||||
|
|
||||||
|
1. **Fix cache usage in `markAllAsRead`**: Fetch fresh data, not cached
|
||||||
|
2. **Improve cache invalidation**: Always invalidate, even on partial success
|
||||||
|
3. **Force count refresh**: Immediately fetch fresh count after marking
|
||||||
|
4. **Test**: Verify count updates correctly after marking
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status**: Analysis complete. Ready to implement fixes.
|
||||||
|
|
||||||
@ -266,14 +266,53 @@ export class LeantimeAdapter implements NotificationAdapter {
|
|||||||
console.log(`[LEANTIME_ADAPTER] markAllAsRead - Leantime user ID: ${leantimeUserId}`);
|
console.log(`[LEANTIME_ADAPTER] markAllAsRead - Leantime user ID: ${leantimeUserId}`);
|
||||||
|
|
||||||
// Leantime doesn't have a "mark all as read" method, so we need to:
|
// Leantime doesn't have a "mark all as read" method, so we need to:
|
||||||
// 1. Fetch all unread notifications
|
// 1. Fetch all unread notifications directly from API (bypassing any cache)
|
||||||
// 2. Mark each one individually using markNotificationRead
|
// 2. Mark each one individually using markNotificationRead
|
||||||
|
|
||||||
console.log(`[LEANTIME_ADAPTER] markAllAsRead - Fetching all unread notifications`);
|
console.log(`[LEANTIME_ADAPTER] markAllAsRead - Fetching all unread notifications directly from API`);
|
||||||
const allNotifications = await this.getNotifications(userId, 1, 1000); // Get up to 1000 notifications
|
|
||||||
const unreadNotifications = allNotifications.filter(n => !n.isRead);
|
|
||||||
|
|
||||||
console.log(`[LEANTIME_ADAPTER] markAllAsRead - Found ${unreadNotifications.length} unread notifications to mark`);
|
// Fetch all notifications directly from API (up to 1000) to get fresh data (not cached)
|
||||||
|
const jsonRpcBody = {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
method: 'leantime.rpc.Notifications.Notifications.getAllNotifications',
|
||||||
|
params: {
|
||||||
|
userId: leantimeUserId,
|
||||||
|
showNewOnly: 0, // Get all, not just unread
|
||||||
|
limitStart: 0,
|
||||||
|
limitEnd: 1000,
|
||||||
|
filterOptions: []
|
||||||
|
},
|
||||||
|
id: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`[LEANTIME_ADAPTER] markAllAsRead - Fetching notifications from API`);
|
||||||
|
const fetchResponse = await fetch(`${this.apiUrl}/api/jsonrpc`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-API-Key': this.apiToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify(jsonRpcBody)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fetchResponse.ok) {
|
||||||
|
console.error(`[LEANTIME_ADAPTER] markAllAsRead - Failed to fetch notifications: HTTP ${fetchResponse.status}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchData = await fetchResponse.json();
|
||||||
|
if (fetchData.error) {
|
||||||
|
console.error(`[LEANTIME_ADAPTER] markAllAsRead - Error fetching notifications:`, fetchData.error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform the raw Leantime notifications to our format
|
||||||
|
const rawNotifications = Array.isArray(fetchData.result) ? fetchData.result : [];
|
||||||
|
const unreadNotifications = rawNotifications
|
||||||
|
.filter((n: any) => n.read === 0 || n.read === false || n.read === '0')
|
||||||
|
.map((n: any) => ({ id: n.id, sourceId: String(n.id) }));
|
||||||
|
|
||||||
|
console.log(`[LEANTIME_ADAPTER] markAllAsRead - Found ${unreadNotifications.length} unread notifications to mark (from ${rawNotifications.length} total)`);
|
||||||
|
|
||||||
if (unreadNotifications.length === 0) {
|
if (unreadNotifications.length === 0) {
|
||||||
console.log(`[LEANTIME_ADAPTER] markAllAsRead - No unread notifications, returning success`);
|
console.log(`[LEANTIME_ADAPTER] markAllAsRead - No unread notifications, returning success`);
|
||||||
@ -282,9 +321,8 @@ export class LeantimeAdapter implements NotificationAdapter {
|
|||||||
|
|
||||||
// Mark each notification as read
|
// Mark each notification as read
|
||||||
const markPromises = unreadNotifications.map(async (notification) => {
|
const markPromises = unreadNotifications.map(async (notification) => {
|
||||||
// Extract the numeric ID from our compound ID (format: "leantime-123")
|
// Use the ID directly (we already have it from the API response)
|
||||||
const sourceId = notification.sourceId;
|
const notificationId = typeof notification.id === 'number' ? notification.id : parseInt(String(notification.id || notification.sourceId));
|
||||||
const notificationId = parseInt(sourceId);
|
|
||||||
|
|
||||||
if (isNaN(notificationId)) {
|
if (isNaN(notificationId)) {
|
||||||
console.error(`[LEANTIME_ADAPTER] markAllAsRead - Invalid notification ID: ${sourceId}`);
|
console.error(`[LEANTIME_ADAPTER] markAllAsRead - Invalid notification ID: ${sourceId}`);
|
||||||
|
|||||||
@ -287,14 +287,17 @@ export class NotificationService {
|
|||||||
console.log(`[NOTIFICATION_SERVICE] markAllAsRead results:`, results);
|
console.log(`[NOTIFICATION_SERVICE] markAllAsRead results:`, results);
|
||||||
|
|
||||||
const success = results.every(result => result);
|
const success = results.every(result => result);
|
||||||
console.log(`[NOTIFICATION_SERVICE] markAllAsRead overall success: ${success}`);
|
const anySuccess = results.some(result => result);
|
||||||
|
console.log(`[NOTIFICATION_SERVICE] markAllAsRead overall success: ${success}, any success: ${anySuccess}`);
|
||||||
|
|
||||||
if (success) {
|
// Always invalidate cache if any notifications were marked (even partial success)
|
||||||
console.log(`[NOTIFICATION_SERVICE] Invalidating caches for user ${userId}`);
|
// This ensures count and list are refreshed after marking
|
||||||
// Invalidate caches
|
if (anySuccess) {
|
||||||
|
console.log(`[NOTIFICATION_SERVICE] Invalidating caches for user ${userId} (some notifications were marked)`);
|
||||||
|
// Invalidate caches to force fresh data on next fetch
|
||||||
await this.invalidateCache(userId);
|
await this.invalidateCache(userId);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[NOTIFICATION_SERVICE] Not invalidating caches - operation failed`);
|
console.log(`[NOTIFICATION_SERVICE] Not invalidating caches - no notifications were marked`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user