sync: herschrijf syncService voor timestamp-based REST sync

This commit is contained in:
eddy 2026-07-20 16:56:28 +02:00
commit cec6c54e61

View file

@ -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<string>): Measurement[] {
export function mergeMeasurements(local: Measurement[], remote: Measurement[]): Measurement[] {
const map = new Map<string, Measurement>()
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<Measurement[]> {
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<void> {
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<void> {
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<void> {
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<string>): Promise<SyncResult> {
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 }
}