From 18a68367bc24e7287949c3e91c5cfb66988592a1 Mon Sep 17 00:00:00 2001 From: elpatron Date: Tue, 2 Jun 2026 19:17:36 +0200 Subject: [PATCH] fix: resolve PWA freeze caused by infinite microtask loop in sync.ts and hung fetches without timeout --- client/src/services/api.ts | 37 +++++++++++++++++++++++++++++-------- client/src/services/sync.ts | 11 ++++++----- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/client/src/services/api.ts b/client/src/services/api.ts index f3532a3..2598d33 100644 --- a/client/src/services/api.ts +++ b/client/src/services/api.ts @@ -10,22 +10,43 @@ export class ApiError extends Error { export async function apiFetch( input: string, - init: RequestInit = {} + init: RequestInit = {}, + timeoutMs = 15000 ): Promise { const headers = new Headers(init.headers) if (init.body !== undefined && !headers.has('Content-Type')) { headers.set('Content-Type', 'application/json') } - return fetch(input, { - ...init, - headers, - credentials: 'include' - }) + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), timeoutMs) + + if (init.signal) { + if (init.signal.aborted) { + controller.abort() + } else { + init.signal.addEventListener('abort', () => controller.abort()) + } + } + + try { + return await fetch(input, { + ...init, + headers, + credentials: 'include', + signal: controller.signal + }) + } finally { + clearTimeout(timeoutId) + } } -export async function apiJson(input: string, init: RequestInit = {}): Promise { - const res = await apiFetch(input, init) +export async function apiJson( + input: string, + init: RequestInit = {}, + timeoutMs = 15000 +): Promise { + const res = await apiFetch(input, init, timeoutMs) const data = await res.json().catch(() => ({})) if (!res.ok) { const message = diff --git a/client/src/services/sync.ts b/client/src/services/sync.ts index 1389325..6a086a5 100644 --- a/client/src/services/sync.ts +++ b/client/src/services/sync.ts @@ -131,12 +131,7 @@ async function coalesceSyncQueue(logbookId: string): Promise { } function scheduleResync(logbookId: string) { - if (pendingResync.has(logbookId)) return pendingResync.add(logbookId) - queueMicrotask(() => { - pendingResync.delete(logbookId) - syncLogbook(logbookId).catch((err) => console.warn('Deferred sync failed:', err)) - }) } type LogbookPushAccess = 'OWNER' | 'WRITE' | 'READ' | 'UNKNOWN' @@ -540,6 +535,12 @@ export async function syncLogbook(logbookId: string): Promise { } finally { syncingLogbooks.delete(logbookId) recomputeSyncingState() + if (pendingResync.has(logbookId)) { + pendingResync.delete(logbookId) + setTimeout(() => { + syncLogbook(logbookId).catch((err) => console.warn('Deferred sync failed:', err)) + }, 1000) + } } }