Poll sync cheaply and make the server deployable

Polling re-downloaded the whole store, photos included, every five
seconds. The app now checks a tiny version endpoint and only fetches the
payload when it actually moved, and the server gained request logging,
graceful shutdown, and self-hosting docs so it can run on a VPS instead
of only on localhost.

💘 Generated with Crush

Assisted-by: Crush:kimi-k3
This commit is contained in:
2026-10-08 10:19:55 +08:00
parent 88780f7701
commit f3e6fc8fa6
6 changed files with 189 additions and 10 deletions
+19 -7
View File
@@ -181,22 +181,34 @@ final class SyncService: ObservableObject {
}
private func poll() {
guard !isSyncing, let code, let url = endpoint("/api/sync/\(code)") else { return }
guard !isSyncing, let code, let versionURL = endpoint("/api/sync/\(code)/version") else { return }
Task {
do {
let (data, response) = try await URLSession.shared.data(from: url)
if (response as? HTTPURLResponse)?.statusCode == 404 {
// Cheap poll first: the full payload can be megabytes of
// base64 images, so only download it when the version moved.
let (versionData, versionResponse) = try await URLSession.shared.data(from: versionURL)
if (versionResponse as? HTTPURLResponse)?.statusCode == 404 {
lastError = "Sync group no longer exists on the server"
return
}
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
syncLog("poll HTTP \((response as? HTTPURLResponse)?.statusCode ?? -1)")
guard let versionHTTP = versionResponse as? HTTPURLResponse, versionHTTP.statusCode == 200 else {
syncLog("poll HTTP \((versionResponse as? HTTPURLResponse)?.statusCode ?? -1)")
return
}
let remoteVersion = try JSONDecoder().decode([String: Int64].self, from: versionData)["version"] ?? 0
guard remoteVersion > lastVersion else {
lastError = nil
return
}
syncLog("poll remote v\(remoteVersion) local v\(lastVersion) — fetching payload")
guard let payloadURL = endpoint("/api/sync/\(code)") else { return }
let (data, response) = try await URLSession.shared.data(from: payloadURL)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return }
let remote = try JSONDecoder().decode(SyncGetResponse.self, from: data)
syncLog("poll remote v\(remote.version) local v\(lastVersion)")
if remote.version > lastVersion, let payload = remote.data {
if let payload = remote.data {
apply(payload, version: remote.version)
} else {
lastVersion = remote.version
}
lastError = nil
} catch {