mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-22 06:41:46 +00:00
fix: stale-while-revalidate for offline reads + axios timeout
navigator.onLine is unreliable on Android — returns true whenever any network interface is up, regardless of actual reachability. This caused all repo reads to take the API branch and either wait 5 s for the SW NetworkFirst timeout (cache hit) or hang indefinitely (cache miss). - All read repos (list/get) now return cached IndexedDB data instantly and carry a background refresh promise that resolves to fresh data or null on failure. Callers that opted in (loadTrip, loadTrips) apply fresh data silently when it arrives. - tripStore.loadTrip: Promise.all now reads all 7 resources from IndexedDB (instant), fires network refreshes in background, sets isLoading: false immediately, then applies fresh data via a second Promise.all when ready. Tags/categories use upsertTags/upsertCategories. - DashboardPage.loadTrips: same pattern — renders from cache instantly, silently updates trip list on refresh. - axios timeout set to 8 s so requests can never hang indefinitely. - SW networkTimeoutSeconds lowered from 5 to 2 as defence in depth.
This commit is contained in:
@@ -4,15 +4,25 @@ import { mutationQueue, generateUUID } from '../sync/mutationQueue'
|
||||
import type { Accommodation } from '../types'
|
||||
|
||||
export const accommodationRepo = {
|
||||
async list(tripId: number | string): Promise<{ accommodations: Accommodation[] }> {
|
||||
if (!navigator.onLine) {
|
||||
const accommodations = await offlineDb.accommodations
|
||||
.where('trip_id').equals(Number(tripId)).toArray()
|
||||
return { accommodations }
|
||||
}
|
||||
const result = await accommodationsApi.list(tripId)
|
||||
upsertAccommodations(result.accommodations || []).catch(() => {})
|
||||
return result
|
||||
async list(tripId: number | string): Promise<{ accommodations: Accommodation[]; refresh: Promise<{ accommodations: Accommodation[] } | null> }> {
|
||||
const cached = await offlineDb.accommodations
|
||||
.where('trip_id').equals(Number(tripId)).toArray()
|
||||
|
||||
const refresh = (async () => {
|
||||
try {
|
||||
const result = await accommodationsApi.list(tripId)
|
||||
upsertAccommodations(result.accommodations || []).catch(() => {})
|
||||
return result
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})()
|
||||
|
||||
if (cached.length > 0) return { accommodations: cached, refresh }
|
||||
|
||||
const fresh = await refresh
|
||||
if (!fresh) return { accommodations: [], refresh: Promise.resolve(null) }
|
||||
return { accommodations: fresh.accommodations, refresh: Promise.resolve(fresh) }
|
||||
},
|
||||
|
||||
async create(tripId: number | string, data: Record<string, unknown>): Promise<{ accommodation: Accommodation }> {
|
||||
|
||||
@@ -4,17 +4,27 @@ import { mutationQueue, generateUUID } from '../sync/mutationQueue'
|
||||
import type { BudgetItem } from '../types'
|
||||
|
||||
export const budgetRepo = {
|
||||
async list(tripId: number | string): Promise<{ items: BudgetItem[] }> {
|
||||
if (!navigator.onLine) {
|
||||
const cached = await offlineDb.budgetItems
|
||||
.where('trip_id')
|
||||
.equals(Number(tripId))
|
||||
.toArray()
|
||||
return { items: cached }
|
||||
}
|
||||
const result = await budgetApi.list(tripId)
|
||||
upsertBudgetItems(result.items)
|
||||
return result
|
||||
async list(tripId: number | string): Promise<{ items: BudgetItem[]; refresh: Promise<{ items: BudgetItem[] } | null> }> {
|
||||
const cached = await offlineDb.budgetItems
|
||||
.where('trip_id')
|
||||
.equals(Number(tripId))
|
||||
.toArray()
|
||||
|
||||
const refresh = (async () => {
|
||||
try {
|
||||
const result = await budgetApi.list(tripId)
|
||||
upsertBudgetItems(result.items)
|
||||
return result
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})()
|
||||
|
||||
if (cached.length > 0) return { items: cached, refresh }
|
||||
|
||||
const fresh = await refresh
|
||||
if (!fresh) return { items: [], refresh: Promise.resolve(null) }
|
||||
return { items: fresh.items, refresh: Promise.resolve(fresh) }
|
||||
},
|
||||
|
||||
async create(tripId: number | string, data: Record<string, unknown>): Promise<{ item: BudgetItem }> {
|
||||
|
||||
+21
-11
@@ -4,17 +4,27 @@ import { mutationQueue, generateUUID } from '../sync/mutationQueue'
|
||||
import type { Day } from '../types'
|
||||
|
||||
export const dayRepo = {
|
||||
async list(tripId: number | string): Promise<{ days: Day[] }> {
|
||||
if (!navigator.onLine) {
|
||||
const cached = await offlineDb.days
|
||||
.where('trip_id')
|
||||
.equals(Number(tripId))
|
||||
.sortBy('day_number' as keyof Day)
|
||||
return { days: cached as Day[] }
|
||||
}
|
||||
const result = await daysApi.list(tripId)
|
||||
upsertDays(result.days)
|
||||
return result
|
||||
async list(tripId: number | string): Promise<{ days: Day[]; refresh: Promise<{ days: Day[] } | null> }> {
|
||||
const cached = (await offlineDb.days
|
||||
.where('trip_id')
|
||||
.equals(Number(tripId))
|
||||
.sortBy('day_number' as keyof Day)) as Day[]
|
||||
|
||||
const refresh = (async () => {
|
||||
try {
|
||||
const result = await daysApi.list(tripId)
|
||||
upsertDays(result.days)
|
||||
return result
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})()
|
||||
|
||||
if (cached.length > 0) return { days: cached, refresh }
|
||||
|
||||
const fresh = await refresh
|
||||
if (!fresh) return { days: [], refresh: Promise.resolve(null) }
|
||||
return { days: fresh.days, refresh: Promise.resolve(fresh) }
|
||||
},
|
||||
|
||||
async update(tripId: number | string, dayId: number | string, data: Record<string, unknown>): Promise<{ day: Day }> {
|
||||
|
||||
+21
-11
@@ -4,17 +4,27 @@ import { mutationQueue, generateUUID } from '../sync/mutationQueue'
|
||||
import type { TripFile } from '../types'
|
||||
|
||||
export const fileRepo = {
|
||||
async list(tripId: number | string): Promise<{ files: TripFile[] }> {
|
||||
if (!navigator.onLine) {
|
||||
const cached = await offlineDb.tripFiles
|
||||
.where('trip_id')
|
||||
.equals(Number(tripId))
|
||||
.toArray()
|
||||
return { files: cached }
|
||||
}
|
||||
const result = await filesApi.list(tripId)
|
||||
upsertTripFiles(result.files)
|
||||
return result
|
||||
async list(tripId: number | string): Promise<{ files: TripFile[]; refresh: Promise<{ files: TripFile[] } | null> }> {
|
||||
const cached = await offlineDb.tripFiles
|
||||
.where('trip_id')
|
||||
.equals(Number(tripId))
|
||||
.toArray()
|
||||
|
||||
const refresh = (async () => {
|
||||
try {
|
||||
const result = await filesApi.list(tripId)
|
||||
upsertTripFiles(result.files)
|
||||
return result
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})()
|
||||
|
||||
if (cached.length > 0) return { files: cached, refresh }
|
||||
|
||||
const fresh = await refresh
|
||||
if (!fresh) return { files: [], refresh: Promise.resolve(null) }
|
||||
return { files: fresh.files, refresh: Promise.resolve(fresh) }
|
||||
},
|
||||
|
||||
async update(tripId: number | string, id: number, data: Record<string, unknown>): Promise<unknown> {
|
||||
|
||||
@@ -4,17 +4,27 @@ import { mutationQueue, generateUUID } from '../sync/mutationQueue'
|
||||
import type { PackingItem } from '../types'
|
||||
|
||||
export const packingRepo = {
|
||||
async list(tripId: number | string): Promise<{ items: PackingItem[] }> {
|
||||
if (!navigator.onLine) {
|
||||
const cached = await offlineDb.packingItems
|
||||
.where('trip_id')
|
||||
.equals(Number(tripId))
|
||||
.toArray()
|
||||
return { items: cached }
|
||||
}
|
||||
const result = await packingApi.list(tripId)
|
||||
upsertPackingItems(result.items)
|
||||
return result
|
||||
async list(tripId: number | string): Promise<{ items: PackingItem[]; refresh: Promise<{ items: PackingItem[] } | null> }> {
|
||||
const cached = await offlineDb.packingItems
|
||||
.where('trip_id')
|
||||
.equals(Number(tripId))
|
||||
.toArray()
|
||||
|
||||
const refresh = (async () => {
|
||||
try {
|
||||
const result = await packingApi.list(tripId)
|
||||
upsertPackingItems(result.items)
|
||||
return result
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})()
|
||||
|
||||
if (cached.length > 0) return { items: cached, refresh }
|
||||
|
||||
const fresh = await refresh
|
||||
if (!fresh) return { items: [], refresh: Promise.resolve(null) }
|
||||
return { items: fresh.items, refresh: Promise.resolve(fresh) }
|
||||
},
|
||||
|
||||
async create(tripId: number | string, data: Record<string, unknown>): Promise<{ item: PackingItem }> {
|
||||
|
||||
@@ -4,17 +4,27 @@ import { mutationQueue, generateUUID } from '../sync/mutationQueue'
|
||||
import type { Place } from '../types'
|
||||
|
||||
export const placeRepo = {
|
||||
async list(tripId: number | string, params?: Record<string, unknown>): Promise<{ places: Place[] }> {
|
||||
if (!navigator.onLine) {
|
||||
const cached = await offlineDb.places
|
||||
.where('trip_id')
|
||||
.equals(Number(tripId))
|
||||
.toArray()
|
||||
return { places: cached }
|
||||
}
|
||||
const result = await placesApi.list(tripId, params)
|
||||
upsertPlaces(result.places)
|
||||
return result
|
||||
async list(tripId: number | string, params?: Record<string, unknown>): Promise<{ places: Place[]; refresh: Promise<{ places: Place[] } | null> }> {
|
||||
const cached = await offlineDb.places
|
||||
.where('trip_id')
|
||||
.equals(Number(tripId))
|
||||
.toArray()
|
||||
|
||||
const refresh = (async () => {
|
||||
try {
|
||||
const result = await placesApi.list(tripId, params)
|
||||
upsertPlaces(result.places)
|
||||
return result
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})()
|
||||
|
||||
if (cached.length > 0) return { places: cached, refresh }
|
||||
|
||||
const fresh = await refresh
|
||||
if (!fresh) return { places: [], refresh: Promise.resolve(null) }
|
||||
return { places: fresh.places, refresh: Promise.resolve(fresh) }
|
||||
},
|
||||
|
||||
async create(tripId: number | string, data: Record<string, unknown>): Promise<{ place: Place }> {
|
||||
|
||||
@@ -4,17 +4,27 @@ import { mutationQueue, generateUUID } from '../sync/mutationQueue'
|
||||
import type { Reservation } from '../types'
|
||||
|
||||
export const reservationRepo = {
|
||||
async list(tripId: number | string): Promise<{ reservations: Reservation[] }> {
|
||||
if (!navigator.onLine) {
|
||||
const cached = await offlineDb.reservations
|
||||
.where('trip_id')
|
||||
.equals(Number(tripId))
|
||||
.toArray()
|
||||
return { reservations: cached }
|
||||
}
|
||||
const result = await reservationsApi.list(tripId)
|
||||
upsertReservations(result.reservations)
|
||||
return result
|
||||
async list(tripId: number | string): Promise<{ reservations: Reservation[]; refresh: Promise<{ reservations: Reservation[] } | null> }> {
|
||||
const cached = await offlineDb.reservations
|
||||
.where('trip_id')
|
||||
.equals(Number(tripId))
|
||||
.toArray()
|
||||
|
||||
const refresh = (async () => {
|
||||
try {
|
||||
const result = await reservationsApi.list(tripId)
|
||||
upsertReservations(result.reservations)
|
||||
return result
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})()
|
||||
|
||||
if (cached.length > 0) return { reservations: cached, refresh }
|
||||
|
||||
const fresh = await refresh
|
||||
if (!fresh) return { reservations: [], refresh: Promise.resolve(null) }
|
||||
return { reservations: fresh.reservations, refresh: Promise.resolve(fresh) }
|
||||
},
|
||||
|
||||
async create(tripId: number | string, data: Record<string, unknown>): Promise<{ reservation: Reservation }> {
|
||||
|
||||
+21
-11
@@ -4,17 +4,27 @@ import { mutationQueue, generateUUID } from '../sync/mutationQueue'
|
||||
import type { TodoItem } from '../types'
|
||||
|
||||
export const todoRepo = {
|
||||
async list(tripId: number | string): Promise<{ items: TodoItem[] }> {
|
||||
if (!navigator.onLine) {
|
||||
const cached = await offlineDb.todoItems
|
||||
.where('trip_id')
|
||||
.equals(Number(tripId))
|
||||
.toArray()
|
||||
return { items: cached }
|
||||
}
|
||||
const result = await todoApi.list(tripId)
|
||||
upsertTodoItems(result.items)
|
||||
return result
|
||||
async list(tripId: number | string): Promise<{ items: TodoItem[]; refresh: Promise<{ items: TodoItem[] } | null> }> {
|
||||
const cached = await offlineDb.todoItems
|
||||
.where('trip_id')
|
||||
.equals(Number(tripId))
|
||||
.toArray()
|
||||
|
||||
const refresh = (async () => {
|
||||
try {
|
||||
const result = await todoApi.list(tripId)
|
||||
upsertTodoItems(result.items)
|
||||
return result
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})()
|
||||
|
||||
if (cached.length > 0) return { items: cached, refresh }
|
||||
|
||||
const fresh = await refresh
|
||||
if (!fresh) return { items: [], refresh: Promise.resolve(null) }
|
||||
return { items: fresh.items, refresh: Promise.resolve(fresh) }
|
||||
},
|
||||
|
||||
async create(tripId: number | string, data: Record<string, unknown>): Promise<{ item: TodoItem }> {
|
||||
|
||||
+44
-19
@@ -3,33 +3,58 @@ import { offlineDb, upsertTrip } from '../db/offlineDb'
|
||||
import { mutationQueue, generateUUID } from '../sync/mutationQueue'
|
||||
import type { Trip } from '../types'
|
||||
|
||||
type TripsRefresh = Promise<{ trips: Trip[]; archivedTrips: Trip[] } | null>
|
||||
type TripRefresh = Promise<{ trip: Trip } | null>
|
||||
|
||||
export const tripRepo = {
|
||||
async list(): Promise<{ trips: Trip[]; archivedTrips: Trip[] }> {
|
||||
if (!navigator.onLine) {
|
||||
const all = await offlineDb.trips.toArray()
|
||||
async list(): Promise<{ trips: Trip[]; archivedTrips: Trip[]; refresh: TripsRefresh }> {
|
||||
const all = await offlineDb.trips.toArray()
|
||||
|
||||
const refresh: TripsRefresh = (async () => {
|
||||
try {
|
||||
const [active, archived] = await Promise.all([
|
||||
tripsApi.list(),
|
||||
tripsApi.list({ archived: 1 }),
|
||||
])
|
||||
active.trips.forEach(t => upsertTrip(t))
|
||||
archived.trips.forEach(t => upsertTrip(t))
|
||||
return { trips: active.trips, archivedTrips: archived.trips }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})()
|
||||
|
||||
if (all.length > 0) {
|
||||
return {
|
||||
trips: all.filter(t => !t.is_archived),
|
||||
archivedTrips: all.filter(t => t.is_archived),
|
||||
refresh,
|
||||
}
|
||||
}
|
||||
const [active, archived] = await Promise.all([
|
||||
tripsApi.list(),
|
||||
tripsApi.list({ archived: 1 }),
|
||||
])
|
||||
active.trips.forEach(t => upsertTrip(t))
|
||||
archived.trips.forEach(t => upsertTrip(t))
|
||||
return { trips: active.trips, archivedTrips: archived.trips }
|
||||
|
||||
const fresh = await refresh
|
||||
if (!fresh) return { trips: [], archivedTrips: [], refresh: Promise.resolve(null) }
|
||||
return { ...fresh, refresh: Promise.resolve(fresh) }
|
||||
},
|
||||
|
||||
async get(tripId: number | string): Promise<{ trip: Trip }> {
|
||||
if (!navigator.onLine) {
|
||||
const cached = await offlineDb.trips.get(Number(tripId))
|
||||
if (cached) return { trip: cached }
|
||||
throw new Error('No cached trip data available offline')
|
||||
}
|
||||
const result = await tripsApi.get(tripId)
|
||||
upsertTrip(result.trip)
|
||||
return result
|
||||
async get(tripId: number | string): Promise<{ trip: Trip; refresh: TripRefresh }> {
|
||||
const cached = await offlineDb.trips.get(Number(tripId))
|
||||
|
||||
const refresh: TripRefresh = (async () => {
|
||||
try {
|
||||
const result = await tripsApi.get(tripId)
|
||||
upsertTrip(result.trip)
|
||||
return result
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})()
|
||||
|
||||
if (cached) return { trip: cached, refresh }
|
||||
|
||||
const fresh = await refresh
|
||||
if (!fresh) throw new Error('No cached trip data available offline')
|
||||
return { trip: fresh.trip, refresh: Promise.resolve(fresh) }
|
||||
},
|
||||
|
||||
async update(tripId: number | string, data: Partial<Trip>): Promise<{ trip: Trip }> {
|
||||
|
||||
Reference in New Issue
Block a user