fix: sync refactor — fix 6 bugs + cache refresh na sync
- 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
This commit is contained in:
parent
55572a1455
commit
df05fb75dd
13 changed files with 405 additions and 62 deletions
139
docs/superpowers/plans/2026-07-25-sync-refactor-plan.md
Normal file
139
docs/superpowers/plans/2026-07-25-sync-refactor-plan.md
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# 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
|
||||
Loading…
Reference in a new issue