From 935d91196b30c2c36c1dbd78fefd03a99d99e4cc Mon Sep 17 00:00:00 2001 From: jubnl Date: Tue, 5 May 2026 21:57:17 +0200 Subject: [PATCH] fix: add versionchange handler to close stale Dexie connection on external IDB delete All repo mutations (places, budget, packing, todo, accommodation, reservations, files) and tripSyncManager's syncTrip() use awaited Dexie writes that would stall indefinitely under the same root cause as the dashboard cold-path hang: Dexie keeping a stale connection after DevTools "Clear site data" fires a versionchange event while the tab is open. Registering an explicit versionchange handler that calls close() lets Dexie cleanly discard the stale connection. The next operation triggers auto-reopen with a fresh IDB connection where writes succeed. This is the standard Dexie pattern and prevents the stall from affecting any part of the app. Also tighten the toArray() guard in tripRepo.list() to catch() a rejection (from a potential close() race) in addition to timing out. --- client/src/db/offlineDb.ts | 7 +++++++ client/src/repo/tripRepo.ts | 7 ++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/client/src/db/offlineDb.ts b/client/src/db/offlineDb.ts index 0726eb27..21da8a7c 100644 --- a/client/src/db/offlineDb.ts +++ b/client/src/db/offlineDb.ts @@ -68,6 +68,13 @@ class TrekOfflineDb extends Dexie { constructor() { super('trek-offline'); + // When the database is deleted externally (e.g. DevTools "Clear site data" + // while the tab is open), IDB fires versionchange on the open connection. + // Without an explicit close() here, Dexie keeps the stale connection alive + // and subsequent write transactions queue behind it indefinitely. Closing + // forces Dexie to auto-reopen on the next operation with a fresh connection. + this.on('versionchange', () => { this.close() }) + this.version(1).stores({ trips: 'id', days: 'id, trip_id', diff --git a/client/src/repo/tripRepo.ts b/client/src/repo/tripRepo.ts index 28d0918e..2726305e 100644 --- a/client/src/repo/tripRepo.ts +++ b/client/src/repo/tripRepo.ts @@ -8,10 +8,11 @@ type TripRefresh = Promise<{ trip: Trip } | null> export const tripRepo = { async list(): Promise<{ trips: Trip[]; archivedTrips: Trip[]; refresh: TripsRefresh }> { - // 2-second guard: if Dexie is in a bad state (e.g. externally deleted while tab - // was open), toArray() may hang. Fall back to the cold/network path. + // Guard: if Dexie is in a bad state (e.g. externally deleted while tab was + // open and the versionchange close() races with this read), fall back to the + // cold/network path rather than throwing or hanging. const all = await Promise.race([ - offlineDb.trips.toArray(), + offlineDb.trips.toArray().catch(() => [] as Trip[]), new Promise(resolve => setTimeout(() => resolve([]), 2000)), ])