From cec6c54e6164b92d74bc016837d7a947cf624be0 Mon Sep 17 00:00:00 2001 From: eddy Date: Mon, 20 Jul 2026 16:56:28 +0200 Subject: [PATCH] sync: herschrijf syncService voor timestamp-based REST sync --- src/sync/syncService.ts | 143 +++++++++++++++++----------------------- 1 file changed, 60 insertions(+), 83 deletions(-) diff --git a/src/sync/syncService.ts b/src/sync/syncService.ts index 94d315f..b52563a 100644 --- a/src/sync/syncService.ts +++ b/src/sync/syncService.ts @@ -1,111 +1,88 @@ import { Measurement } from '../types' -import { initDatabase, getAllMeasurements, upsertMeasurement } from '../database/db' -import { getWebDAVConfig } from '../screens/SettingsScreen' +import { + initDatabase, + getAllMeasurements, + upsertMeasurement, + getMeasurementsUpdatedSince, + removeMeasurement, +} from '../database/db' +import { createApi } from './api' +import { ApiError } from './api' +import * as SecureStore from 'expo-secure-store' -const WEBDAV_PATH = 'weegschaal/measurements.json' +const LAST_SYNC_KEY = 'weegschaal_last_sync' -type SyncResult = { local: number; remote: number; merged: number } - -export function mergeMeasurements(local: Measurement[], remote: Measurement[], excludeIds?: Set): Measurement[] { +export function mergeMeasurements(local: Measurement[], remote: Measurement[]): Measurement[] { const map = new Map() for (const m of local) { - if (excludeIds?.has(m.id)) continue map.set(m.id, m) } - for (const m of remote) { - if (excludeIds?.has(m.id)) continue - const existing = map.get(m.id) - if (!existing || new Date(m.updatedAt) > new Date(existing.updatedAt)) { - map.set(m.id, { ...m, syncStatus: 'synced' }) + for (const r of remote) { + const existing = map.get(r.id) + if (!existing || new Date(r.updatedAt) > new Date(existing.updatedAt)) { + map.set(r.id, { + ...r, + deletedAt: r.deletedAt || undefined, + } as Measurement) } } return Array.from(map.values()) } -async function fetchRemote(config: { url: string; username: string; password: string }): Promise { - const url = `${config.url.replace(/\/+$/, '')}/${WEBDAV_PATH}` - const response = await fetch(url, { - headers: { - Authorization: 'Basic ' + btoa(`${config.username}:${config.password}`), - }, - }) - - if (response.status === 404) { - return [] - } - - if (!response.ok) { - throw new Error(`WebDAV fetch failed: ${response.status}`) - } - - return response.json() +export async function uploadMeasurement(m: Measurement): Promise { + const { getApiConfig } = await import('../screens/SettingsScreen') + const config = await getApiConfig() + if (!config) throw new Error('Geen API configuratie. Stel deze eerst in in Instellingen.') + const api = createApi(config) + await api.createMeasurement(m) } -async function createDir(config: { url: string; username: string; password: string }): Promise { - const base = config.url.replace(/\/+$/, '') - const dirUrl = `${base}/weegschaal` - - const response = await fetch(dirUrl, { - method: 'MKCOL', - headers: { - Authorization: 'Basic ' + btoa(`${config.username}:${config.password}`), - }, - }) - - if (response.status !== 201 && response.status !== 405) { - throw new Error(`WebDAV directory create failed: ${response.status}`) - } -} - -async function uploadRemote( - config: { url: string; username: string; password: string }, - measurements: Measurement[] -): Promise { - const base = config.url.replace(/\/+$/, '') - const url = `${base}/${WEBDAV_PATH}` - const body = JSON.stringify(measurements) - - await createDir(config) - - const response = await fetch(url, { - method: 'PUT', - headers: { - Authorization: 'Basic ' + btoa(`${config.username}:${config.password}`), - 'Content-Type': 'application/json', - }, - body, - }) - - if (!response.ok) { - throw new Error(`WebDAV upload failed: ${response.status}`) - } -} - -export async function syncAll(excludeIds?: Set): Promise { - const config = await getWebDAVConfig() - if (!config) { - throw new Error('Geen WebDAV configuratie. Stel deze eerst in in Instellingen.') - } +export async function syncAll(): Promise<{ pushed: number; deleted: number; pulled: number }> { + const { getApiConfig } = await import('../screens/SettingsScreen') + const config = await getApiConfig() + if (!config) throw new Error('Geen API configuratie. Stel deze eerst in in Instellingen.') + const api = createApi(config) await initDatabase() - const local = await getAllMeasurements() - const remote = await fetchRemote(config) + const lastSyncAt = await SecureStore.getItemAsync(LAST_SYNC_KEY) - const merged = mergeMeasurements(local, remote, excludeIds) + const remote = await api.getMeasurements(lastSyncAt || undefined) + const local = await getAllMeasurements() + + const merged = mergeMeasurements(local, remote) for (const m of merged) { await upsertMeasurement(m) } - await uploadRemote(config, merged) + const since = lastSyncAt || '1970-01-01T00:00:00.000Z' + const updated = await getMeasurementsUpdatedSince(since) - return { - local: local.length, - remote: remote.length, - merged: merged.length, - } + let pushed = 0 + let deleted = 0 + + await Promise.allSettled( + updated.map(async m => { + if (m.deletedAt && m.deletedAt > since) { + try { + await api.deleteMeasurement(m.id) + await removeMeasurement(m.id) + deleted++ + } catch {} + } else { + try { + await api.createMeasurement(m) + pushed++ + } catch {} + } + }) + ) + + await SecureStore.setItemAsync(LAST_SYNC_KEY, new Date().toISOString()) + + return { pushed, deleted, pulled: remote.length } }