fix(offline): presence-вызовы не попадают в офлайн-очередь и молча дропаются из конфликтов
Some checks are pending
Deploy to Server / Type Check & Build (push) Waiting to run
Deploy to Server / Build & Push App Image (push) Blocked by required conditions
Deploy to Server / Deploy to VPS (push) Blocked by required conditions

В PWA накапливались десятки конфликтов «/api/devices/active-chat»: эфемерные
presence-вызовы (активный чат, heartbeat) при сетевых сбоях ставились в очередь,
при реплее ловили 4xx и становились конфликтами, требующими ручной разборки.
Реплеить их бессмысленно — они устаревают мгновенно.
- isQueueable: /api/devices/active-chat и /api/devices/heartbeat больше не
  ставятся в очередь (как read receipts);
- isSafeToDrop: те же URL молча дропаются при 4xx;
- уже помеченные конфликтами presence-записи авто-синк дропает молча —
  накопленные десятки очистятся при следующей синхронизации.
This commit is contained in:
2026-10-01 11:50:57 +03:00
parent 8eff527259
commit b3f4529376
2 changed files with 17 additions and 3 deletions

View File

@@ -113,7 +113,11 @@ async function replayRequest(item: OfflineQueueItem): Promise<Response> {
}
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;
}

View File

@@ -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));
}