chore: commit all pending changes

This commit is contained in:
Trey t
2026-02-10 18:15:36 -06:00
parent b993ed3613
commit 53cc532ca9
51 changed files with 2583 additions and 268 deletions

View File

@@ -62,14 +62,55 @@ enum CloudKitError: Error, LocalizedError {
actor CloudKitService {
static let shared = CloudKitService()
nonisolated static let gameUpdatesSubscriptionID = "game-updates"
nonisolated static let teamUpdatesSubscriptionID = "team-updates"
nonisolated static let stadiumUpdatesSubscriptionID = "stadium-updates"
nonisolated static let leagueStructureUpdatesSubscriptionID = "league-structure-updates"
nonisolated static let teamAliasUpdatesSubscriptionID = "team-alias-updates"
nonisolated static let stadiumAliasUpdatesSubscriptionID = "stadium-alias-updates"
nonisolated static let sportUpdatesSubscriptionID = "sport-updates"
nonisolated static let canonicalSubscriptionIDs: Set<String> = [
gameUpdatesSubscriptionID,
teamUpdatesSubscriptionID,
stadiumUpdatesSubscriptionID,
leagueStructureUpdatesSubscriptionID,
teamAliasUpdatesSubscriptionID,
stadiumAliasUpdatesSubscriptionID,
sportUpdatesSubscriptionID
]
nonisolated static func recordType(forSubscriptionID subscriptionID: String) -> String? {
switch subscriptionID {
case gameUpdatesSubscriptionID:
return "Game"
case teamUpdatesSubscriptionID:
return "Team"
case stadiumUpdatesSubscriptionID:
return "Stadium"
case leagueStructureUpdatesSubscriptionID:
return "LeagueStructure"
case teamAliasUpdatesSubscriptionID:
return "TeamAlias"
case stadiumAliasUpdatesSubscriptionID:
return "StadiumAlias"
case sportUpdatesSubscriptionID:
return "Sport"
default:
return nil
}
}
private let container: CKContainer
private let publicDatabase: CKDatabase
/// Maximum records per CloudKit query (400 is the default limit)
private let recordsPerPage = 400
/// Re-fetch a small overlap window to avoid missing updates around sync boundaries.
private let deltaOverlapSeconds: TimeInterval = 120
private init() {
self.container = CKContainer(identifier: "iCloud.com.88oakapps.SportsTime")
// Use target entitlements (debug/prod) instead of hardcoding a container ID.
self.container = CKContainer.default()
self.publicDatabase = container.publicCloudDatabase
}
@@ -83,6 +124,8 @@ actor CloudKitService {
) async throws -> [CKRecord] {
var allRecords: [CKRecord] = []
var cursor: CKQueryOperation.Cursor?
var partialFailureCount = 0
var firstPartialFailure: Error?
// First page
let (firstResults, firstCursor) = try await publicDatabase.records(
@@ -91,8 +134,14 @@ actor CloudKitService {
)
for result in firstResults {
if case .success(let record) = result.1 {
switch result.1 {
case .success(let record):
allRecords.append(record)
case .failure(let error):
partialFailureCount += 1
if firstPartialFailure == nil {
firstPartialFailure = error
}
}
}
cursor = firstCursor
@@ -110,16 +159,51 @@ actor CloudKitService {
)
for result in results {
if case .success(let record) = result.1 {
switch result.1 {
case .success(let record):
allRecords.append(record)
case .failure(let error):
partialFailureCount += 1
if firstPartialFailure == nil {
firstPartialFailure = error
}
}
}
cursor = nextCursor
}
if partialFailureCount > 0 {
SyncLogger.shared.log("⚠️ [CK] \(query.recordType) query had \(partialFailureCount) per-record failures")
if allRecords.isEmpty, let firstPartialFailure {
throw firstPartialFailure
}
}
return allRecords
}
/// Normalizes a stored sync timestamp into a safe CloudKit delta start.
/// - Returns: `nil` when a full sync should be used.
private func effectiveDeltaStartDate(_ lastSync: Date?) -> Date? {
guard let lastSync else { return nil }
let now = Date()
if lastSync > now.addingTimeInterval(60) {
SyncLogger.shared.log("⚠️ [CK] Last sync timestamp is in the future; falling back to full sync")
return nil
}
return lastSync.addingTimeInterval(-deltaOverlapSeconds)
}
/// Fails fast when CloudKit returned records but none were parseable.
private func throwIfAllDropped(recordType: String, totalRecords: Int, parsedRecords: Int) throws {
guard totalRecords > 0, parsedRecords == 0 else { return }
let message = "All \(recordType) records were unparseable (\(totalRecords) fetched)"
SyncLogger.shared.log("❌ [CK] \(message)")
throw CloudKitError.serverError(message)
}
// MARK: - Sync Types (include canonical IDs from CloudKit)
struct SyncStadium {
@@ -130,7 +214,7 @@ actor CloudKitService {
struct SyncTeam {
let team: Team
let canonicalId: String
let stadiumCanonicalId: String
let stadiumCanonicalId: String?
}
struct SyncGame {
@@ -138,23 +222,29 @@ actor CloudKitService {
let canonicalId: String
let homeTeamCanonicalId: String
let awayTeamCanonicalId: String
let stadiumCanonicalId: String
let stadiumCanonicalId: String?
}
// MARK: - Availability Check
func isAvailable() async -> Bool {
let status = await checkAccountStatus()
return status == .available
switch status {
case .available, .noAccount, .couldNotDetermine:
// Public DB reads should still be attempted without an iCloud account.
return true
case .restricted, .temporarilyUnavailable:
return false
@unknown default:
return false
}
}
func checkAvailabilityWithError() async throws {
let status = await checkAccountStatus()
switch status {
case .available:
case .available, .noAccount:
return
case .noAccount:
throw CloudKitError.notSignedIn
case .restricted:
throw CloudKitError.permissionDenied
case .couldNotDetermine:
@@ -268,23 +358,39 @@ actor CloudKitService {
/// - lastSync: If nil, fetches all stadiums. If provided, fetches only stadiums modified since that date.
/// - cancellationToken: Optional token to check for cancellation between pages
func fetchStadiumsForSync(since lastSync: Date?, cancellationToken: SyncCancellationToken? = nil) async throws -> [SyncStadium] {
let log = SyncLogger.shared
let predicate: NSPredicate
if let lastSync = lastSync {
predicate = NSPredicate(format: "modificationDate >= %@", lastSync as NSDate)
if let deltaStart = effectiveDeltaStartDate(lastSync) {
log.log("☁️ [CK] Fetching stadiums modified since \(deltaStart.formatted()) (overlap window applied)")
predicate = NSPredicate(format: "modificationDate >= %@", deltaStart as NSDate)
} else {
log.log("☁️ [CK] Fetching ALL stadiums (full sync)")
predicate = NSPredicate(value: true)
}
let query = CKQuery(recordType: CKRecordType.stadium, predicate: predicate)
let records = try await fetchAllRecords(matching: query, cancellationToken: cancellationToken)
log.log("☁️ [CK] Received \(records.count) stadium records from CloudKit")
return records.compactMap { record -> SyncStadium? in
var validStadiums: [SyncStadium] = []
var skipped = 0
for record in records {
let ckStadium = CKStadium(record: record)
guard let stadium = ckStadium.stadium,
let canonicalId = ckStadium.canonicalId
else { return nil }
return SyncStadium(stadium: stadium, canonicalId: canonicalId)
else {
skipped += 1
continue
}
validStadiums.append(SyncStadium(stadium: stadium, canonicalId: canonicalId))
}
if skipped > 0 {
log.log("⚠️ [CK] Skipped \(skipped) stadium records due to missing required fields")
}
try throwIfAllDropped(recordType: CKRecordType.stadium, totalRecords: records.count, parsedRecords: validStadiums.count)
return validStadiums
}
/// Fetch teams for sync operations
@@ -292,24 +398,47 @@ actor CloudKitService {
/// - lastSync: If nil, fetches all teams. If provided, fetches only teams modified since that date.
/// - cancellationToken: Optional token to check for cancellation between pages
func fetchTeamsForSync(since lastSync: Date?, cancellationToken: SyncCancellationToken? = nil) async throws -> [SyncTeam] {
let log = SyncLogger.shared
let predicate: NSPredicate
if let lastSync = lastSync {
predicate = NSPredicate(format: "modificationDate >= %@", lastSync as NSDate)
if let deltaStart = effectiveDeltaStartDate(lastSync) {
log.log("☁️ [CK] Fetching teams modified since \(deltaStart.formatted()) (overlap window applied)")
predicate = NSPredicate(format: "modificationDate >= %@", deltaStart as NSDate)
} else {
log.log("☁️ [CK] Fetching ALL teams (full sync)")
predicate = NSPredicate(value: true)
}
let query = CKQuery(recordType: CKRecordType.team, predicate: predicate)
let records = try await fetchAllRecords(matching: query, cancellationToken: cancellationToken)
log.log("☁️ [CK] Received \(records.count) team records from CloudKit")
return records.compactMap { record -> SyncTeam? in
var validTeams: [SyncTeam] = []
var skipped = 0
var missingStadiumRef = 0
for record in records {
let ckTeam = CKTeam(record: record)
guard let team = ckTeam.team,
let canonicalId = ckTeam.canonicalId,
let stadiumCanonicalId = ckTeam.stadiumCanonicalId
else { return nil }
return SyncTeam(team: team, canonicalId: canonicalId, stadiumCanonicalId: stadiumCanonicalId)
let canonicalId = ckTeam.canonicalId
else {
skipped += 1
continue
}
let stadiumCanonicalId = ckTeam.stadiumCanonicalId
if stadiumCanonicalId == nil {
missingStadiumRef += 1
}
validTeams.append(SyncTeam(team: team, canonicalId: canonicalId, stadiumCanonicalId: stadiumCanonicalId))
}
if skipped > 0 {
log.log("⚠️ [CK] Skipped \(skipped) team records due to missing required fields")
}
if missingStadiumRef > 0 {
log.log("⚠️ [CK] \(missingStadiumRef) team records are missing stadium refs; merge will preserve local stadiums when possible")
}
try throwIfAllDropped(recordType: CKRecordType.team, totalRecords: records.count, parsedRecords: validTeams.count)
return validTeams
}
/// Fetch games for sync operations
@@ -319,9 +448,9 @@ actor CloudKitService {
func fetchGamesForSync(since lastSync: Date?, cancellationToken: SyncCancellationToken? = nil) async throws -> [SyncGame] {
let log = SyncLogger.shared
let predicate: NSPredicate
if let lastSync = lastSync {
log.log("☁️ [CK] Fetching games modified since \(lastSync.formatted())")
predicate = NSPredicate(format: "modificationDate >= %@", lastSync as NSDate)
if let deltaStart = effectiveDeltaStartDate(lastSync) {
log.log("☁️ [CK] Fetching games modified since \(deltaStart.formatted()) (overlap window applied)")
predicate = NSPredicate(format: "modificationDate >= %@", deltaStart as NSDate)
} else {
log.log("☁️ [CK] Fetching ALL games (full sync)")
predicate = NSPredicate(value: true)
@@ -334,23 +463,28 @@ actor CloudKitService {
var validGames: [SyncGame] = []
var skippedMissingIds = 0
var skippedInvalidGame = 0
var missingStadiumRef = 0
for record in records {
let ckGame = CKGame(record: record)
guard let canonicalId = ckGame.canonicalId,
let homeTeamCanonicalId = ckGame.homeTeamCanonicalId,
let awayTeamCanonicalId = ckGame.awayTeamCanonicalId,
let stadiumCanonicalId = ckGame.stadiumCanonicalId
let awayTeamCanonicalId = ckGame.awayTeamCanonicalId
else {
skippedMissingIds += 1
continue
}
let stadiumCanonicalId = ckGame.stadiumCanonicalId
if stadiumCanonicalId == nil {
missingStadiumRef += 1
}
guard let game = ckGame.game(
homeTeamId: homeTeamCanonicalId,
awayTeamId: awayTeamCanonicalId,
stadiumId: stadiumCanonicalId
stadiumId: stadiumCanonicalId ?? "stadium_placeholder_\(canonicalId)"
) else {
skippedInvalidGame += 1
continue
@@ -366,6 +500,10 @@ actor CloudKitService {
}
log.log("☁️ [CK] Parsed \(validGames.count) valid games (skipped: \(skippedMissingIds) missing IDs, \(skippedInvalidGame) invalid)")
if missingStadiumRef > 0 {
log.log("⚠️ [CK] \(missingStadiumRef) games are missing stadium refs; merge will derive stadiums from team mappings when possible")
}
try throwIfAllDropped(recordType: CKRecordType.game, totalRecords: records.count, parsedRecords: validGames.count)
// Log sport breakdown
var bySport: [String: Int] = [:]
@@ -443,20 +581,37 @@ actor CloudKitService {
/// - lastSync: If nil, fetches all records. If provided, fetches only records modified since that date.
/// - cancellationToken: Optional token to check for cancellation between pages
func fetchLeagueStructureChanges(since lastSync: Date?, cancellationToken: SyncCancellationToken? = nil) async throws -> [LeagueStructureModel] {
let log = SyncLogger.shared
let predicate: NSPredicate
if let lastSync = lastSync {
predicate = NSPredicate(format: "lastModified > %@", lastSync as NSDate)
if let deltaStart = effectiveDeltaStartDate(lastSync) {
log.log("☁️ [CK] Fetching league structures modified since \(deltaStart.formatted()) (overlap window applied)")
predicate = NSPredicate(format: "modificationDate >= %@", deltaStart as NSDate)
} else {
log.log("☁️ [CK] Fetching ALL league structures (full sync)")
predicate = NSPredicate(value: true)
}
let query = CKQuery(recordType: CKRecordType.leagueStructure, predicate: predicate)
query.sortDescriptors = [NSSortDescriptor(key: CKLeagueStructure.lastModifiedKey, ascending: true)]
let records = try await fetchAllRecords(matching: query, cancellationToken: cancellationToken)
log.log("☁️ [CK] Received \(records.count) league structure records from CloudKit")
return records.compactMap { record in
return CKLeagueStructure(record: record).toModel()
var parsed: [LeagueStructureModel] = []
var skipped = 0
for record in records {
if let model = CKLeagueStructure(record: record).toModel() {
parsed.append(model)
} else {
skipped += 1
}
}
if skipped > 0 {
log.log("⚠️ [CK] Skipped \(skipped) league structure records due to missing required fields")
}
try throwIfAllDropped(recordType: CKRecordType.leagueStructure, totalRecords: records.count, parsedRecords: parsed.count)
return parsed.sorted { lhs, rhs in
lhs.lastModified < rhs.lastModified
}
}
@@ -465,20 +620,37 @@ actor CloudKitService {
/// - lastSync: If nil, fetches all records. If provided, fetches only records modified since that date.
/// - cancellationToken: Optional token to check for cancellation between pages
func fetchTeamAliasChanges(since lastSync: Date?, cancellationToken: SyncCancellationToken? = nil) async throws -> [TeamAlias] {
let log = SyncLogger.shared
let predicate: NSPredicate
if let lastSync = lastSync {
predicate = NSPredicate(format: "lastModified > %@", lastSync as NSDate)
if let deltaStart = effectiveDeltaStartDate(lastSync) {
log.log("☁️ [CK] Fetching team aliases modified since \(deltaStart.formatted()) (overlap window applied)")
predicate = NSPredicate(format: "modificationDate >= %@", deltaStart as NSDate)
} else {
log.log("☁️ [CK] Fetching ALL team aliases (full sync)")
predicate = NSPredicate(value: true)
}
let query = CKQuery(recordType: CKRecordType.teamAlias, predicate: predicate)
query.sortDescriptors = [NSSortDescriptor(key: CKTeamAlias.lastModifiedKey, ascending: true)]
let records = try await fetchAllRecords(matching: query, cancellationToken: cancellationToken)
log.log("☁️ [CK] Received \(records.count) team alias records from CloudKit")
return records.compactMap { record in
return CKTeamAlias(record: record).toModel()
var parsed: [TeamAlias] = []
var skipped = 0
for record in records {
if let model = CKTeamAlias(record: record).toModel() {
parsed.append(model)
} else {
skipped += 1
}
}
if skipped > 0 {
log.log("⚠️ [CK] Skipped \(skipped) team alias records due to missing required fields")
}
try throwIfAllDropped(recordType: CKRecordType.teamAlias, totalRecords: records.count, parsedRecords: parsed.count)
return parsed.sorted { lhs, rhs in
lhs.lastModified < rhs.lastModified
}
}
@@ -487,20 +659,37 @@ actor CloudKitService {
/// - lastSync: If nil, fetches all records. If provided, fetches only records modified since that date.
/// - cancellationToken: Optional token to check for cancellation between pages
func fetchStadiumAliasChanges(since lastSync: Date?, cancellationToken: SyncCancellationToken? = nil) async throws -> [StadiumAlias] {
let log = SyncLogger.shared
let predicate: NSPredicate
if let lastSync = lastSync {
predicate = NSPredicate(format: "lastModified > %@", lastSync as NSDate)
if let deltaStart = effectiveDeltaStartDate(lastSync) {
log.log("☁️ [CK] Fetching stadium aliases modified since \(deltaStart.formatted()) (overlap window applied)")
predicate = NSPredicate(format: "modificationDate >= %@", deltaStart as NSDate)
} else {
log.log("☁️ [CK] Fetching ALL stadium aliases (full sync)")
predicate = NSPredicate(value: true)
}
let query = CKQuery(recordType: CKRecordType.stadiumAlias, predicate: predicate)
query.sortDescriptors = [NSSortDescriptor(key: CKStadiumAlias.lastModifiedKey, ascending: true)]
let records = try await fetchAllRecords(matching: query, cancellationToken: cancellationToken)
log.log("☁️ [CK] Received \(records.count) stadium alias records from CloudKit")
return records.compactMap { record in
return CKStadiumAlias(record: record).toModel()
var parsed: [StadiumAlias] = []
var skipped = 0
for record in records {
if let model = CKStadiumAlias(record: record).toModel() {
parsed.append(model)
} else {
skipped += 1
}
}
if skipped > 0 {
log.log("⚠️ [CK] Skipped \(skipped) stadium alias records due to missing required fields")
}
try throwIfAllDropped(recordType: CKRecordType.stadiumAlias, totalRecords: records.count, parsedRecords: parsed.count)
return parsed.sorted { lhs, rhs in
lhs.lastModified < rhs.lastModified
}
}
@@ -511,19 +700,36 @@ actor CloudKitService {
/// - lastSync: If nil, fetches all sports. If provided, fetches only sports modified since that date.
/// - cancellationToken: Optional token to check for cancellation between pages
func fetchSportsForSync(since lastSync: Date?, cancellationToken: SyncCancellationToken? = nil) async throws -> [CanonicalSport] {
let log = SyncLogger.shared
let predicate: NSPredicate
if let lastSync = lastSync {
predicate = NSPredicate(format: "modificationDate >= %@", lastSync as NSDate)
if let deltaStart = effectiveDeltaStartDate(lastSync) {
log.log("☁️ [CK] Fetching sports modified since \(deltaStart.formatted()) (overlap window applied)")
predicate = NSPredicate(format: "modificationDate >= %@", deltaStart as NSDate)
} else {
log.log("☁️ [CK] Fetching ALL sports (full sync)")
predicate = NSPredicate(value: true)
}
let query = CKQuery(recordType: CKRecordType.sport, predicate: predicate)
let records = try await fetchAllRecords(matching: query, cancellationToken: cancellationToken)
log.log("☁️ [CK] Received \(records.count) sport records from CloudKit")
return records.compactMap { record -> CanonicalSport? in
return CKSport(record: record).toCanonical()
var parsed: [CanonicalSport] = []
var skipped = 0
for record in records {
if let sport = CKSport(record: record).toCanonical() {
parsed.append(sport)
} else {
skipped += 1
}
}
if skipped > 0 {
log.log("⚠️ [CK] Skipped \(skipped) sport records due to missing required fields")
}
try throwIfAllDropped(recordType: CKRecordType.sport, totalRecords: records.count, parsedRecords: parsed.count)
return parsed
}
// MARK: - Sync Status
@@ -542,83 +748,130 @@ actor CloudKitService {
let subscription = CKQuerySubscription(
recordType: CKRecordType.game,
predicate: NSPredicate(value: true),
subscriptionID: "game-updates",
options: [.firesOnRecordCreation, .firesOnRecordUpdate]
subscriptionID: Self.gameUpdatesSubscriptionID,
options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
)
let notification = CKSubscription.NotificationInfo()
notification.shouldSendContentAvailable = true
subscription.notificationInfo = notification
try await publicDatabase.save(subscription)
try await saveSubscriptionIfNeeded(subscription)
}
func subscribeToTeamUpdates() async throws {
let subscription = CKQuerySubscription(
recordType: CKRecordType.team,
predicate: NSPredicate(value: true),
subscriptionID: Self.teamUpdatesSubscriptionID,
options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
)
let notification = CKSubscription.NotificationInfo()
notification.shouldSendContentAvailable = true
subscription.notificationInfo = notification
try await saveSubscriptionIfNeeded(subscription)
}
func subscribeToStadiumUpdates() async throws {
let subscription = CKQuerySubscription(
recordType: CKRecordType.stadium,
predicate: NSPredicate(value: true),
subscriptionID: Self.stadiumUpdatesSubscriptionID,
options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
)
let notification = CKSubscription.NotificationInfo()
notification.shouldSendContentAvailable = true
subscription.notificationInfo = notification
try await saveSubscriptionIfNeeded(subscription)
}
func subscribeToLeagueStructureUpdates() async throws {
let subscription = CKQuerySubscription(
recordType: CKRecordType.leagueStructure,
predicate: NSPredicate(value: true),
subscriptionID: "league-structure-updates",
options: [.firesOnRecordCreation, .firesOnRecordUpdate]
subscriptionID: Self.leagueStructureUpdatesSubscriptionID,
options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
)
let notification = CKSubscription.NotificationInfo()
notification.shouldSendContentAvailable = true
subscription.notificationInfo = notification
try await publicDatabase.save(subscription)
try await saveSubscriptionIfNeeded(subscription)
}
func subscribeToTeamAliasUpdates() async throws {
let subscription = CKQuerySubscription(
recordType: CKRecordType.teamAlias,
predicate: NSPredicate(value: true),
subscriptionID: "team-alias-updates",
options: [.firesOnRecordCreation, .firesOnRecordUpdate]
subscriptionID: Self.teamAliasUpdatesSubscriptionID,
options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
)
let notification = CKSubscription.NotificationInfo()
notification.shouldSendContentAvailable = true
subscription.notificationInfo = notification
try await publicDatabase.save(subscription)
try await saveSubscriptionIfNeeded(subscription)
}
func subscribeToStadiumAliasUpdates() async throws {
let subscription = CKQuerySubscription(
recordType: CKRecordType.stadiumAlias,
predicate: NSPredicate(value: true),
subscriptionID: "stadium-alias-updates",
options: [.firesOnRecordCreation, .firesOnRecordUpdate]
subscriptionID: Self.stadiumAliasUpdatesSubscriptionID,
options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
)
let notification = CKSubscription.NotificationInfo()
notification.shouldSendContentAvailable = true
subscription.notificationInfo = notification
try await publicDatabase.save(subscription)
try await saveSubscriptionIfNeeded(subscription)
}
func subscribeToSportUpdates() async throws {
let subscription = CKQuerySubscription(
recordType: CKRecordType.sport,
predicate: NSPredicate(value: true),
subscriptionID: "sport-updates",
options: [.firesOnRecordCreation, .firesOnRecordUpdate]
subscriptionID: Self.sportUpdatesSubscriptionID,
options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
)
let notification = CKSubscription.NotificationInfo()
notification.shouldSendContentAvailable = true
subscription.notificationInfo = notification
try await publicDatabase.save(subscription)
try await saveSubscriptionIfNeeded(subscription)
}
/// Subscribe to all canonical data updates
func subscribeToAllUpdates() async throws {
try await subscribeToScheduleUpdates()
try await subscribeToTeamUpdates()
try await subscribeToStadiumUpdates()
try await subscribeToLeagueStructureUpdates()
try await subscribeToTeamAliasUpdates()
try await subscribeToStadiumAliasUpdates()
try await subscribeToSportUpdates()
}
private func saveSubscriptionIfNeeded(_ subscription: CKQuerySubscription) async throws {
do {
try await publicDatabase.save(subscription)
} catch let error as CKError where error.code == .serverRejectedRequest {
// Existing subscriptions can be rejected as duplicates in some environments.
// Confirm it exists before treating this as non-fatal.
do {
_ = try await publicDatabase.subscription(for: subscription.subscriptionID)
SyncLogger.shared.log(" [CK] Subscription already exists: \(subscription.subscriptionID)")
} catch {
throw error
}
}
}
}