From c64101b12a4800ce9e24416af6ed2f91b50fa654 Mon Sep 17 00:00:00 2001 From: jubnl Date: Tue, 5 May 2026 22:47:37 +0200 Subject: [PATCH] fix: prevent IDB write-stall from blocking trip page and sync loop clearAll() now clears all tables in a transaction instead of calling offlineDb.delete(), which triggered our versionchange handler and put Dexie into a broken write state for the rest of the session. tripRepo.get() gets the same 2 s timeout guard as list() so a stalled IDB read no longer freezes the trip splash screen. _doSync wraps each syncTrip() in a 30 s per-trip timeout so a single stalled write transaction cannot prevent the loop from advancing to subsequent trips. --- client/src/db/offlineDb.ts | 45 ++++++++++++++++++++++++++++-- client/src/repo/tripRepo.ts | 5 +++- client/src/sync/tripSyncManager.ts | 7 ++++- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/client/src/db/offlineDb.ts b/client/src/db/offlineDb.ts index 21da8a7c..5c0ad9b7 100644 --- a/client/src/db/offlineDb.ts +++ b/client/src/db/offlineDb.ts @@ -199,7 +199,46 @@ export async function clearBlobCache(): Promise { /** Wipe the entire offline database (called on logout). */ export async function clearAll(): Promise { - await offlineDb.delete(); - // Re-open so subsequent operations don't fail - await offlineDb.open(); + // Use table.clear() instead of offlineDb.delete() to avoid triggering the + // versionchange handler (which calls close()), which would put Dexie into a + // broken write state for the remainder of the session. + await offlineDb.transaction( + 'rw', + [ + offlineDb.trips, + offlineDb.days, + offlineDb.places, + offlineDb.packingItems, + offlineDb.todoItems, + offlineDb.budgetItems, + offlineDb.reservations, + offlineDb.tripFiles, + offlineDb.accommodations, + offlineDb.tripMembers, + offlineDb.tags, + offlineDb.categories, + offlineDb.mutationQueue, + offlineDb.syncMeta, + offlineDb.blobCache, + ], + async () => { + await Promise.all([ + offlineDb.trips.clear(), + offlineDb.days.clear(), + offlineDb.places.clear(), + offlineDb.packingItems.clear(), + offlineDb.todoItems.clear(), + offlineDb.budgetItems.clear(), + offlineDb.reservations.clear(), + offlineDb.tripFiles.clear(), + offlineDb.accommodations.clear(), + offlineDb.tripMembers.clear(), + offlineDb.tags.clear(), + offlineDb.categories.clear(), + offlineDb.mutationQueue.clear(), + offlineDb.syncMeta.clear(), + offlineDb.blobCache.clear(), + ]) + }, + ) } diff --git a/client/src/repo/tripRepo.ts b/client/src/repo/tripRepo.ts index 2726305e..53dcd0be 100644 --- a/client/src/repo/tripRepo.ts +++ b/client/src/repo/tripRepo.ts @@ -48,7 +48,10 @@ export const tripRepo = { }, async get(tripId: number | string): Promise<{ trip: Trip; refresh: TripRefresh }> { - const cached = await offlineDb.trips.get(Number(tripId)) + const cached = await Promise.race([ + offlineDb.trips.get(Number(tripId)).catch(() => undefined), + new Promise(resolve => setTimeout(() => resolve(undefined), 2000)), + ]) const refresh: TripRefresh = (async () => { try { diff --git a/client/src/sync/tripSyncManager.ts b/client/src/sync/tripSyncManager.ts index d755badd..18c441fd 100644 --- a/client/src/sync/tripSyncManager.ts +++ b/client/src/sync/tripSyncManager.ts @@ -195,7 +195,12 @@ export const tripSyncManager = { onProgress?.({ phase: 'trip', tripId: trip.id, index: i, total: toSync.length }) let tripOk = false try { - await syncTrip(trip.id) + await Promise.race([ + syncTrip(trip.id), + new Promise((_, reject) => + setTimeout(() => reject(new Error('syncTrip timeout')), 30_000) + ), + ]) tripOk = true } catch (err) { if (isQuotaError(err)) {