mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 14:21:46 +00:00
bcd2c8c959
Repos gated reads on raw navigator.onLine and the online branch had no try/catch, so a captive portal or connected-but-no-internet (navigator.onLine lying "true") threw a network error instead of serving the good cached copy — blanking the trip even though Dexie held it. - new onlineThenCache(onlineFn, cacheFn) helper: reads the cache when offline, and on a network-level failure (Axios error with no HTTP response). A genuine HTTP error (4xx/5xx — the server responded) is rethrown so callers still set error state / navigate, not masked by a stale cache. - gates only on navigator.onLine, NOT the connectivity probe: the probe is a coarse global flag and one failed health check would otherwise divert every read to the (possibly empty) cache even when the request would succeed. - every repo list/get read path routed through it (reads only — writes still go through the mutation queue so failures surface) - tests: captive-portal fallback, HTTP-error rethrow, non-Axios rethrow
119 lines
3.9 KiB
TypeScript
119 lines
3.9 KiB
TypeScript
import { placesApi } from '../api/client'
|
|
import { offlineDb, upsertPlaces } from '../db/offlineDb'
|
|
import { mutationQueue, generateUUID, nextTempId } from '../sync/mutationQueue'
|
|
import { onlineThenCache } from './withOfflineFallback'
|
|
import type { Place } from '../types'
|
|
|
|
export const placeRepo = {
|
|
async list(tripId: number | string, params?: Record<string, unknown>): Promise<{ places: Place[] }> {
|
|
return onlineThenCache(
|
|
async () => {
|
|
const result = await placesApi.list(tripId, params)
|
|
upsertPlaces(result.places)
|
|
return result
|
|
},
|
|
async () => ({
|
|
places: await offlineDb.places
|
|
.where('trip_id').equals(Number(tripId)).toArray(),
|
|
}),
|
|
)
|
|
},
|
|
|
|
async create(tripId: number | string, data: Record<string, unknown> & { name: string }): Promise<{ place: Place }> {
|
|
if (!navigator.onLine) {
|
|
const tempId = nextTempId()
|
|
const tempPlace: Place = {
|
|
...(data as Partial<Place>),
|
|
id: tempId,
|
|
trip_id: Number(tripId),
|
|
name: (data.name as string) ?? 'New place',
|
|
} as Place
|
|
await offlineDb.places.put(tempPlace)
|
|
const id = generateUUID()
|
|
await mutationQueue.enqueue({
|
|
id,
|
|
tripId: Number(tripId),
|
|
method: 'POST',
|
|
url: `/trips/${tripId}/places`,
|
|
body: data,
|
|
resource: 'places',
|
|
tempId,
|
|
})
|
|
return { place: tempPlace }
|
|
}
|
|
const result = await placesApi.create(tripId, data)
|
|
offlineDb.places.put(result.place)
|
|
return result
|
|
},
|
|
|
|
async update(tripId: number | string, id: number | string, data: Record<string, unknown>): Promise<{ place: Place }> {
|
|
if (!navigator.onLine) {
|
|
const existing = await offlineDb.places.get(Number(id))
|
|
const optimistic: Place = { ...(existing ?? {} as Place), ...(data as Partial<Place>), id: Number(id) }
|
|
await offlineDb.places.put(optimistic)
|
|
const mutId = generateUUID()
|
|
const isTemp = Number(id) < 0
|
|
await mutationQueue.enqueue({
|
|
id: mutId,
|
|
tripId: Number(tripId),
|
|
method: 'PUT',
|
|
url: isTemp ? `/trips/${tripId}/places/{id}` : `/trips/${tripId}/places/${id}`,
|
|
body: data,
|
|
resource: 'places',
|
|
entityId: Number(id),
|
|
...(isTemp ? { tempEntityId: Number(id) } : {}),
|
|
})
|
|
return { place: optimistic }
|
|
}
|
|
const result = await placesApi.update(tripId, id, data)
|
|
offlineDb.places.put(result.place)
|
|
return result
|
|
},
|
|
|
|
async delete(tripId: number | string, id: number | string): Promise<unknown> {
|
|
if (!navigator.onLine) {
|
|
await offlineDb.places.delete(Number(id))
|
|
const mutId = generateUUID()
|
|
const isTemp = Number(id) < 0
|
|
await mutationQueue.enqueue({
|
|
id: mutId,
|
|
tripId: Number(tripId),
|
|
method: 'DELETE',
|
|
url: isTemp ? `/trips/${tripId}/places/{id}` : `/trips/${tripId}/places/${id}`,
|
|
body: undefined,
|
|
resource: 'places',
|
|
entityId: Number(id),
|
|
...(isTemp ? { tempEntityId: Number(id) } : {}),
|
|
})
|
|
return { success: true }
|
|
}
|
|
const result = await placesApi.delete(tripId, id)
|
|
offlineDb.places.delete(Number(id))
|
|
return result
|
|
},
|
|
|
|
async deleteMany(tripId: number | string, ids: number[]): Promise<unknown> {
|
|
if (!navigator.onLine) {
|
|
await offlineDb.places.bulkDelete(ids)
|
|
for (const id of ids) {
|
|
const mutId = generateUUID()
|
|
const isTemp = id < 0
|
|
await mutationQueue.enqueue({
|
|
id: mutId,
|
|
tripId: Number(tripId),
|
|
method: 'DELETE',
|
|
url: isTemp ? `/trips/${tripId}/places/{id}` : `/trips/${tripId}/places/${id}`,
|
|
body: undefined,
|
|
resource: 'places',
|
|
entityId: id,
|
|
...(isTemp ? { tempEntityId: id } : {}),
|
|
})
|
|
}
|
|
return { deleted: ids, count: ids.length }
|
|
}
|
|
const result = await placesApi.bulkDelete(tripId, ids)
|
|
await offlineDb.places.bulkDelete(ids)
|
|
return result
|
|
},
|
|
}
|