fix: prevent cold-path hang when Dexie write transactions stall after external IDB clear

When DevTools "Clear site data" deletes the IDB while the tab is open, Dexie
receives a versionchange event and closes its connection. On reopen, read
transactions work (toArray completes after ~400ms), but write transactions can
stall indefinitely, causing the cold-path 'await refresh' to never resolve.

Two changes:
- Make upsertTrip calls fire-and-forget in the IIFE so network data is returned
  immediately without blocking on potentially-stuck IDB writes.
- Add a 2-second timeout to the initial offlineDb.trips.toArray() call so that
  if the read also stalls, the cold path falls through to the network fetch.
- Reduce the outer dashboard timeout from 12s to 5s now that the inner path
  cannot stall for more than ~2s + network RTT.
This commit is contained in:
jubnl
2026-05-05 21:49:07 +02:00
parent 37d9a321ab
commit b71ce3dd5e
2 changed files with 13 additions and 7 deletions
+5 -1
View File
@@ -744,7 +744,11 @@ export default function DashboardPage(): React.ReactElement {
const loadTrips = async () => {
setIsLoading(true)
try {
const { trips, archivedTrips, refresh } = await tripRepo.list()
const listOrTimeout = Promise.race([
tripRepo.list(),
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('trips-load-timeout')), 5_000)),
])
const { trips, archivedTrips, refresh } = await listOrTimeout
setTrips(sortTrips(trips))
setArchivedTrips(sortTrips(archivedTrips))
setIsLoading(false)
+8 -6
View File
@@ -8,7 +8,12 @@ type TripRefresh = Promise<{ trip: Trip } | null>
export const tripRepo = {
async list(): Promise<{ trips: Trip[]; archivedTrips: Trip[]; refresh: TripsRefresh }> {
const all = await offlineDb.trips.toArray()
// 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.
const all = await Promise.race([
offlineDb.trips.toArray(),
new Promise<Trip[]>(resolve => setTimeout(() => resolve([]), 2000)),
])
const refresh: TripsRefresh = (async () => {
try {
@@ -16,6 +21,8 @@ export const tripRepo = {
tripsApi.list(),
tripsApi.list({ archived: 1 }),
])
// Fire-and-forget IDB writes: returning data immediately unblocks the cold
// path even when Dexie write transactions stall after an external DB clear.
Promise.all([
...active.trips.map(t => upsertTrip(t)),
...archived.trips.map(t => upsertTrip(t)),
@@ -36,11 +43,6 @@ export const tripRepo = {
const fresh = await refresh
if (!fresh) return { trips: [], archivedTrips: [], refresh: Promise.resolve(null) }
// Await upserts on cold path so next mount reads from IDB instead of hitting network again
await Promise.all([
...fresh.trips.map(t => upsertTrip(t)),
...fresh.archivedTrips.map(t => upsertTrip(t)),
]).catch(() => {})
return { ...fresh, refresh: Promise.resolve(null) }
},