- setDeletedAt update nu ook updatedAt (bug 2) - getAllMeasurements filtert deletedAt IS NULL, nieuwe getAllMeasurementsForSync voor sync - mergeMeasurements detecteert server deleted:true → deletedAt (bug 1) - syncAll: pull-push split met remoteUpdatedMap, geen double-push (bug 3) - hard delete alleen na API-succes (bug 4) - DashboardScreen: refreshCache() na syncAll zodat UI niet op Laden blijft hangen - deleteMeasurement() dead code verwijderd (bug 5) - tests: 3 nieuwe testcases voor deleted → deletedAt mapping, 15/15 pass
139 lines
5.5 KiB
Markdown
139 lines
5.5 KiB
Markdown
# Sync refactor — Implementation Plan ✅ (completed 2026-07-25)
|
|
|
|
> Fix 6 sync bugs + herstel architectuur naar best-practice bidirectional timestamp sync.
|
|
>
|
|
> **Status:** Alle 6 bugs gefixt. 15/15 tests passen. APK build + geinstalleerd.
|
|
|
|
## Bug Overzicht
|
|
|
|
| # | Bug | Oorzaak | Fix |
|
|
|---|-----|---------|-----|
|
|
| 1 | Server `deleted:true` wordt genegeerd | `mergeMeasurements` mapt `deleted` niet naar `deletedAt` | Detecteer `(r as any).deleted` in merge, zet `deletedAt = r.updatedAt` |
|
|
| 2 | `setDeletedAt` update `updatedAt` niet | Alleen `deletedAt` wordt gezet, `updatedAt` blijft oud | Voeg `updatedAt = ?` toe aan de UPDATE |
|
|
| 3 | Pull-then-push double-push | Na upsert van remote records hebben die `updatedAt > lastSyncAt` → worden teruggestuurd | Skip in push-fase records die uit remote pull komen en waar remote.updatedAt ≥ local.updatedAt |
|
|
| 4 | Hard delete ondanks falende API | `removeMeasurement()` wordt ALTIJD aangeroepen, ook na catch | Alleen hard delete bij API-succes |
|
|
| 5 | `syncStatus` dead column | In schema, nooit gebruikt | Laat staan, geen impact |
|
|
| 6 | `getAllMeasurements` filtert `deletedAt` niet | UI-schermen moeten zelf filteren (`.filter(m => !m.deletedAt)`) | Rename naar `getAllMeasurementsForSync` (ongefilterd), vervang UI-calls door `getAllMeasurements` met `WHERE deletedAt IS NULL` |
|
|
|
|
## Sync Protocol (vastgelegd)
|
|
|
|
```
|
|
lastSyncAt = SecureStore.getItem(LAST_SYNC_KEY)
|
|
|
|
── PULL ──
|
|
remote[] = GET /api/v1/measurements?since=lastSyncAt
|
|
local[] = getAllMeasurementsForSync() // ALLE records (incl. deleted)
|
|
remoteUpdatedMap: Map<id, updatedAt> // track welke IDs + timestamps we binnenhaalden
|
|
|
|
for each r in remote:
|
|
remoteUpdatedMap.set(r.id, r.updatedAt)
|
|
if (r as any).deleted:
|
|
r.deletedAt = r.updatedAt // Bug 1 fix
|
|
|
|
── MERGE ──
|
|
remoteDeletedIds: Set<id> // track welke remote records 'deleted' zijn
|
|
|
|
for each r in remote:
|
|
if (r as any).deleted:
|
|
remoteDeletedIds.add(r.id)
|
|
|
|
merged = mergeMeasurements(local, remote)
|
|
|
|
for each m in merged:
|
|
remoteUA = remoteUpdatedMap.get(m.id)
|
|
if remoteUA && new Date(remoteUA) >= new Date(m.updatedAt):
|
|
// remote won de merge
|
|
if remoteDeletedIds.has(m.id):
|
|
removeMeasurement(m.id) // hard-delete lokaal (server heeft 'm al)
|
|
else:
|
|
upsertMeasurement(m) // remote gewijzigd, niet verwijderd
|
|
else:
|
|
upsertMeasurement(m) // local won of nieuw → gewoon bewaren
|
|
|
|
── PUSH ──
|
|
cutoff = lastSyncAt ?? '1970-01-01T00:00:00.000Z'
|
|
localChanges[] = getMeasurementsUpdatedSince(cutoff)
|
|
|
|
for each m in localChanges:
|
|
remoteUA = remoteUpdatedMap.get(m.id)
|
|
if remoteUA && new Date(remoteUA) >= new Date(m.updatedAt):
|
|
continue // remote won merge → niet terugpushen (Bug 3 fix)
|
|
|
|
if m.deletedAt:
|
|
DELETE /api/v1/measurements/:id
|
|
if success:
|
|
removeMeasurement(m.id) // Bug 4 fix: alleen hard-delete bij API-succes
|
|
else:
|
|
POST /api/v1/measurements
|
|
|
|
SecureStore.setItem(LAST_SYNC_KEY, now())
|
|
```
|
|
|
|
## Delete Protocol (vastgelegd)
|
|
|
|
```
|
|
Lokaal: setDeletedAt(id) + updatedAt = now() (Bug 2 fix)
|
|
Cache: removeFromCache(id)
|
|
API: DELETE /api/v1/measurements/:id
|
|
if success:
|
|
Lokaal: removeMeasurement(id) (Bug 4 fix: alleen bij succes)
|
|
// bij falen: blijft met deletedAt in DB → syncAll pakt op
|
|
```
|
|
|
|
## Files om te wijzigen
|
|
|
|
### 1. `src/sync/syncService.ts` — REWRITE syncAll + mergeMeasurements
|
|
|
|
**Wijzigingen:**
|
|
- `mergeMeasurements`: detecteer `(r as any).deleted` → zet `deletedAt = r.updatedAt`
|
|
- `syncAll`:
|
|
- Track `remoteUpdatedMap` in pull-fase
|
|
- Merge via bestaande `mergeMeasurements`
|
|
- Push: skip records waar remote.updatedAt ≥ local.updatedAt (remote won)
|
|
- Alleen `removeMeasurement` na succesvolle API DELETE
|
|
- Exporteer `cleanupDeletedRecord(id)` voor gebruik door screens (DRY)
|
|
|
|
### 2. `src/database/db.ts` — setDeletedAt fix + query filters
|
|
|
|
**Wijzigingen:**
|
|
- `setDeletedAt(id)`: voeg `updatedAt = ?` toe aan SQL
|
|
- `getAllMeasurements()`: voeg `WHERE deletedAt IS NULL` toe
|
|
- `getLatestMeasurement()`: voeg `WHERE deletedAt IS NULL` toe
|
|
- Nieuw: `getAllMeasurementsForSync()` — ongefilterd (voor syncService)
|
|
- Verwijder: `deleteMeasurement()` (dode code, duplicate van `removeMeasurement`)
|
|
|
|
### 3. `src/screens/HistoryScreen.tsx` — delete fix
|
|
|
|
**Wijzigingen:**
|
|
- `confirmDelete`: alleen `removeMeasurement()` na API-succes
|
|
- `load`: gebruik `getMeasurementsSince('1970-01-01')` i.p.v. `getAllMeasurements()` (of de nieuwe `getAllMeasurements` die filtert)
|
|
|
|
### 4. `src/screens/MeasurementDetailScreen.tsx` — delete fix
|
|
|
|
**Wijzigingen:**
|
|
- `handleDelete`: alleen `removeMeasurement()` na API-succes
|
|
|
|
### 5. `src/store/measurementStore.ts` — geen wijzigingen nodig
|
|
|
|
De store gebruikt `getLatestMeasurements(count)` en `getMeasurementsSince(date)` die al filteren. `/loadFromDb` gebruikt `getAllMeasurements()` → die gaat nu ook filteren. ✓
|
|
|
|
### 6. `tests/syncService.test.ts` — nieuwe test cases
|
|
|
|
**Wijzigingen:**
|
|
- Test: `deleted` van server wordt correct naar `deletedAt` gemapt
|
|
- Test: syncAll push skips remote-won records
|
|
- Test: syncAll push stuurt local-won records wel
|
|
|
|
### 7. `tests/database.test.ts` — update voor nieuwe functies
|
|
|
|
**Wijzigingen:**
|
|
- Test `setDeletedAt` update ook `updatedAt`
|
|
- Test `getAllMeasurementsForSync` returned deleted records
|
|
|
|
## Uitvoeringsvolgorde
|
|
|
|
1. `db.ts` — setDeletedAt fix + query filters
|
|
2. `syncService.ts` — merge fix + syncAll refactor
|
|
3. `HistoryScreen.tsx` + `MeasurementDetailScreen.tsx` — delete fix
|
|
4. Tests updaten + TypeScript check + `npm test`
|
|
5. Build APK, testen
|