mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 14:21:46 +00:00
feat(pwa): implement real offline mode with IndexedDB sync
Add genuine offline read/write capability for trips: - Dexie IndexedDB schema (trips, places, packing, todo, budget, reservations, files, mutationQueue, syncMeta, blobCache) - Repo layer for all domains: offline reads from Dexie, writes optimistically to Dexie and enqueue mutations for later replay - Mutation queue with UUID idempotency keys (X-Idempotency-Key), FIFO flush, temp-ID reconciliation on 2xx, fail-and-continue on 4xx - Trip sync manager: caches all trips with end_date >= today or null, auto-evicts 7d after end_date, fetches bundle endpoint in one request - Map tile prefetcher: bbox from place coords, zooms 10-16, 50MB cap, warms SW cache via fetch - Sync triggers: network online → flush + syncAll; WS reconnect → flush only (rate-limiter safe); visibilitychange/30s → flush only - WS remoteEventHandler writes through to Dexie on every event - Server idempotency middleware + idempotency_keys table (migration 100, 24h TTL nightly cleanup) - GET /api/trips/:id/bundle endpoint for efficient single-request sync - OfflineBanner component: amber (offline) / blue (syncing) / hidden - OfflineTab in Settings: cached trip list, re-sync and clear actions - usePendingMutations hook for per-item pending indicators Closes #505 #541
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
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[] }> {
|
||||
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 create(tripId: number | string, data: Record<string, unknown>): Promise<{ place: Place }> {
|
||||
if (!navigator.onLine) {
|
||||
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)
|
||||
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()
|
||||
await mutationQueue.enqueue({
|
||||
id: mutId,
|
||||
tripId: Number(tripId),
|
||||
method: 'PUT',
|
||||
url: `/trips/${tripId}/places/${id}`,
|
||||
body: data,
|
||||
resource: 'places',
|
||||
})
|
||||
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()
|
||||
await mutationQueue.enqueue({
|
||||
id: mutId,
|
||||
tripId: Number(tripId),
|
||||
method: 'DELETE',
|
||||
url: `/trips/${tripId}/places/${id}`,
|
||||
body: undefined,
|
||||
resource: 'places',
|
||||
entityId: Number(id),
|
||||
})
|
||||
return { success: true }
|
||||
}
|
||||
const result = await placesApi.delete(tripId, id)
|
||||
offlineDb.places.delete(Number(id))
|
||||
return result
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user