sync: herschrijf syncService voor timestamp-based REST sync
This commit is contained in:
parent
a13f2b821b
commit
cec6c54e61
1 changed files with 62 additions and 85 deletions
|
|
@ -1,111 +1,88 @@
|
||||||
import { Measurement } from '../types'
|
import { Measurement } from '../types'
|
||||||
import { initDatabase, getAllMeasurements, upsertMeasurement } from '../database/db'
|
import {
|
||||||
import { getWebDAVConfig } from '../screens/SettingsScreen'
|
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[]): Measurement[] {
|
||||||
|
|
||||||
export function mergeMeasurements(local: Measurement[], remote: Measurement[], excludeIds?: Set<string>): Measurement[] {
|
|
||||||
const map = new Map<string, Measurement>()
|
const map = new Map<string, Measurement>()
|
||||||
|
|
||||||
for (const m of local) {
|
for (const m of local) {
|
||||||
if (excludeIds?.has(m.id)) continue
|
|
||||||
map.set(m.id, m)
|
map.set(m.id, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const m of remote) {
|
for (const r of remote) {
|
||||||
if (excludeIds?.has(m.id)) continue
|
const existing = map.get(r.id)
|
||||||
const existing = map.get(m.id)
|
if (!existing || new Date(r.updatedAt) > new Date(existing.updatedAt)) {
|
||||||
if (!existing || new Date(m.updatedAt) > new Date(existing.updatedAt)) {
|
map.set(r.id, {
|
||||||
map.set(m.id, { ...m, syncStatus: 'synced' })
|
...r,
|
||||||
|
deletedAt: r.deletedAt || undefined,
|
||||||
|
} as Measurement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(map.values())
|
return Array.from(map.values())
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchRemote(config: { url: string; username: string; password: string }): Promise<Measurement[]> {
|
export async function uploadMeasurement(m: Measurement): Promise<void> {
|
||||||
const url = `${config.url.replace(/\/+$/, '')}/${WEBDAV_PATH}`
|
const { getApiConfig } = await import('../screens/SettingsScreen')
|
||||||
const response = await fetch(url, {
|
const config = await getApiConfig()
|
||||||
headers: {
|
if (!config) throw new Error('Geen API configuratie. Stel deze eerst in in Instellingen.')
|
||||||
Authorization: 'Basic ' + btoa(`${config.username}:${config.password}`),
|
const api = createApi(config)
|
||||||
},
|
await api.createMeasurement(m)
|
||||||
})
|
|
||||||
|
|
||||||
if (response.status === 404) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`WebDAV fetch failed: ${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createDir(config: { url: string; username: string; password: string }): Promise<void> {
|
export async function syncAll(): Promise<{ pushed: number; deleted: number; pulled: number }> {
|
||||||
const base = config.url.replace(/\/+$/, '')
|
const { getApiConfig } = await import('../screens/SettingsScreen')
|
||||||
const dirUrl = `${base}/weegschaal`
|
const config = await getApiConfig()
|
||||||
|
if (!config) throw new Error('Geen API configuratie. Stel deze eerst in in Instellingen.')
|
||||||
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.')
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const api = createApi(config)
|
||||||
await initDatabase()
|
await initDatabase()
|
||||||
|
|
||||||
const local = await getAllMeasurements()
|
const lastSyncAt = await SecureStore.getItemAsync(LAST_SYNC_KEY)
|
||||||
const remote = await fetchRemote(config)
|
|
||||||
|
|
||||||
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) {
|
for (const m of merged) {
|
||||||
await upsertMeasurement(m)
|
await upsertMeasurement(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
await uploadRemote(config, merged)
|
const since = lastSyncAt || '1970-01-01T00:00:00.000Z'
|
||||||
|
const updated = await getMeasurementsUpdatedSince(since)
|
||||||
|
|
||||||
return {
|
let pushed = 0
|
||||||
local: local.length,
|
let deleted = 0
|
||||||
remote: remote.length,
|
|
||||||
merged: merged.length,
|
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 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue