mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-20 13:51:45 +00:00
b194e8317d
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
89 lines
2.8 KiB
TypeScript
89 lines
2.8 KiB
TypeScript
import { packingApi } from '../api/client'
|
|
import { offlineDb, upsertPackingItems } from '../db/offlineDb'
|
|
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 create(tripId: number | string, data: Record<string, unknown>): Promise<{ item: PackingItem }> {
|
|
if (!navigator.onLine) {
|
|
const tempId = -(Date.now())
|
|
const tempItem: PackingItem = {
|
|
...(data as Partial<PackingItem>),
|
|
id: tempId,
|
|
trip_id: Number(tripId),
|
|
name: (data.name as string) ?? 'New item',
|
|
checked: 0,
|
|
} as PackingItem
|
|
await offlineDb.packingItems.put(tempItem)
|
|
const id = generateUUID()
|
|
await mutationQueue.enqueue({
|
|
id,
|
|
tripId: Number(tripId),
|
|
method: 'POST',
|
|
url: `/trips/${tripId}/packing`,
|
|
body: data,
|
|
resource: 'packingItems',
|
|
tempId,
|
|
})
|
|
return { item: tempItem }
|
|
}
|
|
const result = await packingApi.create(tripId, data)
|
|
offlineDb.packingItems.put(result.item)
|
|
return result
|
|
},
|
|
|
|
async update(tripId: number | string, id: number, data: Record<string, unknown>): Promise<{ item: PackingItem }> {
|
|
if (!navigator.onLine) {
|
|
const existing = await offlineDb.packingItems.get(id)
|
|
const optimistic: PackingItem = { ...(existing ?? {} as PackingItem), ...(data as Partial<PackingItem>), id }
|
|
await offlineDb.packingItems.put(optimistic)
|
|
const mutId = generateUUID()
|
|
await mutationQueue.enqueue({
|
|
id: mutId,
|
|
tripId: Number(tripId),
|
|
method: 'PUT',
|
|
url: `/trips/${tripId}/packing/${id}`,
|
|
body: data,
|
|
resource: 'packingItems',
|
|
})
|
|
return { item: optimistic }
|
|
}
|
|
const result = await packingApi.update(tripId, id, data)
|
|
offlineDb.packingItems.put(result.item)
|
|
return result
|
|
},
|
|
|
|
async delete(tripId: number | string, id: number): Promise<unknown> {
|
|
if (!navigator.onLine) {
|
|
await offlineDb.packingItems.delete(id)
|
|
const mutId = generateUUID()
|
|
await mutationQueue.enqueue({
|
|
id: mutId,
|
|
tripId: Number(tripId),
|
|
method: 'DELETE',
|
|
url: `/trips/${tripId}/packing/${id}`,
|
|
body: undefined,
|
|
resource: 'packingItems',
|
|
entityId: id,
|
|
})
|
|
return { success: true }
|
|
}
|
|
const result = await packingApi.delete(tripId, id)
|
|
offlineDb.packingItems.delete(id)
|
|
return result
|
|
},
|
|
}
|