Check the CloudKit schema when packaging and show broken iCloud sync

This commit is contained in:
Ian Gulliver
2026-10-20 08:55:37 -07:00
parent e7724977c8
commit 8f3226350e
14 changed files with 502 additions and 288 deletions
+39 -1
View File
@@ -1,3 +1,5 @@
import ByreAPI
import CloudKit
import CoreData
import Foundation
import Observation
@@ -11,6 +13,7 @@ final class Servers {
private(set) var entries: [(origin: String, record: ServerRecord)] = []
private(set) var connections: [String: Connection] = [:]
private var syncs: [NSPersistentCloudKitContainer.EventType: ClientSync] = [:]
var current: String?
@ObservationIgnored var openWindow: (() -> Void)?
@@ -19,7 +22,11 @@ final class Servers {
current = origins.first
Task {
for await note in NotificationCenter.default.notifications(named: NSPersistentCloudKitContainer.eventChangedNotification) {
if let event = note.userInfo?[NSPersistentCloudKitContainer.eventNotificationUserInfoKey] as? NSPersistentCloudKitContainer.Event, event.type == .import, event.endDate != nil {
guard let event = note.userInfo?[NSPersistentCloudKitContainer.eventNotificationUserInfoKey] as? NSPersistentCloudKitContainer.Event, let end = event.endDate else {
continue
}
synced(event, end)
if event.type == .import {
for connection in connections.values {
connection.store?.drafts?.reload()
}
@@ -29,6 +36,26 @@ final class Servers {
}
}
private func synced(_ event: NSPersistentCloudKitContainer.Event, _ end: Date) {
let error = event.succeeded ? nil : syncError(event.error)
if let error {
log.error("icloud \(String(describing: event.type), privacy: .public) failed: \(error, privacy: .public)")
}
syncs[event.type] = ClientSync(ok: event.succeeded, error: error, at: end)
guard let sync else {
return
}
for connection in connections.values {
connection.store?.reportSync(sync)
}
}
// A failed export stays the state until an export succeeds, whatever imports do meanwhile.
var sync: ClientSync? {
let failed = syncs.values.filter { !$0.ok }
return (failed.isEmpty ? Array(syncs.values) : failed).max { $0.at < $1.at }
}
var origins: [String] {
entries.map(\.origin)
}
@@ -103,6 +130,17 @@ final class Servers {
}
}
private func syncError(_ error: (any Error)?) -> String {
guard let error else {
return "failed with no error"
}
let ns = error as NSError
if let partial = ns.userInfo[CKPartialErrorsByItemIDKey] as? [AnyHashable: any Error], let first = partial.values.first {
return "\(first.localizedDescription) (\(partial.count) records)"
}
return "\(ns.localizedDescription) (\(ns.domain) \(ns.code))"
}
func serverName(_ origin: String) -> String {
let host = URL(string: origin)?.host() ?? origin
return host.split(separator: ".").first.map(String.init) ?? host