fix: update baseline logic in useDocuments and useInbox hooks to accurately track new items and unread counts, addressing timing issues with Electric updates

This commit is contained in:
Anish Sarkar 2026-03-06 20:21:28 +05:30
parent 1a688c7161
commit 3e4db20bcb
3 changed files with 41 additions and 17 deletions

View file

@ -351,11 +351,13 @@ export function useDocuments(
const liveIds = new Set(validItems.map((d) => d.id)); const liveIds = new Set(validItems.map((d) => d.id));
const prevIds = new Set(prev.map((d) => d.id)); const prevIds = new Set(prev.map((d) => d.id));
// First callback: snapshot all Electric IDs as the baseline. // Only baseline items already rendered from API.
// Everything in this set existed before the sidebar opened and // Items in Electric but NOT in prev are genuinely new
// should only appear via API pagination, not Electric. // (created between the API fetch and Electric's first callback).
if (electricBaselineIdsRef.current === null) { if (electricBaselineIdsRef.current === null) {
electricBaselineIdsRef.current = new Set(liveIds); electricBaselineIdsRef.current = new Set(
[...liveIds].filter((id) => prevIds.has(id))
);
} }
// Genuinely new = not in rendered list, not in baseline snapshot. // Genuinely new = not in rendered list, not in baseline snapshot.

View file

@ -203,7 +203,11 @@ export function useInbox(
const prevIds = new Set(prev.map((d) => d.id)); const prevIds = new Set(prev.map((d) => d.id));
if (electricBaselineIdsRef.current === null) { if (electricBaselineIdsRef.current === null) {
electricBaselineIdsRef.current = new Set(liveIds); // Only baseline items already rendered from API.
// Items in Electric but NOT in prev are genuinely new.
electricBaselineIdsRef.current = new Set(
[...liveIds].filter((id) => prevIds.has(id))
);
} }
const baseline = electricBaselineIdsRef.current; const baseline = electricBaselineIdsRef.current;
@ -237,11 +241,35 @@ export function useInbox(
return updated; return updated;
}); });
// Calibrate the older-unread offset using baseline items
// (items present in both Electric and the API-loaded list).
// This avoids the timing bug where new items arriving between
// the API fetch and Electric's first callback would be absorbed
// into the offset, making the count appear unchanged.
const baseline = electricBaselineIdsRef.current;
if (olderUnreadOffsetRef.current === null && baseline !== null) {
const baselineUnreadCount = validItems.filter(
(item) => baseline.has(item.id) && !item.read
).length;
olderUnreadOffsetRef.current = Math.max(
0,
apiUnreadTotalRef.current - baselineUnreadCount
);
}
// Derive unread count from all Electric items + the older offset
if (olderUnreadOffsetRef.current !== null) {
const electricUnreadCount = validItems.filter((item) => !item.read).length;
setUnreadCount(olderUnreadOffsetRef.current + electricUnreadCount);
}
}); });
liveQueryRef.current = liveQuery; liveQueryRef.current = liveQuery;
// Per-instance unread count live query filtered by category types // Per-instance unread count live query filtered by category types.
// Acts as a secondary reactive path for read-status changes that
// may not trigger the items live query in all edge cases.
const countQuery = `SELECT COUNT(*) as count FROM notifications const countQuery = `SELECT COUNT(*) as count FROM notifications
WHERE user_id = $1 WHERE user_id = $1
AND (search_space_id = $2 OR search_space_id IS NULL) AND (search_space_id = $2 OR search_space_id IS NULL)
@ -258,15 +286,8 @@ export function useInbox(
countLiveQuery.subscribe((result: { rows: Array<{ count: number | string }> }) => { countLiveQuery.subscribe((result: { rows: Array<{ count: number | string }> }) => {
if (!mounted || !result.rows?.[0] || !initialLoadDoneRef.current) return; if (!mounted || !result.rows?.[0] || !initialLoadDoneRef.current) return;
if (olderUnreadOffsetRef.current === null) return;
const liveRecentUnread = Number(result.rows[0].count) || 0; const liveRecentUnread = Number(result.rows[0].count) || 0;
if (olderUnreadOffsetRef.current === null) {
olderUnreadOffsetRef.current = Math.max(
0,
apiUnreadTotalRef.current - liveRecentUnread
);
}
setUnreadCount(olderUnreadOffsetRef.current + liveRecentUnread); setUnreadCount(olderUnreadOffsetRef.current + liveRecentUnread);
}); });

View file

@ -71,7 +71,8 @@ const pendingSyncs = new Map<string, Promise<SyncHandle>>();
// real-time documents table with title/created_by_id/status columns, // real-time documents table with title/created_by_id/status columns,
// consolidated single documents sync, pending state for document queue visibility // consolidated single documents sync, pending state for document queue visibility
// v6: added enable_summary column to search_source_connectors // v6: added enable_summary column to search_source_connectors
const SYNC_VERSION = 6; // v7: fixed connector-popup using invalid category for useInbox
const SYNC_VERSION = 7;
// Database name prefix for identifying SurfSense databases // Database name prefix for identifying SurfSense databases
const DB_PREFIX = "surfsense-"; const DB_PREFIX = "surfsense-";