Files
TREK/client/src/repo/placeRepo.ts
T
jubnl 69620e7276 feat: always-optimistic write pattern across all repos
All create/update/delete repo methods now write to IndexedDB optimistically
and fire mutationQueue.flush() as fire-and-forget, returning immediately
without waiting for the network. This eliminates the 8-second UX freeze
previously seen when the API was unreachable but navigator.onLine was true.

- Repos rewritten: trip, day, place, packing, todo, budget, accommodation,
  reservation, file — write methods never throw, always return optimistic data
- mutationQueue.flush() changed to iterative (one item per loop iteration)
  so mutations enqueued mid-flush (e.g. bulk check-all) are picked up
- fileRepo.toggleStar skips the IDB put when the file is not cached locally
- DayDetailPanel passes place_name into accommodationRepo.create so the
  optimistic accommodation renders the correct hotel label immediately
- Test suite updated throughout to reflect optimistic-first semantics:
  no more rollback assertions, IDB cleared in component test beforeEach hooks,
  FileManager tests switched from filesApi spy to MSW endpoint assertions
2026-05-05 02:14:39 +02:00

101 lines
3.1 KiB
TypeScript

import { placesApi } from '../api/client'
import { offlineDb, upsertPlaces } from '../db/offlineDb'
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[]; refresh: Promise<{ places: Place[] } | null> }> {
const cached = await offlineDb.places
.where('trip_id')
.equals(Number(tripId))
.toArray()
const refresh = (async () => {
if (!navigator.onLine) return null
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 }> {
const tempId = -(Date.now())
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)
await mutationQueue.enqueue({
id: generateUUID(),
tripId: Number(tripId),
method: 'POST',
url: `/trips/${tripId}/places`,
body: data,
resource: 'places',
tempId,
})
mutationQueue.flush().catch(() => {})
return { place: tempPlace }
},
async update(tripId: number | string, id: number | string, data: Record<string, unknown>): Promise<{ place: Place }> {
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)
await mutationQueue.enqueue({
id: generateUUID(),
tripId: Number(tripId),
method: 'PUT',
url: `/trips/${tripId}/places/${id}`,
body: data,
resource: 'places',
})
mutationQueue.flush().catch(() => {})
return { place: optimistic }
},
async delete(tripId: number | string, id: number | string): Promise<unknown> {
await offlineDb.places.delete(Number(id))
await mutationQueue.enqueue({
id: generateUUID(),
tripId: Number(tripId),
method: 'DELETE',
url: `/trips/${tripId}/places/${id}`,
body: undefined,
resource: 'places',
entityId: Number(id),
})
mutationQueue.flush().catch(() => {})
return { success: true }
},
async deleteMany(tripId: number | string, ids: number[]): Promise<unknown> {
await offlineDb.places.bulkDelete(ids)
for (const id of ids) {
await mutationQueue.enqueue({
id: generateUUID(),
tripId: Number(tripId),
method: 'DELETE',
url: `/trips/${tripId}/places/${id}`,
body: undefined,
resource: 'places',
entityId: id,
})
}
mutationQueue.flush().catch(() => {})
return { deleted: ids, count: ids.length }
},
}