diff --git a/client/src/hooks/useOfflineSync.ts b/client/src/hooks/useOfflineSync.ts index c0d168a..6e26c76 100644 --- a/client/src/hooks/useOfflineSync.ts +++ b/client/src/hooks/useOfflineSync.ts @@ -113,7 +113,11 @@ async function replayRequest(item: OfflineQueueItem): Promise { } function isSafeToDrop(item: OfflineQueueItem): boolean { - return item.method === 'POST' && /\/api\/messages\/\d+\/read$/.test(item.url); + if (item.method === 'POST' && /\/api\/messages\/\d+\/read$/.test(item.url)) return true; + // Эфемерные presence-вызовы, случайно попавшие в очередь до исключения из + // isQueueable (см. queryClient.ts) — реплеить бессмысленно, дропаем молча + if (item.url.includes('/api/devices/active-chat') || item.url.includes('/api/devices/heartbeat')) return true; + return false; } // Module-level lock so only one sync runs at a time across all hook instances. @@ -443,9 +447,16 @@ export function useOfflineSync(userId?: number): UseOfflineSyncReturn { const newConflicts: OfflineQueueItem[] = []; for (const item of itemsToReplay) { - // Items already marked as conflicted need manual resolution — skip during auto-sync + // Items already marked as conflicted need manual resolution — skip during auto-sync. + // Исключение — некритичные эфемерные запросы (presence, read receipts): + // дропаем молча даже из конфликтов, пользователю незачем разбирать их вручную. if (item.conflictStatus === 'conflict') { - newConflicts.push(item); + if (isSafeToDrop(item)) { + await remove(item.id); + window.dispatchEvent(new CustomEvent('offline-queue-updated')); + } else { + newConflicts.push(item); + } continue; } diff --git a/client/src/lib/queryClient.ts b/client/src/lib/queryClient.ts index 0681263..b03abe5 100644 --- a/client/src/lib/queryClient.ts +++ b/client/src/lib/queryClient.ts @@ -62,6 +62,9 @@ function isQueueable(method: string, url: string, data?: unknown): boolean { if (data instanceof FormData) return false; // Read-receipt requests are non-critical — don't queue them offline if (method.toUpperCase() === 'POST' && /\/api\/messages\/\d+\/read$/.test(url)) return false; + // Эфемерные presence-вызовы (активный чат, heartbeat) бессмысленно реплеить — + // они устаревают мгновенно; иначе копились сотнями в конфликтах + if (url.includes('/api/devices/active-chat') || url.includes('/api/devices/heartbeat')) return false; return !OFFLINE_QUEUE_EXCLUDED.some((prefix) => url.includes(prefix)); }